chore: init
This commit is contained in:
92
web/src/routes/(manage)/+layout.svelte
Normal file
92
web/src/routes/(manage)/+layout.svelte
Normal file
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import '../../app.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar';
|
||||
import AppSidebar from '$lib/AppSidebar.svelte';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import * as Breadcrumb from '$lib/components/ui/breadcrumb';
|
||||
import Spinner from '$lib/Spinner.svelte';
|
||||
import { connectWS, wsState } from '$lib/ws.svelte';
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
import ConnectionLostDialog from '$lib/ConnectionLostDialog.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
onMount(() => {
|
||||
if(!browser) return;
|
||||
|
||||
connectWS();
|
||||
console.log("Connecting to WebSocket...");
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
{#if wsState.closed}
|
||||
<ConnectionLostDialog />
|
||||
{/if}
|
||||
|
||||
{#if !wsState.connected && !wsState.closed}
|
||||
<div id="blocker">
|
||||
<Spinner />
|
||||
|
||||
<h2 class="text-2xl">Verbindung herstellen...</h2>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Toaster position="bottom-right" theme="dark" richColors={true} />
|
||||
|
||||
<Sidebar.Provider>
|
||||
<AppSidebar />
|
||||
<Sidebar.Inset>
|
||||
<!-- <header class="flex h-16 shrink-0 items-center gap-2 px-4"> -->
|
||||
<header class="flex h-2 shrink-0 items-center gap-2 px-4">
|
||||
<!-- <Sidebar.Trigger class="-ml-1" /> -->
|
||||
<!-- <Separator orientation="vertical" class="mr-2 data-[orientation=vertical]:h-4" /> -->
|
||||
<!-- <Breadcrumb.Root>
|
||||
<Breadcrumb.List>
|
||||
<Breadcrumb.Item class="hidden md:block">
|
||||
<Breadcrumb.Link href="#">Building Your Application</Breadcrumb.Link>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Separator class="hidden md:block" />
|
||||
<Breadcrumb.Item>
|
||||
<Breadcrumb.Page>Data Fetching</Breadcrumb.Page>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb.List>
|
||||
</Breadcrumb.Root> -->
|
||||
</header>
|
||||
<div class="flex flex-col gap-2">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<!-- <div class="flex flex-1 flex-col gap-4 p-4">
|
||||
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div class="bg-muted/50 aspect-video rounded-xl"></div>
|
||||
<div class="bg-muted/50 aspect-video rounded-xl"></div>
|
||||
<div class="bg-muted/50 aspect-video rounded-xl"></div>
|
||||
</div>
|
||||
<div class="bg-muted/50 min-h-[100vh] flex-1 rounded-xl md:min-h-min"></div>
|
||||
</div> -->
|
||||
</Sidebar.Inset>
|
||||
</Sidebar.Provider>
|
||||
|
||||
<style>
|
||||
#blocker {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(5px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
43
web/src/routes/(manage)/+page.svelte
Normal file
43
web/src/routes/(manage)/+page.svelte
Normal file
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { eventTarget } from "$lib/ws.svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let noRoom = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
// ws.addEventListener("message", (e) => {
|
||||
// const msg = JSON.parse(e.data);
|
||||
// if(msg.type === "my-room") {
|
||||
// goto(`/room/${msg.room}`);
|
||||
// }
|
||||
// });
|
||||
// ws.addEventListener("open", () => {
|
||||
// ws.send(JSON.stringify({ type: "my-room" }));
|
||||
// });
|
||||
eventTarget.addEventListener("my-room", (e) => {
|
||||
console.log("Received my-room event", e);
|
||||
const ev = e as CustomEvent;
|
||||
// if(ev.detail.room == "entry") {
|
||||
// goto(`/entry`);
|
||||
// return;
|
||||
// }
|
||||
goto(`/room/${ev.detail.room}`);
|
||||
});
|
||||
eventTarget.addEventListener("error", (e) => {
|
||||
const ev = e as CustomEvent;
|
||||
if(ev.detail.code === -1) {
|
||||
noRoom = true;
|
||||
}
|
||||
});
|
||||
eventTarget.addEventListener("hello", (e) => {
|
||||
eventTarget.dispatchEvent(new CustomEvent("send", { detail: { type: "my-room" } }));
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<h1>Willkommen zu NextUp!</h1>
|
||||
<span>Sie werden zu Ihrem Raum weitergeleitet.</span>
|
||||
{#if noRoom}
|
||||
<span class="text-red-500">Kein Raum verfügbar</span>
|
||||
{/if}
|
||||
1
web/src/routes/(manage)/admin/+page.svelte
Normal file
1
web/src/routes/(manage)/admin/+page.svelte
Normal file
@@ -0,0 +1 @@
|
||||
ADMIN
|
||||
120
web/src/routes/(manage)/entry/+page.svelte
Normal file
120
web/src/routes/(manage)/entry/+page.svelte
Normal file
@@ -0,0 +1,120 @@
|
||||
<!-- <script>
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
|
||||
let newTicket = $state("---");
|
||||
</script>
|
||||
|
||||
<h1 class="text-2xl font-bold">Empfang</h1>
|
||||
<Button onclick={() => {
|
||||
newTicket = Math.floor(100 + Math.random() * 900) + "";
|
||||
}}>Neues Ticket</Button>
|
||||
|
||||
<div class="flex flex-col items-center mt-4 gap-2">
|
||||
<span>Neues Ticket:</span>
|
||||
<span id="new-ticket">{newTicket}</span>
|
||||
<Button variant="secondary" onclick={() => {
|
||||
newTicket = "---";
|
||||
}}>Leeren</Button>
|
||||
</div>
|
||||
|
||||
<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;
|
||||
}
|
||||
</style> -->
|
||||
|
||||
<!-- <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 { onMount } from "svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
const slug = $derived(page.params.slug);
|
||||
|
||||
let nextTicket = $state("");
|
||||
|
||||
let isInvalid = $state(false);
|
||||
|
||||
const TICKET_INPUT_REGEX = "^[A-Z]{0,1}[0-9]{0,2}$";
|
||||
const TICKET_REGEX = /^[A-Z]\d{2}$/;
|
||||
|
||||
onMount(() => {
|
||||
eventTarget.addEventListener("error", (e) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if(detail.code === -2) {
|
||||
toast.error(`Ticket ${detail.num} existiert bereits und wartet / ist aufgerufen`);
|
||||
isInvalid = true;
|
||||
}
|
||||
});
|
||||
eventTarget.addEventListener("create-ticket", (e) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if(detail.status === "created") {
|
||||
toast.success(`Ticket ${detail.num} eingetragen`);
|
||||
nextTicket = "";
|
||||
}
|
||||
});
|
||||
eventTarget.addEventListener("complete-ticket", (e) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if(detail.status === "skipped") {
|
||||
toast.success(`Ticket ${detail.num} übersprungen`);
|
||||
} else if(detail.status === "completed") {
|
||||
toast.success(`Ticket ${detail.num} abgeschlossen`);
|
||||
} else if(detail.status === "no-show") {
|
||||
toast.success(`Ticket ${detail.num} nicht erschienen`);
|
||||
}
|
||||
nextTicket = "";
|
||||
});
|
||||
})
|
||||
|
||||
function submit() {
|
||||
if(!TICKET_REGEX.test(nextTicket)) {
|
||||
isInvalid = true;
|
||||
return;
|
||||
}
|
||||
|
||||
eventTarget.dispatchEvent(new CustomEvent("send", { detail: { type: "create-ticket", ticket: nextTicket } }));
|
||||
}
|
||||
|
||||
function remove() {
|
||||
if(!TICKET_REGEX.test(nextTicket)) {
|
||||
isInvalid = true;
|
||||
return;
|
||||
}
|
||||
|
||||
eventTarget.dispatchEvent(new CustomEvent("send", { detail: { type: "complete-ticket", ticket: nextTicket } }));
|
||||
}
|
||||
</script>
|
||||
|
||||
<h1 class="text-2xl font-bold">Empfang</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();
|
||||
}}>Eintragen</Button>
|
||||
<Button variant="destructive" onclick={() => {
|
||||
remove();
|
||||
}}>Entfernen</Button>
|
||||
</div> -->
|
||||
68
web/src/routes/(manage)/log/+page.svelte
Normal file
68
web/src/routes/(manage)/log/+page.svelte
Normal file
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import * as Table from "$lib/components/ui/table";
|
||||
import { eventTarget } from "$lib/ws.svelte";
|
||||
import { TicketCheckIcon, TicketIcon, TicketSlashIcon, TicketXIcon } from "@lucide/svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
type LogAction = "called" | "completed" | "no-show";
|
||||
const colors = {
|
||||
called: "var(--chart-1)",
|
||||
completed: "var(--chart-2)",
|
||||
"no-show": "var(--destructive)",
|
||||
}
|
||||
const text = {
|
||||
called: "Aufgerufen",
|
||||
completed: "Erledigt",
|
||||
"no-show": "Nicht erschienen",
|
||||
}
|
||||
const icons = {
|
||||
called: TicketSlashIcon,
|
||||
completed: TicketCheckIcon,
|
||||
"no-show": TicketXIcon,
|
||||
}
|
||||
|
||||
interface LogEntry {
|
||||
ticket: string;
|
||||
time: string;
|
||||
action: LogAction;
|
||||
room: string | null;
|
||||
};
|
||||
|
||||
let logs = $state<LogEntry[]>([]);
|
||||
|
||||
onMount(() => {
|
||||
eventTarget.addEventListener("log", (e) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
logs = detail.log;
|
||||
})
|
||||
|
||||
eventTarget.dispatchEvent(new CustomEvent("send", { detail: { type: "get-log" } }));
|
||||
})
|
||||
</script>
|
||||
|
||||
<h1 class="text-2xl font-bold">Log</h1>
|
||||
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Ticketnr.</Table.Head>
|
||||
<Table.Head>Zeit</Table.Head>
|
||||
<Table.Head>Aktion</Table.Head>
|
||||
<Table.Head>Raum</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each logs as log}
|
||||
<Table.Row>
|
||||
<Table.Cell class="font-medium">{log.ticket}</Table.Cell>
|
||||
<Table.Cell>{new Date(log.time).toLocaleString("de-de")}</Table.Cell>
|
||||
<Table.Cell style="display: flex; align-items: center; gap: 0.25rem; color: {colors[log.action]};">
|
||||
{@const Icon = icons[log.action]}
|
||||
<Icon />
|
||||
{text[log.action]}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{log.room}</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
135
web/src/routes/(manage)/room/[slug]/+page.svelte
Normal file
135
web/src/routes/(manage)/room/[slug]/+page.svelte
Normal 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>
|
||||
100
web/src/routes/(manage)/ticket/[ticket]/+page.svelte
Normal file
100
web/src/routes/(manage)/ticket/[ticket]/+page.svelte
Normal file
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import Badge from "$lib/components/ui/badge/badge.svelte";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
const ticket = $derived(page.params.ticket);
|
||||
|
||||
type TicketStatus = "waiting" | "called" | "skipped" | "done";
|
||||
|
||||
const ticketInfo = $state({
|
||||
created: new Date(),
|
||||
status: "called" as TicketStatus,
|
||||
room: "A" as string | undefined,
|
||||
calledDate: new Date() as Date | undefined,
|
||||
})
|
||||
ticketInfo.created.setHours(ticketInfo.created.getHours() - Math.floor(Math.random() * 48));
|
||||
|
||||
onMount(() => {
|
||||
setInterval(() => {
|
||||
ticketInfo.status = (["waiting", "called", "skipped", "done"] as TicketStatus[])[Math.floor(Math.random() * 4)];
|
||||
if(ticketInfo.status === "called") {
|
||||
ticketInfo.calledDate = new Date();
|
||||
ticketInfo.calledDate.setMinutes(ticketInfo.calledDate.getMinutes() - 10);
|
||||
} else if(ticketInfo.status === "waiting") {
|
||||
ticketInfo.calledDate = undefined;
|
||||
}
|
||||
if(ticketInfo.status === "called" || ticketInfo.status === "skipped" || ticketInfo.status === "done") {
|
||||
ticketInfo.room = ["A", "B"][Math.floor(Math.random() * 2)];
|
||||
} else {
|
||||
ticketInfo.room = undefined;
|
||||
}
|
||||
}, 3000);
|
||||
})
|
||||
|
||||
const rtf = new Intl.RelativeTimeFormat("de", { numeric: "auto" });
|
||||
|
||||
function timeAgo(date: Date) {
|
||||
const now = new Date();
|
||||
const diff = (date.getTime() - now.getTime()) / 1000; // in seconds
|
||||
|
||||
const units: [Intl.RelativeTimeFormatUnit, number][] = [
|
||||
["year", 60 * 60 * 24 * 365],
|
||||
["month", 60 * 60 * 24 * 30],
|
||||
["week", 60 * 60 * 24 * 7],
|
||||
["day", 60 * 60 * 24],
|
||||
["hour", 60 * 60],
|
||||
["minute", 60],
|
||||
["second", 1],
|
||||
];
|
||||
|
||||
for (const [unit, secondsInUnit] of units) {
|
||||
if (Math.abs(diff) >= secondsInUnit || unit === "second") {
|
||||
return rtf.format(Math.round(diff / secondsInUnit), unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ticketStatus(status: TicketStatus) {
|
||||
switch (status) {
|
||||
case "waiting": return "Wartet";
|
||||
case "called": return "Aufgerufen";
|
||||
case "skipped": return "Übersprungen";
|
||||
case "done": return "Erledigt";
|
||||
}
|
||||
}
|
||||
|
||||
function ticketStatusColor(status: TicketStatus) {
|
||||
switch (status) {
|
||||
case "waiting": return "secondary";
|
||||
case "called": return "default";
|
||||
case "skipped": return "destructive";
|
||||
case "done": return "outline";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<h1 class="text-2xl font-bold flex items-center gap-2">Ticket {ticket} <Badge variant={ticketStatusColor(ticketInfo.status)}>{ticketStatus(ticketInfo.status)}</Badge></h1>
|
||||
|
||||
<span>Geöffnet <span class="bg-accent p-2 rounded-md" title={Intl.DateTimeFormat("de", { dateStyle: "medium", timeStyle: "short" }).format(ticketInfo.created)}>{timeAgo(ticketInfo.created)}</span></span>
|
||||
|
||||
{#if ticketInfo.room}
|
||||
<span>Aufgerufen in: <span class="bg-accent p-2 rounded-md">{ticketInfo.room}</span></span>
|
||||
{/if}
|
||||
{#if ticketInfo.calledDate}
|
||||
<span>Aufgerufen <span class="bg-accent p-2 rounded-md" title={Intl.DateTimeFormat("de", { dateStyle: "medium", timeStyle: "short" }).format(ticketInfo.calledDate)}>{timeAgo(ticketInfo.calledDate)}</span></span>
|
||||
{/if}
|
||||
|
||||
{#if ticketInfo.status != "done"}
|
||||
<Button variant="destructive" onclick={() => {
|
||||
ticketInfo.status = "done";
|
||||
}}>Ticket schließen</Button>
|
||||
{:else}
|
||||
<Button variant="secondary" onclick={() => {
|
||||
ticketInfo.status = "waiting";
|
||||
ticketInfo.calledDate = undefined;
|
||||
ticketInfo.room = undefined;
|
||||
ticketInfo.created = new Date();
|
||||
}}>Ticket wieder öffnen</Button>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user