chore: init

This commit is contained in:
2025-09-27 14:27:24 +02:00
commit 7679f138b6
157 changed files with 5197 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
<script lang="ts">
import { page } from "$app/state";
import Button from "$lib/components/ui/button/button.svelte";
import * as InputOTP from "$lib/components/ui/input-otp";
import { eventTarget } from "$lib/ws.svelte";
import { toast } from "svelte-sonner";
const slug = $derived(page.params.slug);
let nextTicket = $state("");
let currentTicket = $state("");
let isInvalid = $state(false);
const TICKET_INPUT_REGEX = "^[A-B]{0,1}[0-9]{0,2}$";
const TICKET_REGEX = /^[A-B]\d{2}$/;
async function submit() {
if(!TICKET_REGEX.test(nextTicket)) {
isInvalid = true;
return;
}
if(currentTicket !== "") {
eventTarget.dispatchEvent(new CustomEvent("send", { detail: { type: "complete-ticket", ticket: currentTicket } }));
await new Promise(r => {
eventTarget.addEventListener("complete-ticket", (e) => {
const detail = (e as CustomEvent).detail;
if(detail.status === "completed" || detail.status === "no-show") {
r(true);
}
}, { once: true });
})
}
eventTarget.dispatchEvent(new CustomEvent("send", { detail: { type: "call-ticket", ticket: nextTicket, room: slug } }));
}
async function stopCurrent() {
if(!currentTicket) return;
eventTarget.dispatchEvent(new CustomEvent("send", { detail: { type: "complete-ticket", ticket: currentTicket } }));
}
async function noShow() {
if(!currentTicket) return;
eventTarget.dispatchEvent(new CustomEvent("send", { detail: { type: "no-show", ticket: currentTicket } }));
}
$effect(() => {
eventTarget.addEventListener("error", (e) => {
const detail = (e as CustomEvent).detail;
if(detail.code === -3) {
toast.error(`Ticket ${detail.num} existiert nicht oder ist bereits abgeschlossen / nicht erschienen`);
isInvalid = true;
}
});
eventTarget.addEventListener("current-ticket", (e) => {
const detail = (e as CustomEvent).detail;
if(detail.room === slug) {
currentTicket = detail.num ?? "";
}
})
eventTarget.addEventListener("call-ticket", (e) => {
const detail = (e as CustomEvent).detail;
if(detail.status === "called" && detail.room === slug) {
currentTicket = detail.num;
nextTicket = "";
}
})
eventTarget.addEventListener("complete-ticket", (e) => {
const detail = (e as CustomEvent).detail;
if(detail.status === "completed" || detail.status === "no-show") {
currentTicket = "";
}
if(detail.status === "completed") {
toast.success(`Ticket ${detail.num} abgeschlossen`);
} else if(detail.status === "no-show") {
toast.success(`Ticket ${detail.num} nicht erschienen`);
}
})
eventTarget.dispatchEvent(new CustomEvent("send", { detail: { type: "get-current-ticket", room: slug } }));
})
</script>
<h1 class="text-2xl font-bold">Raum {slug}</h1>
<div style="display: flex; align-items: center; gap: 1rem;">
<InputOTP.Root maxlength={3} pattern={TICKET_INPUT_REGEX} bind:value={nextTicket} onkeypress={(e) => {
isInvalid = false;
if (e.key === "Enter") {
submit();
}
}}>
{#snippet children({ cells })}
<InputOTP.Group>
{#each cells.slice(0, 3) as cell}
<InputOTP.Slot {cell} aria-invalid={isInvalid} />
{/each}
</InputOTP.Group>
{/snippet}
</InputOTP.Root>
<Button onclick={() => {
submit();
}}>Aufrufen</Button>
<Button variant="destructive" onclick={() => {
noShow();
}}>Nicht erschienen</Button>
<Button variant="secondary" onclick={() => {
stopCurrent();
}}>Erledigen</Button>
</div>
{#if currentTicket !== ""}
<div class="flex flex-col items-center mt-4 gap-2">
<span>Jetzt Aufgerufen:</span>
<span id="new-ticket">{currentTicket}</span>
</div>
{/if}
<style>
#new-ticket {
font-size: 2rem;
font-family: monospace;
padding: 0.5rem 1rem;
border: 2px solid var(--border);
border-radius: 0.5rem;
background: var(--background);
box-shadow: var(--shadow);
user-select: none;
text-align: center;
}
#ticket {
display: flex;
}
</style>