(add): Baskets function

This commit is contained in:
2026-06-25 17:31:56 -04:00
parent e5b6fbd7c5
commit 9d6473d4bf
33 changed files with 987 additions and 463 deletions
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 Basket:
prefix: str
b_id: int
description: str = ""
donors: str = ""
winning_ticket: int = 0
class BasketRepo(RepoTemplate):
def get_all_baskets(self):
self.cur.execute("SELECT * FROM baskets ORDER BY prefix, b_id")
results = self.cur.fetchall()
return [Basket(*r) for r in results]
def get_prefix_baskets(self, prefix: str):
self.cur.execute("SELECT * FROM baskets WHERE prefix = ? ORDER BY prefix, b_id", (prefix,))
results = self.cur.fetchall()
return [Basket(*r) for r in results]
def get_single_basket(self, prefix: str, b_id: int):
self.cur.execute("SELECT * FROM baskets WHERE prefix = ? AND b_id = ? ORDER BY prefix, b_id", (prefix, b_id))
results = self.cur.fetchall()
return [Basket(*r) for r in results]
def get_range_baskets(self, prefix: str, id_from: int, id_to: int):
self.cur.execute("""SELECT * FROM baskets WHERE prefix = ? AND b_id BETWEEN ? AND ?
ORDER BY prefix, b_id""", (prefix, id_from, id_to))
results = self.cur.fetchall()
return [Basket(*r) for r in results]
def post_baskets(self, bs: list[Basket]):
for b in bs:
self.cur.execute("""INSERT INTO baskets VALUES (?, ?, ?, ?, ?) ON CONFLICT (prefix, b_id) DO UPDATE SET
description = EXCLUDED.description, donors = EXCLUDED.donors""",
(b.prefix, b.b_id, b.description, b.donors, b.winning_ticket))
self.conn.commit()
return bs
basket_router = APIRouter(prefix="/api/baskets")
@basket_router.get("")
def get_all_baskets(tam_key: str = Header("")):
AuthRepo().verify_key(tam_key)
return BasketRepo().get_all_baskets()
@basket_router.get("/{prefix}")
def get_prefix_baskets(prefix: str, tam_key: str = Header("")):
AuthRepo().verify_key(tam_key)
return BasketRepo().get_prefix_baskets(prefix)
@basket_router.get("/{prefix}/{b_id}")
def get_single_basket(prefix: str, b_id: int, tam_key: str = Header("")):
AuthRepo().verify_key(tam_key)
return BasketRepo().get_single_basket(prefix, b_id)
@basket_router.get("/{prefix}/{id_from}/{id_to}")
def get_range_baskets(prefix: str, id_from: int, id_to: int, tam_key: str = Header("")):
AuthRepo().verify_key(tam_key)
return BasketRepo().get_range_baskets(prefix, id_from, id_to)
@basket_router.post("")
def post_baskets(bs: list[Basket], tam_key: str = Header("")):
AuthRepo().verify_key(tam_key)
return BasketRepo().post_baskets(bs)
+2
View File
@@ -19,6 +19,8 @@ def init_db():
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, 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))""") phone_number TEXT, pref TEXT, PRIMARY KEY (prefix, t_id))""")
cur.execute("""CREATE TABLE IF NOT EXISTS baskets (prefix TEXT, b_id INTEGER, description TEXT, donors TEXT,
winning_ticket INTEGER, PRIMARY KEY (prefix, b_id))""")
conn.commit() conn.commit()
conn.close() conn.close()
+2
View File
@@ -2,8 +2,10 @@ 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 from data.tickets import tickets_router
from data.baskets import basket_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) app.include_router(tickets_router)
app.include_router(basket_router)
+1 -1
View File
@@ -1,6 +1,6 @@
import { defineConfig } from 'drizzle-kit'; import { defineConfig } from 'drizzle-kit';
const dbURL = (process.env.TAM_DATA_DIR || "./data") + "/tam-local.db" const dbURL = (process.env.TAM_DATA_DIR || './data') + '/tam-local.db';
export default defineConfig({ export default defineConfig({
schema: './src/lib/server/db/schema.js', schema: './src/lib/server/db/schema.js',
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="text-scale" content="scale" /> <meta name="text-scale" content="scale" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="tap">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>
@@ -1,12 +1,14 @@
<script> <script>
import { bS } from "../styles"; import { bS } from '../styles';
import { browser } from "$app/environment"; import { browser } from '$app/environment';
import hotkeys from "hotkeys-js"; import hotkeys from 'hotkeys-js';
let { prefix, functions } = $props(); let { prefix, functions } = $props();
if (browser) { if (browser) {
hotkeys.filter = () => {return true}; hotkeys.filter = () => {
return true;
};
hotkeys('alt+l', (e) => { hotkeys('alt+l', (e) => {
e.preventDefault(); e.preventDefault();
if (functions.nextLine) functions.nextLine(); if (functions.nextLine) functions.nextLine();
@@ -41,27 +43,40 @@
<div class="flex flex-row gap-1 py-1 justify-between"> <div class="flex flex-row gap-1 py-1 justify-between">
<div class="flex flex-row gap-1"> <div class="flex flex-row gap-1">
{#if functions.dupDown} {#if functions.dupDown}
<button class={bS[prefix.color]} title="Alt + J" onclick={() => functions.dupDown()}>Duplicate Down</button> <button class={bS[prefix.color]} title="Alt + J" onclick={() => functions.dupDown()}
>Duplicate Down</button
>
{/if} {/if}
{#if functions.dupUp} {#if functions.dupUp}
<button class={bS[prefix.color]} title="Alt + U" onclick={() => functions.dupUp()}>Duplicate Up</button> <button class={bS[prefix.color]} title="Alt + U" onclick={() => functions.dupUp()}
>Duplicate Up</button
>
{/if} {/if}
{#if functions.nextLine} {#if functions.nextLine}
<button class={bS[prefix.color]} title="Alt + L" onclick={() => functions.nextLine()}>Next Line</button> <button class={bS[prefix.color]} title="Alt + L" onclick={() => functions.nextLine()}
>Next Line</button
>
{/if} {/if}
{#if functions.prevLine} {#if functions.prevLine}
<button class={bS[prefix.color]} title="Alt + O" onclick={() => functions.prevLine()}>Previous Line</button> <button class={bS[prefix.color]} title="Alt + O" onclick={() => functions.prevLine()}
>Previous Line</button
>
{/if} {/if}
{#if functions.copy} {#if functions.copy}
<button class={bS[prefix.color]} title="Alt + C" onclick={() => functions.copy()}>Copy</button> <button class={bS[prefix.color]} title="Alt + C" onclick={() => functions.copy()}>Copy</button
>
{/if} {/if}
{#if functions.paste} {#if functions.paste}
<button class={bS[prefix.color]} title="Alt + V" onclick={() => functions.paste()}>Paste</button> <button class={bS[prefix.color]} title="Alt + V" onclick={() => functions.paste()}
>Paste</button
>
{/if} {/if}
</div> </div>
<div class="flex flex-row gap-1"> <div class="flex flex-row gap-1">
{#if functions.save} {#if functions.save}
<button class={bS[prefix.color]} title="Alt + S" onclick={() => functions.save()}>Save Marked</button> <button class={bS[prefix.color]} title="Alt + S" onclick={() => functions.save()}
>Save Marked</button
>
{/if} {/if}
</div> </div>
</div> </div>
@@ -5,7 +5,7 @@
let { children } = $props(); let { children } = $props();
</script> </script>
<div id="header_bar" class="p-1 flex flex-row gap-1 items-center"> <div id="header_bar" class="py-1 flex flex-row gap-1 items-center">
<a href={resolve('/')} class="{bS.gray} font-bold">Main Menu</a> <a href={resolve('/')} class="{bS.gray} font-bold">Main Menu</a>
{#if children} {#if children}
{@render children()} {@render children()}
+14 -9
View File
@@ -2,28 +2,33 @@ export const tS = {
green: 'text-green-800 font-bold', green: 'text-green-800 font-bold',
yellow: 'text-yellow-800 font-bold', yellow: 'text-yellow-800 font-bold',
red: 'text-red-800 font-bold' red: 'text-red-800 font-bold'
} };
export const bS = { export const bS = {
gray: 'px-2 py-1 bg-gray-300 border border-gray-800 rounded hover:bg-gray-400 cursor-pointer', gray: 'px-2 py-1 bg-gray-300 border border-gray-800 rounded hover:bg-gray-400 cursor-pointer',
white: 'px-2 py-1 bg-white border border-gray-700 rounded hover:bg-gray-200 cursor-pointer', white: 'px-2 py-1 bg-white border border-gray-700 rounded hover:bg-gray-200 cursor-pointer',
blue: 'px-2 py-1 bg-blue-300 border border-gray-700 rounded hover:bg-blue-400 cursor-pointer', blue: 'px-2 py-1 bg-blue-300 border border-gray-700 rounded hover:bg-blue-400 cursor-pointer',
yellow: 'px-2 py-1 bg-yellow-300 border border-gray-700 rounded hover:bg-yellow-400 cursor-pointer', yellow:
'px-2 py-1 bg-yellow-300 border border-gray-700 rounded hover:bg-yellow-400 cursor-pointer',
green: 'px-2 py-1 bg-green-300 border border-gray-700 rounded hover:bg-green-400 cursor-pointer', green: 'px-2 py-1 bg-green-300 border border-gray-700 rounded hover:bg-green-400 cursor-pointer',
orange: 'px-2 py-1 bg-orange-300 border border-gray-700 rounded hover:bg-orange-400 cursor-pointer', orange:
'px-2 py-1 bg-orange-300 border border-gray-700 rounded hover:bg-orange-400 cursor-pointer',
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 = { export const bAS = {
gray: 'px-2 py-1 bg-gray-300 border-4 border-gray-800 rounded hover:bg-gray-400 cursor-pointer', 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', 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', 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', yellow:
green: 'px-2 py-1 bg-green-300 border-4 border-gray-700 rounded hover:bg-green-400 cursor-pointer', 'px-2 py-1 bg-yellow-300 border-4 border-gray-700 rounded hover:bg-yellow-400 cursor-pointer',
orange: 'px-2 py-1 bg-orange-300 border-4 border-gray-700 rounded hover:bg-orange-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' 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'
} };
+1 -1
View File
@@ -3,7 +3,7 @@ import Database from 'better-sqlite3';
import * as schema from './schema'; import * as schema from './schema';
import { env } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
const dbURL = (env.TAM_DATA_DIR || "./data") + "/tam-local.db"; const dbURL = (env.TAM_DATA_DIR || './data') + '/tam-local.db';
const client = new Database(dbURL); const client = new Database(dbURL);
+18 -2
View File
@@ -6,11 +6,27 @@ export const prefixes = sqliteTable('prefixes', {
weight: integer('weight') weight: integer('weight')
}); });
export const tickets = sqliteTable('tickets', { export const tickets = sqliteTable(
'tickets',
{
prefix: text('prefix'), prefix: text('prefix'),
t_id: integer('t_id'), t_id: integer('t_id'),
first_name: text('first_name'), first_name: text('first_name'),
last_name: text('last_name'), last_name: text('last_name'),
phone_number: text('phone_number'), phone_number: text('phone_number'),
pref: text('pref') pref: text('pref')
}, (t) => [primaryKey({columns: [t.prefix, t.t_id]})]) },
(t) => [primaryKey({ columns: [t.prefix, t.t_id] })]
);
export const baskets = sqliteTable(
'baskets',
{
prefix: text('prefix'),
b_id: integer('b_id'),
description: text('description'),
donors: text('donors'),
winning_ticket: integer('winning_ticket')
},
(b) => [primaryKey({ columns: [b.prefix, b.b_id] })]
);
+1 -1
View File
@@ -16,7 +16,7 @@ export const getSettings = () => {
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
return settings; return settings;
} catch { } catch {
mkdirSync(env.TAM_DATA_DIR || './data', {recursive: true}) mkdirSync(env.TAM_DATA_DIR || './data', { recursive: true });
writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2), 'utf-8'); writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2), 'utf-8');
return defaultSettings; return defaultSettings;
} }
+1 -1
View File
@@ -7,6 +7,6 @@
<svelte:head> <svelte:head>
<link rel="icon" href={favicon} /> <link rel="icon" href={favicon} />
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json" />
</svelte:head> </svelte:head>
{@render children()} {@render children()}
+27 -13
View File
@@ -4,15 +4,15 @@
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import hotkeys from 'hotkeys-js'; import hotkeys from 'hotkeys-js';
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 prefixes = $derived(data.prefixes);
let curPrefix = $state(""); let curPrefix = $state('');
let pColor = $derived.by(() => { let pColor = $derived.by(() => {
if (curPrefix) return prefixes.find(p => curPrefix == p.prefix).color if (curPrefix) return prefixes.find((p) => curPrefix == p.prefix).color;
else return "gray"; else return 'gray';
}) });
const status = $derived.by(() => { const status = $derived.by(() => {
if (data.whoami === 'TAM Server') { if (data.whoami === 'TAM Server') {
@@ -28,16 +28,18 @@
} else { } else {
return { return {
mode: 'Unknown' mode: 'Unknown'
};
} }
} });
})
if (browser) { if (browser) {
hotkeys.filter = () => {return true}; hotkeys.filter = () => {
return true;
};
hotkeys('alt+a', (event) => { hotkeys('alt+a', (event) => {
event.preventDefault(); event.preventDefault();
adminMode = !adminMode; adminMode = !adminMode;
}) });
} }
</script> </script>
@@ -53,14 +55,22 @@
<div id="prefixes" class="flex flex-col gap-1 p-2 border border-black rounded"> <div id="prefixes" class="flex flex-col gap-1 p-2 border border-black rounded">
<h2 class="text-lg font-bold">Prefix Selection:</h2> <h2 class="text-lg font-bold">Prefix Selection:</h2>
{#each prefixes as prefix (prefix.prefix)} {#each prefixes as prefix (prefix.prefix)}
<button class={curPrefix == prefix.prefix ? bAS[prefix.color] : bS[prefix.color]} onclick={() => curPrefix = prefix.prefix}>{prefix.prefix}</button> <button
class={curPrefix == prefix.prefix ? bAS[prefix.color] : bS[prefix.color]}
onclick={() => (curPrefix = prefix.prefix)}>{prefix.prefix}</button
>
{/each} {/each}
</div> </div>
{#if curPrefix} {#if curPrefix}
<div class="flex flex-col gap-1 items-center border border-black rounded"> <div class="flex flex-col gap-1 items-center border border-black rounded">
<h2 class="text-lg font-bold">Forms:</h2> <h2 class="text-lg font-bold">Forms:</h2>
<div class="grid grid-cols-2 gap-1 p-1 min-w-2xs"> <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> <a href={resolve('/tickets/[prefix]', { prefix: curPrefix })} class={bS[pColor]}
>Tickets</a
>
<a href={resolve('/baskets/[prefix]', { prefix: curPrefix })} class={bS[pColor]}
>Baskets</a
>
</div> </div>
</div> </div>
{:else} {:else}
@@ -82,10 +92,14 @@
<div id="footer"> <div id="footer">
<div>Mode: {status.mode}</div> <div>Mode: {status.mode}</div>
{#if data.authenticated !== undefined} {#if data.authenticated !== undefined}
<div>Authenticated: <span class={tS[status.auth]}>{data.authenticated ? 'Yes' : 'No'}</span></div> <div>
Authenticated: <span class={tS[status.auth]}>{data.authenticated ? 'Yes' : 'No'}</span>
</div>
{/if} {/if}
{#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"> <div class="text-center text-xs">
<p>&copy; 2026 Ticket Auction Manager</p> <p>&copy; 2026 Ticket Auction Manager</p>
+1 -1
View File
@@ -10,7 +10,7 @@ export const GET = async () => {
headers: { 'TAM-KEY': s.remote_key } headers: { 'TAM-KEY': s.remote_key }
}); });
if (!res.ok) throw error(res.status); if (!res.ok) throw error(res.status);
const data = await res.json() const data = await res.json();
return json(data); return json(data);
} catch { } catch {
const data = { whoami: 'TAM Server', authenticated: false, healthy: false }; const data = { whoami: 'TAM Server', authenticated: false, healthy: false };
+11 -11
View File
@@ -1,5 +1,5 @@
import { getSettings, getPath } from "$lib/server/settings"; import { getSettings, getPath } from '$lib/server/settings';
import { json, error } from "@sveltejs/kit"; import { json, error } from '@sveltejs/kit';
export const GET = async ({ request }) => { export const GET = async ({ request }) => {
const tamPwd = request.headers.get('TAM-PWD'); const tamPwd = request.headers.get('TAM-PWD');
@@ -14,12 +14,12 @@ export const GET = async ({ request }) => {
const data = await res.json(); const data = await res.json();
return json(data); return json(data);
} catch { } catch {
throw error(502) throw error(502);
} }
} else { } else {
throw error(500, 'Not configured.') throw error(500, 'Not configured.');
}
} }
};
export const POST = async ({ request }) => { export const POST = async ({ request }) => {
const tamPwd = request.headers.get('TAM-PWD'); const tamPwd = request.headers.get('TAM-PWD');
@@ -40,9 +40,9 @@ export const POST = async ({ request }) => {
throw error(502); throw error(502);
} }
} else { } else {
throw error(500, 'Not configured.') throw error(500, 'Not configured.');
}
} }
};
export const DELETE = async ({ request, url }) => { export const DELETE = async ({ request, url }) => {
const tamPwd = request.headers.get('TAM-PWD'); const tamPwd = request.headers.get('TAM-PWD');
@@ -54,14 +54,14 @@ export const DELETE = async ({ request, url }) => {
const res = await fetch(`${connStr}/api/auth?key_to_del=${keyToDel}`, { const res = await fetch(`${connStr}/api/auth?key_to_del=${keyToDel}`, {
headers: { 'TAM-PW': tamPwd }, headers: { 'TAM-PW': tamPwd },
method: 'DELETE' method: 'DELETE'
}) });
if (!res.ok) throw error(res.status) if (!res.ok) throw error(res.status);
const data = await res.json(); const data = await res.json();
return json(data); return json(data);
} catch { } catch {
throw error(502); throw error(502);
} }
} else { } else {
throw error(500, 'Not configured.') throw error(500, 'Not configured.');
}
} }
};
+45
View File
@@ -0,0 +1,45 @@
import { db } from '$lib/server/db';
import { baskets } from '$lib/server/db/schema';
import { getSettings, getPath } from '$lib/server/settings';
import { error, json } from '@sveltejs/kit';
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/baskets`, { 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(baskets).orderBy(baskets.prefix, baskets.b_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/baskets`, {
method: 'POST',
headers: { 'TAM-KEY': s.remote_key, 'Content-Type': 'application/json' },
body: JSON.stringify(reqData)
});
if (!res.ok) throw error(res.status);
}
await db
.insert(baskets)
.values(reqData)
.onConflictDoUpdate({
target: [baskets.prefix, baskets.b_id],
set: { description: sql`EXCLUDED.description`, donors: sql`EXCLUDED.donors` }
});
return json(reqData);
};
@@ -0,0 +1,30 @@
import { db } from '$lib/server/db/index.js';
import { baskets } from '$lib/server/db/schema.js';
import { getPath, getSettings } from '$lib/server/settings';
import { error, json } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
export const GET = async ({ params }) => {
const { prefix } = params;
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
try {
const req = await fetch(`${connStr}/api/baskets/${prefix}`, {
headers: { 'TAM-KEY': s.remote_key }
});
if (!req.ok) throw error(req.status);
const data = await req.json();
return json(data);
} catch {
return json([]);
}
} else {
const data = await db
.select()
.from(baskets)
.where(eq(baskets.prefix, prefix))
.orderBy(baskets.prefix, baskets.b_id);
return json(data);
}
};
@@ -0,0 +1,38 @@
import { db } from '$lib/server/db/index.js';
import { baskets } 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 [prefix, b_id] = [params.prefix, parseInt(params.b_id)];
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
try {
const res = await fetch(`${connStr}/api/tickets/${prefix}/${b_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, b_id, description: '', donors: '', winning_ticket: 0 });
}
} catch {
return json({ prefix, b_id, description: '', donors: '', winning_ticket: 0 });
}
} else {
const [data] = await db
.select()
.from(baskets)
.where(and(eq(baskets.prefix, prefix), eq(baskets.b_id, b_id)))
.orderBy(baskets.prefix, baskets.b_id);
if (data) {
return json(data);
} else {
return json({ prefix, b_id, descrption: '', donors: '', winning_ticket: 0 });
}
}
};
@@ -0,0 +1,39 @@
import { db } from '$lib/server/db/index.js';
import { baskets } 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, id_from, id_to] = [
params.prefix,
parseInt(params.id_from),
parseInt(params.id_to)
];
const rtnData = {};
for (let i = id_from; i <= id_to; i++) {
rtnData[i] = { prefix, b_id: i, description: '', donors: '', winning_ticket: 0 };
}
if (s.remote_server) {
const connStr = getPath(s);
try {
const res = await fetch(`${connStr}/api/baskets/${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((b) => (rtnData[b.b_id] = b));
} catch {
return json([]);
}
} else {
const data = await db
.select()
.from(baskets)
.where(and(eq(baskets.prefix, prefix), between(baskets.b_id, id_from, id_to)))
.orderBy(baskets.prefix, baskets.b_id);
data.forEach((b) => (rtnData[b.b_id] = b));
}
return json(Object.values(rtnData));
};
+17 -11
View File
@@ -1,8 +1,8 @@
import { getSettings, getPath } from "$lib/server/settings" import { getSettings, getPath } from '$lib/server/settings';
import { json, error } from "@sveltejs/kit"; import { json, error } from '@sveltejs/kit';
import { db } from "$lib/server/db"; import { db } from '$lib/server/db';
import { prefixes } from "$lib/server/db/schema"; import { prefixes } from '$lib/server/db/schema';
import { sql, eq } from "drizzle-orm"; import { sql, eq } from 'drizzle-orm';
export const GET = async () => { export const GET = async () => {
const s = getSettings(); const s = getSettings();
@@ -22,7 +22,7 @@ export const GET = async () => {
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);
} }
} };
export const POST = async ({ request }) => { export const POST = async ({ request }) => {
const reqData = await request.json(); const reqData = await request.json();
@@ -33,12 +33,18 @@ export const POST = async ({ request }) => {
method: 'POST', method: 'POST',
headers: { 'TAM-KEY': s.remote_key, 'Content-Type': 'application/json' }, headers: { 'TAM-KEY': s.remote_key, 'Content-Type': 'application/json' },
body: JSON.stringify(reqData) body: JSON.stringify(reqData)
}) });
if (!res.ok) throw error(res.status); if (!res.ok) throw error(res.status);
} }
await db.insert(prefixes).values(reqData).onConflictDoUpdate({ target: prefixes.prefix, set: { color: sql`EXCLUDED.color`, weight: sql`EXCLUDED.weight` } }); await db
.insert(prefixes)
.values(reqData)
.onConflictDoUpdate({
target: prefixes.prefix,
set: { color: sql`EXCLUDED.color`, weight: sql`EXCLUDED.weight` }
});
return json(reqData); return json(reqData);
} };
export const DELETE = async ({ url }) => { export const DELETE = async ({ url }) => {
const p = url.searchParams.get('p'); const p = url.searchParams.get('p');
@@ -52,5 +58,5 @@ export const DELETE = async ({ url }) => {
if (!res.ok) throw error(res.status); if (!res.ok) throw error(res.status);
} }
await db.delete(prefixes).where(eq(prefixes.prefix, p)); await db.delete(prefixes).where(eq(prefixes.prefix, p));
return json({ message: "Deleted successfully."}); return json({ message: 'Deleted successfully.' });
} };
+3 -3
View File
@@ -1,8 +1,8 @@
import { setSettings } from "$lib/server/settings"; import { setSettings } from '$lib/server/settings';
import { json } from "@sveltejs/kit"; import { json } from '@sveltejs/kit';
export const POST = async ({ request }) => { export const POST = async ({ request }) => {
const newSettings = await request.json(); const newSettings = await request.json();
setSettings(newSettings); setSettings(newSettings);
return json(newSettings); return json(newSettings);
} };
@@ -32,7 +32,11 @@ export const GET = async ({ params }) => {
return json([]); return json([]);
} }
} else { } 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); 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)); data.forEach((t) => (rtnData[t.t_id] = t));
} }
return json(Object.values(rtnData)); return json(Object.values(rtnData));
@@ -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,220 @@
<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} Baskets | 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/baskets/${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/baskets', {
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 = ['Basket ID', 'Description', 'Donors', '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 p-1">
<thead class="sticky top-1 bg-white">
<tr>
<td colspan="50">
<HeaderBar>
<div>Baskets:</div>
{#each prefixes as p (p.prefix)}
<a
href={resolve('/baskets/[prefix]', { prefix: p.prefix })}
class={prefix.prefix == p.prefix ? bAS[p.color] : bS[p.color]}>{p.prefix}</a
>
{/each}
</HeaderBar>
<h1 class="text-xl font-bold p-1">{pageTitle}</h1>
<PagerBar {prefix} {functions} bind:pager />
<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.b_id)}
<tr
class="focus-within:font-bold"
onfocusin={(e) => {
changeIdx(idx);
e.target.scrollIntoView({ block: 'center' });
}}
>
<td class="p-0.5 border">{item.b_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.description}
/></td
>
<td class="p-0.5 border"
><input
type="text"
class="{iS.normal} w-full"
id="{idx}_second"
onchangecapture={() => (item.changed = true)}
bind:value={item.donors}
/></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>
+2 -2
View File
@@ -1,6 +1,6 @@
import { getSettings } from "$lib/server/settings"; import { getSettings } from '$lib/server/settings';
export const load = () => { export const load = () => {
const settings = getSettings(); const settings = getSettings();
return { settings }; return { settings };
} };
+27 -16
View File
@@ -1,21 +1,21 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { resolve } from '$app/paths' import { resolve } from '$app/paths';
import { bS, iS, tS } from '$lib/client/styles'; import { bS, iS, tS } from '$lib/client/styles';
import HeaderBar from '$lib/client/components/HeaderBar.svelte' import HeaderBar from '$lib/client/components/HeaderBar.svelte';
let { data } = $props(); let { data } = $props();
let settings = $state({}); let settings = $state({});
let status = $state({ let status = $state({
message: '', message: '',
color: 'green' color: 'green'
}) });
const pageTitle = 'Settings | TAM'; const pageTitle = 'Settings | TAM';
onMount(() => { onMount(() => {
settings = { ...data.settings }; settings = { ...data.settings };
}) });
</script> </script>
<svelte:head> <svelte:head>
@@ -36,36 +36,46 @@
<h2 class="text-lg font-bold">Remote Mode:</h2> <h2 class="text-lg font-bold">Remote Mode:</h2>
<div class="flex flex-row gap-1 items-center"> <div class="flex flex-row gap-1 items-center">
<div>Remote Server:</div> <div>Remote Server:</div>
<input type="text" class={iS.normal} bind:value={settings.remote_server}> <input type="text" class={iS.normal} bind:value={settings.remote_server} />
</div> </div>
<div class="flex flex-row gap-1 items-center"> <div class="flex flex-row gap-1 items-center">
<div>Remote Port:</div> <div>Remote Port:</div>
<input type="text" class={iS.normal} bind:value={settings.remote_port}> <input type="text" class={iS.normal} bind:value={settings.remote_port} />
</div> </div>
<div class="flex flex-row gap-1 items-center"> <div class="flex flex-row gap-1 items-center">
<div>Remote TLS:</div> <div>Remote TLS:</div>
<button class={bS.gray} onclick={() => { <button
settings.remote_tls = !settings.remote_tls class={bS.gray}
}}>{settings.remote_tls ? 'Yes' : 'No'}</button> onclick={() => {
settings.remote_tls = !settings.remote_tls;
}}>{settings.remote_tls ? 'Yes' : 'No'}</button
>
</div> </div>
<h2 class="text-lg font-bold">Default Preferences:</h2> <h2 class="text-lg font-bold">Default Preferences:</h2>
<div class="flex flex-row gap-1 items-center"> <div class="flex flex-row gap-1 items-center">
<div>Contact Preference:</div> <div>Contact Preference:</div>
<button class={bS.gray} onclick={() => { <button
settings.default_pref === 'CALL' ? settings.default_pref = 'TEXT' : settings.default_pref = 'CALL'; class={bS.gray}
}}>{settings.default_pref}</button> onclick={() => {
settings.default_pref === 'CALL'
? (settings.default_pref = 'TEXT')
: (settings.default_pref = 'CALL');
}}>{settings.default_pref}</button
>
</div> </div>
<div class="flex flex-row gap-1 items-center"> <div class="flex flex-row gap-1 items-center">
<div>Venue Name:</div> <div>Venue Name:</div>
<input type="text" class={iS.normal} bind:value={settings.venue_name}> <input type="text" class={iS.normal} bind:value={settings.venue_name} />
</div> </div>
<div class="flex flex-row gap-1 items-center"> <div class="flex flex-row gap-1 items-center">
<button class={bS.gray} onclick={async () => { <button
class={bS.gray}
onclick={async () => {
const res = await fetch('/api/settings', { const res = await fetch('/api/settings', {
method: 'POST', method: 'POST',
body: JSON.stringify(settings), body: JSON.stringify(settings),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}) });
if (!res.ok) { if (!res.ok) {
status.message = `Error Code: ${res.status}`; status.message = `Error Code: ${res.status}`;
status.color = 'red'; status.color = 'red';
@@ -75,7 +85,8 @@
status.message = 'Settings saved successfully!'; status.message = 'Settings saved successfully!';
status.color = 'green'; status.color = 'green';
} }
}}>Save</button> }}>Save</button
>
<button class={bS.gray}>Cancel</button> <button class={bS.gray}>Cancel</button>
</div> </div>
<div> <div>
@@ -1,6 +1,6 @@
import { getSettings } from "$lib/server/settings"; import { getSettings } from '$lib/server/settings';
export const load = () => { export const load = () => {
const s = getSettings(); const s = getSettings();
return { authKey: s.remote_key }; return { authKey: s.remote_key };
} };
@@ -92,9 +92,9 @@
}); });
if (res.ok) { if (res.ok) {
setTimeout(() => { setTimeout(() => {
const newKeys = authKeys.filter(i => i.auth_key !== key.auth_key); const newKeys = authKeys.filter((i) => i.auth_key !== key.auth_key);
authKeys = [...newKeys]; authKeys = [...newKeys];
}, 1) }, 1);
} }
}}>Delete</button }}>Delete</button
> >
@@ -2,4 +2,4 @@ export const load = async ({ fetch }) => {
const res = await fetch('/api/prefixes'); const res = await fetch('/api/prefixes');
const prefixes = await res.json(); const prefixes = await res.json();
return { prefixes }; return { prefixes };
} };
@@ -2,6 +2,6 @@ export const load = async ({ params, fetch }) => {
const { prefix } = params; const { prefix } = params;
const res = await fetch('/api/prefixes'); const res = await fetch('/api/prefixes');
const prefixes = await res.json(); const prefixes = await res.json();
const prefixObj = Array.from(prefixes).find(p => p.prefix == prefix); const prefixObj = Array.from(prefixes).find((p) => p.prefix == prefix);
return { prefix: prefixObj, prefixes }; return { prefix: prefixObj, prefixes };
} };
+25 -20
View File
@@ -78,7 +78,7 @@
} else { } else {
setTimeout(() => { setTimeout(() => {
focusIdx(curIdx); focusIdx(curIdx);
}, 1) }, 1);
} }
}, },
prevLine: () => { prevLine: () => {
@@ -88,14 +88,14 @@
}, 1); }, 1);
} else { } else {
setTimeout(() => { setTimeout(() => {
focusIdx(curIdx) focusIdx(curIdx);
}, 1); }, 1);
}; }
}, },
dupDown: () => { dupDown: () => {
if (items[nextIdx]) { if (items[nextIdx]) {
const buffer = { ...items[curIdx] }; const buffer = { ...items[curIdx] };
["prefix", "t_id"].forEach(key => delete buffer[key]); ['prefix', 't_id'].forEach((key) => delete buffer[key]);
items[nextIdx] = { ...items[nextIdx], ...buffer, changed: true }; items[nextIdx] = { ...items[nextIdx], ...buffer, changed: true };
functions.nextLine(); functions.nextLine();
} else { } else {
@@ -105,7 +105,7 @@
dupUp: () => { dupUp: () => {
if (curIdx > 0) { if (curIdx > 0) {
const buffer = { ...items[curIdx] }; const buffer = { ...items[curIdx] };
["prefix", "t_id"].forEach(key => delete buffer[key]); ['prefix', 't_id'].forEach((key) => delete buffer[key]);
items[prevIdx] = { ...items[prevIdx], ...buffer, changed: true }; items[prevIdx] = { ...items[prevIdx], ...buffer, changed: true };
functions.prevLine(); functions.prevLine();
} else { } else {
@@ -115,16 +115,16 @@
copy: () => { copy: () => {
if (items[curIdx]) { if (items[curIdx]) {
const buffer = { ...items[curIdx] }; const buffer = { ...items[curIdx] };
["prefix", "t_id"].forEach(key => delete buffer[key]); ['prefix', 't_id'].forEach((key) => delete buffer[key]);
window.localStorage.setItem('tam-ticket', JSON.stringify(buffer)); window.localStorage.setItem('tam-ticket', JSON.stringify(buffer));
}; }
focusIdx(curIdx); focusIdx(curIdx);
}, },
paste: () => { paste: () => {
if (items[curIdx]) { if (items[curIdx]) {
const buffer = JSON.parse(window.localStorage.getItem('tam-ticket')); const buffer = JSON.parse(window.localStorage.getItem('tam-ticket'));
items[curIdx] = { ...items[curIdx], ...buffer, changed: true }; items[curIdx] = { ...items[curIdx], ...buffer, changed: true };
}; }
focusIdx(curIdx); focusIdx(curIdx);
} }
}; };
@@ -154,8 +154,8 @@
<title>{pageTitle}</title> <title>{pageTitle}</title>
</svelte:head> </svelte:head>
<table class="w-full box-border border-separate"> <table class="w-full box-border border-separate p-1">
<thead class="sticky top-1 box-border bg-white"> <thead class="sticky top-1 bg-white">
<tr> <tr>
<td colspan="50"> <td colspan="50">
<HeaderBar> <HeaderBar>
@@ -167,16 +167,10 @@
> >
{/each} {/each}
</HeaderBar> </HeaderBar>
</td> <h1 class="text-xl font-bold p-1">{pageTitle}</h1>
</tr> <PagerBar {prefix} {functions} bind:pager />
<tr> <CommandBar {prefix} {functions} /></td
<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>
<tr> <tr>
{#each headers as header (header)} {#each headers as header (header)}
@@ -207,6 +201,7 @@
><input ><input
type="text" type="text"
class="{iS.normal} w-full" class="{iS.normal} w-full"
id="{idx}_second"
onchangecapture={() => (item.changed = true)} onchangecapture={() => (item.changed = true)}
bind:value={item.last_name} bind:value={item.last_name}
/></td /></td
@@ -215,6 +210,7 @@
><input ><input
type="text" type="text"
class="{iS.normal} w-full" class="{iS.normal} w-full"
id="{idx}_third"
onchangecapture={() => (item.changed = true)} onchangecapture={() => (item.changed = true)}
bind:value={item.phone_number} bind:value={item.phone_number}
/></td /></td
@@ -225,6 +221,15 @@
onclick={() => { onclick={() => {
item.pref == 'CALL' ? (item.pref = 'TEXT') : (item.pref = 'CALL'); item.pref == 'CALL' ? (item.pref = 'TEXT') : (item.pref = 'CALL');
item.changed = true; item.changed = true;
}}
onkeydown={(e) => {
if (e.key == 't') {
if (item.pref != 'TEXT') item.changed = true;
item.pref = 'TEXT';
} else if (e.key == 'c') {
if (item.pref != 'CALL') item.changed = true;
item.pref = 'CALL';
}
}}>{item.pref}</button }}>{item.pref}</button
></td ></td
> >