(add): Tickets Logic

This commit is contained in:
2026-06-23 18:26:31 -04:00
parent 80c747226f
commit e5b6fbd7c5
20 changed files with 827 additions and 104 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
data /data
*/__pycache__ */__pycache__
Binary file not shown.
+65
View File
@@ -0,0 +1,65 @@
from dataclasses import dataclass
from db import RepoTemplate
from system.auth import AuthRepo
from fastapi import APIRouter, Header
@dataclass
class Ticket:
prefix: str
t_id: int
first_name: str = ""
last_name: str = ""
phone_number: str = ""
pref: str = ""
class TicketRepo(RepoTemplate):
def get_all_tickets(self):
self.cur.execute("SELECT * FROM tickets ORDER BY prefix, t_id")
results = self.cur.fetchall()
return [Ticket(*r) for r in results]
def get_prefix_tickets(self, prefix: str):
self.cur.execute("SELECT * FROM tickets WHERE prefix = ? ORDER BY prefix, t_id", (prefix,))
results = self.cur.fetchall()
return [Ticket(*r) for r in results]
def get_single_ticket(self, prefix: str, id: int):
self.cur.execute("SELECT * FROM tickets WHERE prefix = ? AND t_id = ? ORDER BY prefix, t_id", (prefix, id))
results = self.cur.fetchall()
return [Ticket(*r) for r in results]
def get_range_tickets(self, prefix: str, id_from: int, id_to: int):
self.cur.execute("SELECT * FROM tickets WHERE prefix = ? AND t_id BETWEEN ? AND ? ORDER BY prefix, t_id", (prefix, id_from, id_to))
results = self.cur.fetchall()
return [Ticket(*r) for r in results]
def post_tickets(self, ts: list[Ticket]):
for t in ts:
self.cur.execute("""INSERT INTO tickets VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (prefix, t_id) DO UPDATE SET
first_name = EXCLUDED.first_name, last_name = EXCLUDED.last_name, phone_number = EXCLUDED.phone_number,
pref = EXCLUDED.pref""", (t.prefix, t.t_id, t.first_name, t.last_name, t.phone_number, t.pref))
self.conn.commit()
return ts
tickets_router = APIRouter(prefix="/api/tickets")
@tickets_router.get("")
def get_all_tickets(tam_key: str = Header("")):
AuthRepo().verify_key(tam_key)
return TicketRepo().get_all_tickets()
@tickets_router.get("/{prefix}")
def get_prefix_tickets(prefix: str, tam_key: str = Header("")):
AuthRepo().verify_key(tam_key)
return TicketRepo().get_prefix_tickets(prefix)
@tickets_router.get("/{prefix}/{t_id}")
def get_single_ticket(prefix: str, t_id: int, tam_key: str = Header("")):
AuthRepo().verify_key(tam_key)
return TicketRepo().get_single_ticket(prefix, t_id)
@tickets_router.get("/{prefix}/{id_from}/{id_to}")
def get_range_tickets(prefix: str, id_from: int, id_to: int, tam_key: str = Header("")):
AuthRepo().verify_key(tam_key)
return TicketRepo().get_range_tickets(prefix, id_from, id_to)
@tickets_router.post("")
def post_tickets(ts: list[Ticket], tam_key: str = Header("")):
AuthRepo().verify_key(tam_key)
return TicketRepo().post_tickets(ts)
+2
View File
@@ -17,6 +17,8 @@ def init_db():
conn, cur = session() conn, cur = session()
cur.execute("CREATE TABLE IF NOT EXISTS auth_keys (auth_key TEXT PRIMARY KEY, description TEXT)") cur.execute("CREATE TABLE IF NOT EXISTS auth_keys (auth_key TEXT PRIMARY KEY, description TEXT)")
cur.execute("CREATE TABLE IF NOT EXISTS prefixes (prefix TEXT PRIMARY KEY, color TEXT, weight INTEGER)") cur.execute("CREATE TABLE IF NOT EXISTS prefixes (prefix TEXT PRIMARY KEY, color TEXT, weight INTEGER)")
cur.execute("""CREATE TABLE IF NOT EXISTS tickets (prefix TEXT, t_id INTEGER, first_name TEXT, last_name TEXT,
phone_number TEXT, pref TEXT, PRIMARY KEY (prefix, t_id))""")
conn.commit() conn.commit()
conn.close() conn.close()
+2
View File
@@ -1,7 +1,9 @@
from fastapi import FastAPI from fastapi import FastAPI
from system.auth import auth_router from system.auth import auth_router
from system.prefixes import prefix_router from system.prefixes import prefix_router
from data.tickets import tickets_router
def imp_routers(app: FastAPI): def imp_routers(app: FastAPI):
app.include_router(auth_router) app.include_router(auth_router)
app.include_router(prefix_router) app.include_router(prefix_router)
app.include_router(tickets_router)
@@ -0,0 +1,67 @@
<script>
import { bS } from "../styles";
import { browser } from "$app/environment";
import hotkeys from "hotkeys-js";
let { prefix, functions } = $props();
if (browser) {
hotkeys.filter = () => {return true};
hotkeys('alt+l', (e) => {
e.preventDefault();
if (functions.nextLine) functions.nextLine();
});
hotkeys('alt+o', (e) => {
e.preventDefault();
if (functions.prevLine) functions.prevLine();
});
hotkeys('alt+j', (e) => {
e.preventDefault();
if (functions.dupDown) functions.dupDown();
});
hotkeys('alt+u', (e) => {
e.preventDefault();
if (functions.dupUp) functions.dupUp();
});
hotkeys('alt+c', (e) => {
e.preventDefault();
if (functions.copy) functions.copy();
});
hotkeys('alt+v', (e) => {
e.preventDefault();
if (functions.paste) functions.paste();
});
hotkeys('alt+s', (e) => {
e.preventDefault();
if (functions.save) functions.save();
});
}
</script>
<div class="flex flex-row gap-1 py-1 justify-between">
<div class="flex flex-row gap-1">
{#if functions.dupDown}
<button class={bS[prefix.color]} title="Alt + J" onclick={() => functions.dupDown()}>Duplicate Down</button>
{/if}
{#if functions.dupUp}
<button class={bS[prefix.color]} title="Alt + U" onclick={() => functions.dupUp()}>Duplicate Up</button>
{/if}
{#if functions.nextLine}
<button class={bS[prefix.color]} title="Alt + L" onclick={() => functions.nextLine()}>Next Line</button>
{/if}
{#if functions.prevLine}
<button class={bS[prefix.color]} title="Alt + O" onclick={() => functions.prevLine()}>Previous Line</button>
{/if}
{#if functions.copy}
<button class={bS[prefix.color]} title="Alt + C" onclick={() => functions.copy()}>Copy</button>
{/if}
{#if functions.paste}
<button class={bS[prefix.color]} title="Alt + V" onclick={() => functions.paste()}>Paste</button>
{/if}
</div>
<div class="flex flex-row gap-1">
{#if functions.save}
<button class={bS[prefix.color]} title="Alt + S" onclick={() => functions.save()}>Save Marked</button>
{/if}
</div>
</div>
@@ -0,0 +1,79 @@
<script>
import { browser } from '$app/environment';
import hotkeys from 'hotkeys-js';
import { bS, iS } from '../styles';
let { prefix, functions, pager = $bindable() } = $props();
if (browser) {
hotkeys.filter = () => {
return true;
};
hotkeys('alt+q', (e) => {
e.preventDefault();
const id_from = document.getElementById('id_from');
if (id_from) id_from.select();
});
hotkeys('alt+w', (e) => {
e.preventDefault();
const id_to = document.getElementById('id_to');
if (id_to) id_to.select();
});
hotkeys('alt+b', (e) => {
e.preventDefault();
if (functions.prevPage) functions.prevPage();
});
hotkeys('alt+n', (e) => {
e.preventDefault();
if (functions.nextPage) functions.nextPage();
});
}
</script>
<div class="flex flex-row justify-between gap-1 p-1">
<div class="flex flex-row gap-1">
<input
type="number"
id="id_from"
class={iS.normal}
onclick={(e) => {
e.target.select();
}}
bind:value={pager.idFrom}
/>
<div>-</div>
<input
type="number"
id="id_to"
class={iS.normal}
onclick={(e) => {
e.target.select();
}}
bind:value={pager.idTo}
/>
<button
class={bS[prefix.color]}
onclick={() => {
functions.getPage();
}}>Go</button
>
</div>
<div class="flex flex-row gap-1">
{#if functions.prevPage}
<button
class={bS[prefix.color]}
onclick={() => {
functions.prevPage();
}}>Prev Page</button
>
{/if}
{#if functions.nextPage}
<button
class={bS[prefix.color]}
onclick={() => {
functions.nextPage();
}}>Next Page</button
>
{/if}
</div>
</div>
+10
View File
@@ -14,6 +14,16 @@ export const bS = {
red: 'px-2 py-1 bg-red-300 border border-gray-700 rounded hover:bg-red-400 cursor-pointer' red: 'px-2 py-1 bg-red-300 border border-gray-700 rounded hover:bg-red-400 cursor-pointer'
} }
export const bAS = {
gray: 'px-2 py-1 bg-gray-300 border-4 border-gray-800 rounded hover:bg-gray-400 cursor-pointer',
white: 'px-2 py-1 bg-white border-4 border-gray-700 rounded hover:bg-gray-200 cursor-pointer',
blue: 'px-2 py-1 bg-blue-300 border-4 border-gray-700 rounded hover:bg-blue-400 cursor-pointer',
yellow: 'px-2 py-1 bg-yellow-300 border-4 border-gray-700 rounded hover:bg-yellow-400 cursor-pointer',
green: 'px-2 py-1 bg-green-300 border-4 border-gray-700 rounded hover:bg-green-400 cursor-pointer',
orange: 'px-2 py-1 bg-orange-300 border-4 border-gray-700 rounded hover:bg-orange-400 cursor-pointer',
red: 'px-2 py-1 bg-red-300 border-4 border-gray-700 rounded hover:bg-red-400 cursor-pointer'
}
export const iS = { export const iS = {
normal: 'p-1 border border-black' normal: 'p-1 border border-black'
} }
+10 -1
View File
@@ -1,7 +1,16 @@
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; import { integer, sqliteTable, text, primaryKey } from 'drizzle-orm/sqlite-core';
export const prefixes = sqliteTable('prefixes', { export const prefixes = sqliteTable('prefixes', {
prefix: text('prefix').primaryKey(), prefix: text('prefix').primaryKey(),
color: text('color'), color: text('color'),
weight: integer('weight') weight: integer('weight')
}); });
export const tickets = sqliteTable('tickets', {
prefix: text('prefix'),
t_id: integer('t_id'),
first_name: text('first_name'),
last_name: text('last_name'),
phone_number: text('phone_number'),
pref: text('pref')
}, (t) => [primaryKey({columns: [t.prefix, t.t_id]})])
+3 -1
View File
@@ -4,5 +4,7 @@ export const load = async ({ fetch }) => {
const settings = getSettings(); const settings = getSettings();
const res = await fetch('/api'); const res = await fetch('/api');
const data = await res.json(); const data = await res.json();
return { ...data, venueName: settings.venue_name }; const resPrefixes = await fetch('/api/prefixes');
const dataPrefixes = await resPrefixes.json();
return { ...data, venueName: settings.venue_name, prefixes: dataPrefixes };
}; };
+32 -2
View File
@@ -1,5 +1,5 @@
<script> <script>
import { tS, bS } from '$lib/client/styles.js'; import { tS, bS, bAS } from '$lib/client/styles.js';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import hotkeys from 'hotkeys-js'; import hotkeys from 'hotkeys-js';
@@ -7,6 +7,12 @@
const pageTitle = 'Main Menu | TAM' const pageTitle = 'Main Menu | TAM'
const { data } = $props(); const { data } = $props();
let adminMode = $state(false); let adminMode = $state(false);
let prefixes = $derived(data.prefixes);
let curPrefix = $state("");
let pColor = $derived.by(() => {
if (curPrefix) return prefixes.find(p => curPrefix == p.prefix).color
else return "gray";
})
const status = $derived.by(() => { const status = $derived.by(() => {
if (data.whoami === 'TAM Server') { if (data.whoami === 'TAM Server') {
@@ -43,8 +49,29 @@
<h1 class="text-xl font-bold">{pageTitle}</h1> <h1 class="text-xl font-bold">{pageTitle}</h1>
<p class="text-lg italic">{data.venueName}</p> <p class="text-lg italic">{data.venueName}</p>
<div class="flex flex-col md:flex-row md:flex-wrap gap-1 py-1">
<div id="prefixes" class="flex flex-col gap-1 p-2 border border-black rounded">
<h2 class="text-lg font-bold">Prefix Selection:</h2>
{#each prefixes as prefix (prefix.prefix)}
<button class={curPrefix == prefix.prefix ? bAS[prefix.color] : bS[prefix.color]} onclick={() => curPrefix = prefix.prefix}>{prefix.prefix}</button>
{/each}
</div>
{#if curPrefix}
<div class="flex flex-col gap-1 items-center border border-black rounded">
<h2 class="text-lg font-bold">Forms:</h2>
<div class="grid grid-cols-2 gap-1 p-1 min-w-2xs">
<a href={resolve('/tickets/[prefix]', {prefix: curPrefix})} class="{bS[pColor]}">Tickets</a>
</div>
</div>
{:else}
<div class="flex flex-col gap-1 items-center justify-center p-2 border border-black rounded">
<h2 class="text-lg font-bold">Please select a prefix to continue.</h2>
</div>
{/if}
</div>
{#if adminMode} {#if adminMode}
<div id="admin_mode"> <div id="admin_mode" class="py-1">
<h2 class="text-lg font-bold">Admin Mode:</h2> <h2 class="text-lg font-bold">Admin Mode:</h2>
<div class="flex flex-row gap-1"> <div class="flex flex-row gap-1">
<a href={resolve('/settings')} class={bS.gray}>Settings</a> <a href={resolve('/settings')} class={bS.gray}>Settings</a>
@@ -60,5 +87,8 @@
{#if data.healthy !== undefined} {#if data.healthy !== undefined}
<div>Server Healthy: <span class={tS[status.healthy]}>{data.healthy ? 'Yes' : 'No'}</span></div> <div>Server Healthy: <span class={tS[status.healthy]}>{data.healthy ? 'Yes' : 'No'}</span></div>
{/if} {/if}
<div class="text-center text-xs">
<p>&copy; 2026 Ticket Auction Manager</p>
</div>
</div> </div>
</div> </div>
+10 -6
View File
@@ -8,12 +8,16 @@ export const GET = async () => {
const s = getSettings(); const s = getSettings();
if (s.remote_server) { if (s.remote_server) {
const connStr = getPath(s); const connStr = getPath(s);
const res = await fetch(`${connStr}/api/prefixes`, { try {
headers: { 'TAM-KEY': s.remote_key } const res = await fetch(`${connStr}/api/prefixes`, {
}); headers: { 'TAM-KEY': s.remote_key }
if (!res.ok) throw error(res.status); });
const data = await res.json(); if (!res.ok) throw error(res.status);
return json(data); const data = await res.json();
return json(data);
} catch {
return json([]);
}
} else { } else {
const data = await db.select().from(prefixes).orderBy(prefixes.weight, prefixes.prefix); const data = await db.select().from(prefixes).orderBy(prefixes.weight, prefixes.prefix);
return json(data); return json(data);
+52
View File
@@ -0,0 +1,52 @@
import { getSettings, getPath } from '$lib/server/settings';
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { tickets } from '$lib/server/db/schema';
import { sql } from 'drizzle-orm';
export const GET = async () => {
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
try {
const res = await fetch(`${connStr}/api/tickets`, {
headers: { 'TAM-KEY': s.remote_key }
});
if (!res.ok) throw error(res.status);
const data = await res.json();
return json(data);
} catch {
return json([]);
}
} else {
const data = await db.select().from(tickets).orderBy(tickets.prefix, tickets.t_id);
return json(data);
}
};
export const POST = async ({ request }) => {
const reqData = await request.json();
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
const res = await fetch(`${connStr}/api/tickets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'TAM-KEY': s.remote_key },
body: JSON.stringify(reqData)
});
if (!res.ok) throw error(res.status);
}
await db
.insert(tickets)
.values(reqData)
.onConflictDoUpdate({
target: [tickets.prefix, tickets.t_id],
set: {
first_name: sql`EXCLUDED.first_name`,
last_name: sql`EXCLUDED.last_name`,
phone_number: sql`EXCLUDED.phone_number`,
pref: sql`EXCLUDED.pref`
}
});
return json(reqData);
};
@@ -0,0 +1,26 @@
import { db } from '$lib/server/db/index.js';
import { tickets } from '$lib/server/db/schema.js';
import { getSettings, getPath } from '$lib/server/settings';
import { error, json } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
export const GET = async ({ params }) => {
const s = getSettings();
const { prefix } = params;
if (s.remote_server) {
const connStr = getPath(s);
try {
const res = await fetch(`${connStr}/api/tickets/${prefix}`, {
headers: { 'TAM-KEY': s.remote_key }
});
if (!res.ok) throw error(res.status);
const data = await res.json();
return json(data);
} catch {
return json([]);
}
} else {
const data = await db.select().from(tickets).where(eq(tickets.prefix, prefix));
return json(data);
}
};
@@ -0,0 +1,39 @@
import { db } from '$lib/server/db/index.js';
import { tickets } from '$lib/server/db/schema.js';
import { getPath, getSettings } from '$lib/server/settings';
import { error, json } from '@sveltejs/kit';
import { and, between, eq } from 'drizzle-orm';
export const GET = async ({ params }) => {
const s = getSettings();
const { prefix } = params,
[id_from, id_to] = [parseInt(params.id_from), parseInt(params.id_to)];
const rtnData = {};
for (let i = id_from; i <= id_to; i++) {
rtnData[i] = {
prefix,
t_id: i,
first_name: '',
last_name: '',
phone_number: '',
pref: s.default_pref
};
}
if (s.remote_server) {
const connStr = getPath(s);
try {
const res = await fetch(`${connStr}/api/tickets/${prefix}/${id_from}/${id_to}`, {
headers: { 'TAM-KEY': s.remote_key }
});
if (!res.ok) throw error(res.status);
const data = Array.from(await res.json());
data.forEach((t) => (rtnData[t.t_id] = t));
} catch {
return json([]);
}
} else {
const data = await db.select().from(tickets).where(and(eq(tickets.prefix, prefix), between(tickets.t_id, id_from, id_to))).orderBy(tickets.prefix, tickets.t_id);
data.forEach((t) => (rtnData[t.t_id] = t));
}
return json(Object.values(rtnData));
};
@@ -0,0 +1,59 @@
import { db } from '$lib/server/db/index.js';
import { tickets } from '$lib/server/db/schema.js';
import { getPath, getSettings } from '$lib/server/settings';
import { error, json } from '@sveltejs/kit';
import { and, eq } from 'drizzle-orm';
export const GET = async ({ params }) => {
const s = getSettings();
const { prefix, t_id } = params;
if (s.remote_server) {
const connStr = getPath(s);
try {
const res = await fetch(`${connStr}/api/tickets/${prefix}/${t_id}`, {
headers: { 'TAM-KEY': s.remote_key }
});
if (!res.ok) throw error(res.status);
const [data] = await res.json();
if (data) {
return json(data);
} else {
return json({
prefix,
t_id,
first_name: '',
last_name: '',
phone_number: '',
pref: s.default_pref
});
}
} catch {
return json({
prefix,
t_id,
first_name: '',
last_name: '',
phone_number: '',
pref: s.default_pref
});
}
} else {
const [data] = await db
.select()
.from(tickets)
.where(and(eq(tickets.prefix, prefix), eq(tickets.t_id, t_id)))
.orderBy(tickets.prefix, tickets.t_id);
if (data) {
return json(data);
} else {
return json({
prefix,
t_id,
first_name: '',
last_name: '',
phone_number: '',
pref: s.default_pref
});
}
}
};
@@ -23,6 +23,7 @@
<HeaderBar> <HeaderBar>
<a href={resolve('/settings')} class={bS.gray}>Back to Settings</a> <a href={resolve('/settings')} class={bS.gray}>Back to Settings</a>
<div>Other Settings:</div> <div>Other Settings:</div>
<a href={resolve('/settings/prefixes')} class={bS.gray}>Prefixes</a>
</HeaderBar> </HeaderBar>
<div id="app_container" class="p-1"> <div id="app_container" class="p-1">
+120 -93
View File
@@ -1,109 +1,136 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from 'svelte';
import HeaderBar from "$lib/client/components/HeaderBar.svelte"; import HeaderBar from '$lib/client/components/HeaderBar.svelte';
import { bS, iS } from "$lib/client/styles"; import { bS, bAS, iS } from '$lib/client/styles';
import { resolve } from "$app/paths"; import { resolve } from '$app/paths';
let { data } = $props(); let { data } = $props();
const pageTitle = 'Prefixes | TAM'; const pageTitle = 'Prefixes | TAM';
let prefixes = $state([]); let prefixes = $state([]);
let editPrefix = $state({prefix: '', color: 'white', weight: 1}) let editPrefix = $state({ prefix: '', color: 'white', weight: 1 });
onMount(() => { onMount(() => {
prefixes = [...data.prefixes]; prefixes = [...data.prefixes];
}) });
</script> </script>
<svelte:head> <svelte:head>
<title>{pageTitle}</title> <title>{pageTitle}</title>
</svelte:head> </svelte:head>
<HeaderBar> <HeaderBar>
<a href={resolve('/settings')} class={bS.gray}>Back to Settings</a> <a href={resolve('/settings')} class={bS.gray}>Back to Settings</a>
</HeaderBar> </HeaderBar>
<div id="app_container" class="p-1"> <div id="app_container" class="p-1">
<h1 class="text-xl font-bold">{pageTitle}</h1> <h1 class="text-xl font-bold">{pageTitle}</h1>
<div class="flex flex-row gap-1 py-1 items-center"> <div class="flex flex-row gap-1 py-1 items-center">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div>Prefix</div> <div>Prefix</div>
<input type="text" class={iS.normal} bind:value={editPrefix.prefix}> <input type="text" class={iS.normal} bind:value={editPrefix.prefix} />
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div>Color</div> <div>Color</div>
<select name="prefix_color" class={iS.normal} bind:value={editPrefix.color}> <select name="prefix_color" class={iS.normal} bind:value={editPrefix.color}>
<option value="white">White</option> <option value="white">White</option>
<option value="blue">Blue</option> <option value="blue">Blue</option>
<option value="yellow">Yellow</option> <option value="yellow">Yellow</option>
<option value="green">Green</option> <option value="green">Green</option>
<option value="orange">Orange</option> <option value="orange">Orange</option>
<option value="red">Red</option> <option value="red">Red</option>
</select> </select>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div>Weight</div> <div>Weight</div>
<input type="number" class={iS.normal} bind:value={editPrefix.weight}> <input type="number" class={iS.normal} bind:value={editPrefix.weight} />
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div>Actions</div> <div>Actions</div>
<button class={bS[editPrefix.color]} onclick={async () => { <button
if (editPrefix.prefix) { class={bS[editPrefix.color]}
const req = await fetch('/api/prefixes', { onclick={async () => {
method: 'POST', if (editPrefix.prefix) {
headers: {'Content-Type': 'application/json'}, const req = await fetch('/api/prefixes', {
body: JSON.stringify([editPrefix]) method: 'POST',
}); headers: { 'Content-Type': 'application/json' },
if (req.ok) window.location.reload(); body: JSON.stringify([editPrefix])
} });
}}>Add/Change</button> if (req.ok) window.location.reload();
</div> }
</div> }}>Add/Change</button
<div class="flex flex-row gap-1 py-1 items-center"> >
<div>Colors:</div> </div>
<button class={bS.white} onclick={() => editPrefix.color = "white"}>White</button> </div>
<button class={bS.blue} onclick={() => editPrefix.color = "blue"}>Blue</button> <div class="flex flex-row gap-1 py-1 items-center">
<button class={bS.yellow} onclick={() => editPrefix.color = "yellow"}>Yellow</button> <div>Colors:</div>
<button class={bS.green} onclick={() => editPrefix.color = "green"}>Green</button> <button
<button class={bS.orange} onclick={() => editPrefix.color = "orange"}>Orange</button> class={editPrefix.color == 'white' ? bAS.white : bS.white}
<button class={bS.red} onclick={() => editPrefix.color = "red"}>Red</button> onclick={() => (editPrefix.color = 'white')}>White</button
</div> >
<table class="w-full"> <button
<thead class="text-left"> class={editPrefix.color == 'blue' ? bAS.blue : bS.blue}
<tr> onclick={() => (editPrefix.color = 'blue')}>Blue</button
<th>Prefix</th> >
<th>Color</th> <button
<th>Weight</th> class={editPrefix.color == 'yellow' ? bAS.yellow : bS.yellow}
<th>Actions</th> onclick={() => (editPrefix.color = 'yellow')}>Yellow</button
</tr> >
</thead> <button
<tbody> class={editPrefix.color == 'green' ? bAS.green : bS.green}
{#each prefixes as prefix (prefix.prefix)} onclick={() => (editPrefix.color = 'green')}>Green</button
<tr> >
<td>{prefix.prefix}</td> <button
<td>{prefix.color.charAt(0).toUpperCase() + prefix.color.slice(1)}</td> class={editPrefix.color == 'orange' ? bAS.orange : bS.orange}
<td>{prefix.weight}</td> onclick={() => (editPrefix.color = 'orange')}>Orange</button
<td> >
<div class="flex flex-row gap-1 items-center"> <button
<button class={bS[prefix.color]} onclick={() => editPrefix = {...prefix}}>Edit</button> class={editPrefix.color == 'red' ? bAS.red : bS.red}
<button class={bS[prefix.color]} onclick={async () => { onclick={() => (editPrefix.color = 'red')}>Red</button
const res = await fetch(`/api/prefixes?p=${prefix.prefix}`, { >
method: 'DELETE' </div>
}); <table class="w-full">
if (res.ok) window.location.reload(); <thead class="text-left">
}}>Delete</button> <tr>
</div> <th>Prefix</th>
</td> <th>Color</th>
</tr> <th>Weight</th>
{/each} <th>Actions</th>
</tbody> </tr>
</table> </thead>
<tbody>
{#each prefixes as prefix (prefix.prefix)}
<tr>
<td>{prefix.prefix}</td>
<td>{prefix.color.charAt(0).toUpperCase() + prefix.color.slice(1)}</td>
<td>{prefix.weight}</td>
<td>
<div class="flex flex-row gap-1 items-center">
<button class={bS[prefix.color]} onclick={() => (editPrefix = { ...prefix })}
>Edit</button
>
<button
class={bS[prefix.color]}
onclick={async () => {
const res = await fetch(`/api/prefixes?p=${prefix.prefix}`, {
method: 'DELETE'
});
if (res.ok) window.location.reload();
}}>Delete</button
>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div> </div>
<style> <style>
table td, table th { table td,
border: solid black 1px; table th {
padding: 0.25rem border: solid black 1px;
} padding: 0.25rem;
}
</style> </style>
@@ -0,0 +1,7 @@
export const load = async ({ params, fetch }) => {
const { prefix } = params;
const res = await fetch('/api/prefixes');
const prefixes = await res.json();
const prefixObj = Array.from(prefixes).find(p => p.prefix == prefix);
return { prefix: prefixObj, prefixes};
}
@@ -0,0 +1,242 @@
<script>
import { resolve } from '$app/paths';
import { afterNavigate, beforeNavigate } from '$app/navigation';
import { browser } from '$app/environment';
import { bS, bAS, iS } from '$lib/client/styles';
import HeaderBar from '$lib/client/components/HeaderBar.svelte';
import PagerBar from '$lib/client/components/PagerBar.svelte';
import CommandBar from '$lib/client/components/CommandBar.svelte';
let { data } = $props();
let { prefix, prefixes } = $derived(data);
let pageTitle = $derived(`${prefix.prefix} Tickets | TAM`);
let curIdx = $state(0),
nextIdx = $derived(curIdx + 1),
prevIdx = $derived(curIdx - 1);
const changeIdx = (idx) => {
curIdx = idx;
};
const focusIdx = (idx) => {
curIdx = idx;
const elemIdx = document.getElementById(`${idx}_first`);
if (elemIdx) {
elemIdx.focus();
}
};
let pager = $state({ idFrom: 0, idTo: 0 });
let items = $state([]);
let itemsLength = $derived(items.length);
let itemsBuffer = $derived(items.filter((i) => i.changed));
const functions = {
getPage: async () => {
functions.save();
if (pager.idFrom > pager.idTo) {
[pager.idFrom, pager.idTo] = [pager.idTo, pager.idFrom];
}
if (pager.idTo - pager.idFrom > 300) {
pager.idTo = pager.idFrom + 300;
}
const res = await fetch(`/api/tickets/${prefix.prefix}/${pager.idFrom}/${pager.idTo}`);
const resData = await res.json();
resData.map((i) => (i.changed = false));
items = [...resData];
setTimeout(() => focusIdx(0));
},
save: async () => {
if (itemsBuffer.length > 0) {
const res = await fetch('/api/tickets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(itemsBuffer)
});
if (res.ok) {
itemsBuffer.forEach((i) => (i.changed = false));
} else {
alert('Error saving items.');
}
}
setTimeout(() => {
focusIdx(0);
}, 1);
},
prevPage: () => {
((pager.idFrom -= itemsLength), (pager.idTo -= itemsLength));
functions.getPage();
},
nextPage: () => {
((pager.idFrom += itemsLength), (pager.idTo += itemsLength));
functions.getPage();
},
nextLine: () => {
if (items[nextIdx]) {
setTimeout(() => {
focusIdx(nextIdx);
}, 1);
} else {
setTimeout(() => {
focusIdx(curIdx);
}, 1)
}
},
prevLine: () => {
if (items[prevIdx]) {
setTimeout(() => {
focusIdx(prevIdx);
}, 1);
} else {
setTimeout(() => {
focusIdx(curIdx)
}, 1);
};
},
dupDown: () => {
if (items[nextIdx]) {
const buffer = {...items[curIdx]};
["prefix", "t_id"].forEach(key => delete buffer[key]);
items[nextIdx] = {...items[nextIdx], ...buffer, changed: true};
functions.nextLine();
} else {
focusIdx(curIdx);
}
},
dupUp: () => {
if (curIdx > 0) {
const buffer = {...items[curIdx]};
["prefix", "t_id"].forEach(key => delete buffer[key]);
items[prevIdx] = {...items[prevIdx], ...buffer, changed: true};
functions.prevLine();
} else {
focusIdx(curIdx);
}
},
copy: () => {
if (items[curIdx]) {
const buffer = {...items[curIdx]};
["prefix", "t_id"].forEach(key => delete buffer[key]);
window.localStorage.setItem('tam-ticket', JSON.stringify(buffer));
};
focusIdx(curIdx);
},
paste: () => {
if (items[curIdx]) {
const buffer = JSON.parse(window.localStorage.getItem('tam-ticket'));
items[curIdx] = {...items[curIdx], ...buffer, changed: true};
};
focusIdx(curIdx);
}
};
const headers = ['Ticket ID', 'First Name', 'Last Name', 'Phone Number', 'Pref', 'Save?'];
beforeNavigate(({ cancel }) => {
if (itemsBuffer.length > 0) {
if (!confirm('Are you sure you want to leave this page? There are unsaved changes!'))
cancel();
}
});
afterNavigate(() => {
items = [];
pager = { idFrom: 0, idTo: 0 };
curIdx = 0;
});
if (browser) {
window.addEventListener('beforeunload', (e) => {
if (itemsBuffer.length > 0) e.preventDefault();
});
}
</script>
<svelte:head>
<title>{pageTitle}</title>
</svelte:head>
<table class="w-full box-border border-separate">
<thead class="sticky top-1 box-border bg-white">
<tr>
<td colspan="50">
<HeaderBar>
<div>Tickets:</div>
{#each prefixes as p (p.prefix)}
<a
href={resolve('/tickets/[prefix]', { prefix: p.prefix })}
class={prefix.prefix == p.prefix ? bAS[p.color] : bS[p.color]}>{p.prefix}</a
>
{/each}
</HeaderBar>
</td>
</tr>
<tr>
<td colspan="50"><h1 class="text-xl font-bold p-1">{pageTitle}</h1></td>
</tr>
<tr>
<td colspan="50"><PagerBar {prefix} {functions} bind:pager /></td>
</tr>
<tr>
<td colspan="50"><CommandBar {prefix} {functions} /></td>
</tr>
<tr>
{#each headers as header (header)}
<th class="border text-left p-0.5">{header}</th>
{/each}
</tr>
</thead>
<tbody>
{#each items as item, idx (item.t_id)}
<tr
class="focus-within:font-bold"
onfocusin={(e) => {
changeIdx(idx);
e.target.scrollIntoView({ block: 'center' });
}}
>
<td class="p-0.5 border">{item.t_id}</td>
<td class="p-0.5 border"
><input
type="text"
class="{iS.normal} w-full"
id="{idx}_first"
onchangecapture={() => (item.changed = true)}
bind:value={item.first_name}
/></td
>
<td class="p-0.5 border"
><input
type="text"
class="{iS.normal} w-full"
onchangecapture={() => (item.changed = true)}
bind:value={item.last_name}
/></td
>
<td class="p-0.5 border"
><input
type="text"
class="{iS.normal} w-full"
onchangecapture={() => (item.changed = true)}
bind:value={item.phone_number}
/></td
>
<td class="p-0.5 border"
><button
class={bS[prefix.color]}
onclick={() => {
item.pref == 'CALL' ? (item.pref = 'TEXT') : (item.pref = 'CALL');
item.changed = true;
}}>{item.pref}</button
></td
>
<td class="p-0.5 border"
><button
class={bS[prefix.color]}
onclick={() => {
item.changed ? (item.changed = false) : (item.changed = true);
}}>{item.changed ? 'Yes' : 'No'}</button
></td
>
</tr>
{/each}
</tbody>
</table>