6 Commits
22 changed files with 1170 additions and 825 deletions

No files matched your search

+16 -16
View File
@@ -17,27 +17,27 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@sveltejs/adapter-node": "^5.5.4", "@sveltejs/adapter-node": "^5.5.7",
"@sveltejs/kit": "^2.63.0", "@sveltejs/kit": "^2.70.3",
"@sveltejs/vite-plugin-svelte": "^7.1.2", "@sveltejs/vite-plugin-svelte": "^7.3.0",
"@tailwindcss/vite": "^4.3.0", "@tailwindcss/vite": "^4.3.3",
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "^7.6.13",
"@types/node": "^24", "@types/node": "^24.13.4",
"drizzle-kit": "^0.31.10", "drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
"eslint": "^10.4.1", "eslint": "^10.10.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.19.0", "eslint-plugin-svelte": "^3.23.0",
"globals": "^17.6.0", "globals": "^17.12.0",
"prettier": "^3.8.3", "prettier": "^3.9.6",
"prettier-plugin-svelte": "^4.1.0", "prettier-plugin-svelte": "^4.1.1",
"prettier-plugin-tailwindcss": "^0.8.0", "prettier-plugin-tailwindcss": "^0.8.1",
"svelte": "^5.56.1", "svelte": "^5.57.0",
"tailwindcss": "^4.3.0", "tailwindcss": "^4.3.3",
"vite": "^8.0.16" "vite": "^8.3.0"
}, },
"dependencies": { "dependencies": {
"better-sqlite3": "^12.10.0", "better-sqlite3": "^12.11.1",
"hotkeys-js": "^4.0.4" "hotkeys-js": "^4.0.8"
} }
} }
+843 -680
View File
File diff suppressed because it is too large. Load diff
+40 -9
View File
@@ -1,7 +1,18 @@
import { db } from "$lib/server/db"; import { db } from '$lib/server/db';
import { env } from '$env/dynamic/public';
import { randomUUID } from 'crypto';
export const init = async () => { export const init = async () => {
await db.run(`CREATE TABLE IF NOT EXISTS baskets ( env.PUBLIC_TAM_CLIENT_ID = randomUUID();
const originalEmitWarning = process.emitWarning;
process.emitWarning = (warning, ...args) => {
if (typeof warning === 'string' && warning.includes('NODE_TLS_REJECT_UNAUTHORIZED')) {
return;
};
originalEmitWarning.call(process, warning, ...args);
};
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
await db.run(`CREATE TABLE IF NOT EXISTS baskets (
prefix text, prefix text,
b_id integer, b_id integer,
description text, description text,
@@ -9,12 +20,12 @@ export const init = async () => {
winning_ticket integer, winning_ticket integer,
PRIMARY KEY(prefix, b_id) PRIMARY KEY(prefix, b_id)
)`); )`);
await db.run(`CREATE TABLE IF NOT EXISTS prefixes ( await db.run(`CREATE TABLE IF NOT EXISTS prefixes (
prefix text PRIMARY KEY NOT NULL, prefix text PRIMARY KEY NOT NULL,
color text, color text,
weight integer weight integer
)`); )`);
await db.run(`CREATE TABLE IF NOT EXISTS tickets ( await db.run(`CREATE TABLE IF NOT EXISTS tickets (
prefix text, prefix text,
t_id integer, t_id integer,
first_name text, first_name text,
@@ -23,19 +34,39 @@ export const init = async () => {
pref text, pref text,
PRIMARY KEY(prefix, t_id) PRIMARY KEY(prefix, t_id)
)`); )`);
await db.run(`CREATE VIEW IF NOT EXISTS drawing AS SELECT b.prefix, b.b_id, b.description, b.winning_ticket, t.last_name, t.first_name, t.phone_number await db.run(`CREATE VIEW IF NOT EXISTS drawing AS SELECT b.prefix, b.b_id, b.description, b.winning_ticket, t.last_name, t.first_name, t.phone_number
FROM baskets b LEFT JOIN tickets t ON b.prefix = t.prefix AND b.winning_ticket = t.t_id FROM baskets b LEFT JOIN tickets t ON b.prefix = t.prefix AND b.winning_ticket = t.t_id
ORDER BY b.prefix, b.b_id`); ORDER BY b.prefix, b.b_id`);
await db.run(`CREATE VIEW IF NOT EXISTS report_by_basket AS SELECT b.prefix, b.b_id, b.description, b.donors, b.winning_ticket, t.last_name, t.first_name, t.phone_number, t.pref await db.run(`CREATE VIEW IF NOT EXISTS report_by_basket AS SELECT b.prefix, b.b_id, b.description, b.donors, b.winning_ticket, t.last_name, t.first_name, t.phone_number, t.pref
FROM baskets b LEFT JOIN tickets t on b.prefix = t.prefix AND b.winning_ticket = t.t_id FROM baskets b LEFT JOIN tickets t on b.prefix = t.prefix AND b.winning_ticket = t.t_id
ORDER BY b.prefix, b.b_id`); ORDER BY b.prefix, b.b_id`);
await db.run(`CREATE VIEW IF NOT EXISTS report_by_name AS SELECT t.last_name, t.first_name, t.phone_number, t.pref, b.prefix, b.b_id, b.description, b.donors, b.winning_ticket await db.run(`CREATE VIEW IF NOT EXISTS report_by_name AS SELECT t.last_name, t.first_name, t.phone_number, t.pref, b.prefix, b.b_id, b.description, b.donors, b.winning_ticket
FROM baskets b LEFT JOIN tickets t ON b.prefix = t.prefix AND b.winning_ticket = t.t_id FROM baskets b LEFT JOIN tickets t ON b.prefix = t.prefix AND b.winning_ticket = t.t_id
ORDER BY t.last_name, t.first_name, t.phone_number, b.prefix, b.b_id`) ORDER BY t.last_name, t.first_name, t.phone_number, b.prefix, b.b_id`);
await db.run(`CREATE VIEW IF NOT EXISTS report_counts AS SELECT prefix, COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))) AS unique_buyers, COUNT(*) AS total_buys await db.run(`CREATE VIEW IF NOT EXISTS report_counts AS SELECT prefix, COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))) AS unique_buyers, COUNT(*) AS total_buys
FROM tickets FROM tickets
GROUP BY prefix GROUP BY prefix
UNION ALL UNION ALL
SELECT 'Total', COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))), COUNT(*) SELECT 'Total', COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))), COUNT(*)
FROM tickets`); FROM tickets`);
};
export const handle = async ({ event, resolve }) => {
if (event.url.pathname.startsWith('/api')) {
const clientID = event.request.headers.get('TAM-CLIENT-ID');
if (env.PUBLIC_TAM_CLIENT_ID == clientID) {
const response = await resolve(event);
return response;
} else {
return new Response(JSON.stringify({detail: "Unauthorized"}), { status: 401 })
}
} else {
const response = await resolve(event);
return response;
}
}
export const handleFetch = async ({ request, fetch }) => {
request.headers.set('TAM-CLIENT-ID', env.PUBLIC_TAM_CLIENT_ID);
return fetch(request);
} }
@@ -40,7 +40,7 @@
} }
</script> </script>
<div class="flex flex-row gap-1 py-1 justify-between"> <div class="flex flex-row justify-between gap-1 py-1">
<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()} <button class={bS[prefix.color]} title="Alt + J" onclick={() => functions.dupDown()}
@@ -78,5 +78,19 @@
>Save Marked</button >Save Marked</button
> >
{/if} {/if}
{#if functions.cancel}
<button
class={bS[prefix.color]}
title="Alt + S"
onclick={() => {
const yes_no = confirm(
'This will cancel all pending changes. Are you sure you want to cancel?'
);
if (yes_no) {
functions.cancel();
}
}}>Cancel Marked</button
>
{/if}
</div> </div>
</div> </div>
@@ -43,6 +43,11 @@
oninput={() => { oninput={() => {
if (functions.pagerFromUpdate) functions.pagerFromUpdate(); if (functions.pagerFromUpdate) functions.pagerFromUpdate();
}} }}
onkeydown={(e) => {
if (e.key == "Enter") {
functions.getPage()
}
}}
bind:value={pager.idFrom} bind:value={pager.idFrom}
/> />
<div>-</div> <div>-</div>
@@ -54,6 +59,11 @@
onclick={(e) => { onclick={(e) => {
e.target.select(); e.target.select();
}} }}
onkeydown={(e) => {
if (e.key == "Enter") {
functions.getPage()
}
}}
bind:value={pager.idTo} bind:value={pager.idTo}
/> />
<button <button
+3 -3
View File
@@ -1,5 +1,5 @@
import { env } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
import { readFileSync, writeFileSync, mkdirSync } from 'fs'; import { readFileSync, writeFileSync } from 'fs';
const defaultSettings = { const defaultSettings = {
remote_server: '', remote_server: '',
@@ -7,7 +7,8 @@ const defaultSettings = {
remote_port: '8000', remote_port: '8000',
remote_tls: false, remote_tls: false,
default_pref: 'CALL', default_pref: 'CALL',
venue_name: 'TAM' venue_name: 'Venue Name',
disable_attrib: false
}; };
export const getSettings = () => { export const getSettings = () => {
@@ -16,7 +17,6 @@ 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 });
writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2), 'utf-8'); writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2), 'utf-8');
return defaultSettings; return defaultSettings;
} }
+6
View File
@@ -0,0 +1,6 @@
import { env } from "$env/dynamic/public"
export const load = async () => {
const tamClientID = `${env.PUBLIC_TAM_CLIENT_ID}`;
return { tamClientID }
}
+2 -1
View File
@@ -2,9 +2,10 @@ import { getSettings } from '$lib/server/settings';
export const load = async ({ fetch }) => { export const load = async ({ fetch }) => {
const settings = getSettings(); const settings = getSettings();
const disableAttrib = settings.disable_attrib || false;
const res = await fetch('/api'); const res = await fetch('/api');
const data = await res.json(); const data = await res.json();
const resPrefixes = await fetch('/api/prefixes'); const resPrefixes = await fetch('/api/prefixes');
const dataPrefixes = await resPrefixes.json(); const dataPrefixes = await resPrefixes.json();
return { ...data, venueName: settings.venue_name, prefixes: dataPrefixes }; return { ...data, venueName: settings.venue_name, prefixes: dataPrefixes, disableAttrib };
}; };
+5 -2
View File
@@ -134,12 +134,15 @@
{/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>
{#if !data.disableAttrib}
<p> <p>
Created by Dilan Gilluly. Support me on <a Created by Dilan Gilluly. <a
href="https://ko-fi.com/techguydilan" href="https://ko-fi.com/techguydilan"
class="text-blue-500">Ko-Fi</a class="text-blue-500"
target="_blank">My Ko-Fi</a
>. >.
</p> </p>
{/if}
</div> </div>
</div> </div>
</div> </div>
@@ -15,44 +15,50 @@ export const GET = async () => {
export const POST = async ({ request }) => { export const POST = async ({ request }) => {
const reqData = await request.json(); const reqData = await request.json();
for (let i = 0; i < reqData.prefixes.length; i += chunk_size) { if (reqData.prefixes) {
const chunk = reqData.prefixes.slice(i, i + chunk_size); for (let i = 0; i < reqData.prefixes.length; i += chunk_size) {
await db const chunk = reqData.prefixes.slice(i, i + chunk_size);
.insert(prefixes) await db
.values(chunk) .insert(prefixes)
.onConflictDoUpdate({ .values(chunk)
target: prefixes.prefix, .onConflictDoUpdate({
set: { color: sql`EXCLUDED.color`, weight: sql`EXCLUDED.weight` } target: prefixes.prefix,
}); set: { color: sql`EXCLUDED.color`, weight: sql`EXCLUDED.weight` }
} });
for (let i = 0; i < reqData.baskets.length; i += chunk_size) { };
const chunk = reqData.baskets.slice(i, i + chunk_size); };
await db if (reqData.baskets) {
.insert(baskets) for (let i = 0; i < reqData.baskets.length; i += chunk_size) {
.values(chunk) const chunk = reqData.baskets.slice(i, i + chunk_size);
.onConflictDoUpdate({ await db
target: [baskets.prefix, baskets.b_id], .insert(baskets)
set: { .values(chunk)
description: sql`EXCLUDED.description`, .onConflictDoUpdate({
donors: sql`EXCLUDED.donors`, target: [baskets.prefix, baskets.b_id],
winning_ticket: sql`EXCLUDED.winning_ticket` set: {
} description: sql`EXCLUDED.description`,
}); donors: sql`EXCLUDED.donors`,
} winning_ticket: sql`EXCLUDED.winning_ticket`
for (let i = 0; i < reqData.tickets.length; i += chunk_size) { }
const chunk = reqData.tickets.slice(i, i + chunk_size); });
await db };
.insert(tickets) };
.values(chunk) if (reqData.tickets) {
.onConflictDoNothing({ for (let i = 0; i < reqData.tickets.length; i += chunk_size) {
target: [tickets.prefix, tickets.t_id], const chunk = reqData.tickets.slice(i, i + chunk_size);
set: { await db
first_name: sql`EXCLUDED.first_name`, .insert(tickets)
last_name: sql`EXCLUDED.last_name`, .values(chunk)
phone_number: sql`EXCLUDED.phone_number`, .onConflictDoNothing({
pref: sql`EXCLUDED.pref` 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({ details: 'Data loaded successfully.' }); return json({ details: 'Data loaded successfully.' });
}; };
+28 -17
View File
@@ -7,7 +7,7 @@
import CommandBar from '$lib/client/components/CommandBar.svelte'; import CommandBar from '$lib/client/components/CommandBar.svelte';
let { data } = $props(); let { data } = $props();
let { prefix, prefixes } = $derived(data); let { prefix, prefixes, tamClientID } = $derived(data);
let pageTitle = $derived(`${prefix.prefix} Baskets | TAM`); let pageTitle = $derived(`${prefix.prefix} Baskets | TAM`);
@@ -30,7 +30,7 @@
let itemsLength = $derived(items.length || 1); let itemsLength = $derived(items.length || 1);
let itemsBuffer = $derived(items.filter((i) => i.changed)); let itemsBuffer = $derived(items.filter((i) => i.changed));
const functions = { const functions = {
async getPage(){ async getPage() {
this.save(); this.save();
if (pager.idFrom > pager.idTo) { if (pager.idFrom > pager.idTo) {
[pager.idFrom, pager.idTo] = [pager.idTo, pager.idFrom]; [pager.idFrom, pager.idTo] = [pager.idTo, pager.idFrom];
@@ -38,20 +38,25 @@
if (pager.idTo - pager.idFrom > 300) { if (pager.idTo - pager.idFrom > 300) {
pager.idTo = pager.idFrom + 300; pager.idTo = pager.idFrom + 300;
} }
const res = await fetch(`/api/baskets/${prefix.prefix}/${pager.idFrom}/${pager.idTo}`); const res = await fetch(`/api/baskets/${prefix.prefix}/${pager.idFrom}/${pager.idTo}`, {
headers: { 'TAM-CLIENT-ID': tamClientID }
});
const resData = await res.json(); const resData = await res.json();
resData.map((i) => (i.changed = false)); resData.map((i) => (i.changed = false));
items = [...resData]; items = [...resData];
setTimeout(() => focusIdx(0)); setTimeout(() => focusIdx(0));
}, },
pagerFromUpdate(){ pagerFromUpdate() {
pager.idTo = pager.idFrom + (itemsLength - 1); pager.idTo = pager.idFrom + (itemsLength - 1);
}, },
async save(){ async save() {
if (itemsBuffer.length > 0) { if (itemsBuffer.length > 0) {
const res = await fetch('/api/baskets', { const res = await fetch('/api/baskets', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
'TAM-CLIENT-ID': tamClientID
},
body: JSON.stringify(itemsBuffer) body: JSON.stringify(itemsBuffer)
}); });
if (res.ok) { if (res.ok) {
@@ -64,15 +69,21 @@
focusIdx(0); focusIdx(0);
}, 1); }, 1);
}, },
prevPage(){ cancel() {
if (itemsBuffer.length > 0) {
itemsBuffer.forEach((i) => (i.changed = false));
this.getPage();
}
},
prevPage() {
((pager.idFrom -= itemsLength), (pager.idTo -= itemsLength)); ((pager.idFrom -= itemsLength), (pager.idTo -= itemsLength));
this.getPage(); this.getPage();
}, },
nextPage(){ nextPage() {
((pager.idFrom += itemsLength), (pager.idTo += itemsLength)); ((pager.idFrom += itemsLength), (pager.idTo += itemsLength));
this.getPage(); this.getPage();
}, },
nextLine(){ nextLine() {
if (items[nextIdx]) { if (items[nextIdx]) {
setTimeout(() => { setTimeout(() => {
focusIdx(nextIdx); focusIdx(nextIdx);
@@ -83,7 +94,7 @@
}, 1); }, 1);
} }
}, },
prevLine(){ prevLine() {
if (curIdx > 0) { if (curIdx > 0) {
setTimeout(() => { setTimeout(() => {
focusIdx(prevIdx); focusIdx(prevIdx);
@@ -94,7 +105,7 @@
}, 1); }, 1);
} }
}, },
dupDown(){ dupDown() {
if (items[nextIdx]) { if (items[nextIdx]) {
const buffer = { ...items[curIdx] }; const buffer = { ...items[curIdx] };
['prefix', 'b_id'].forEach((key) => delete buffer[key]); ['prefix', 'b_id'].forEach((key) => delete buffer[key]);
@@ -104,7 +115,7 @@
focusIdx(curIdx); focusIdx(curIdx);
} }
}, },
dupUp(){ dupUp() {
if (curIdx > 0) { if (curIdx > 0) {
const buffer = { ...items[curIdx] }; const buffer = { ...items[curIdx] };
['prefix', 'b_id'].forEach((key) => delete buffer[key]); ['prefix', 'b_id'].forEach((key) => delete buffer[key]);
@@ -114,7 +125,7 @@
focusIdx(curIdx); focusIdx(curIdx);
} }
}, },
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]);
@@ -122,7 +133,7 @@
} }
focusIdx(curIdx); focusIdx(curIdx);
}, },
paste(){ paste() {
if (items[curIdx]) { if (items[curIdx]) {
const buffer = JSON.parse(window.localStorage.getItem('tam-basket')); const buffer = JSON.parse(window.localStorage.getItem('tam-basket'));
items[curIdx] = { ...items[curIdx], ...buffer, changed: true }; items[curIdx] = { ...items[curIdx], ...buffer, changed: true };
@@ -133,8 +144,8 @@
const headers = ['Basket ID', 'Description', 'Donors', 'Save?']; const headers = ['Basket ID', 'Description', 'Donors', 'Save?'];
if (browser) { if (browser) {
window.addEventListener('beforeunload', (e) => { window.addEventListener('beforeunload', () => {
if (itemsBuffer.length > 0) e.preventDefault(); if (itemsBuffer.length > 0) functions.save();
}); });
} }
</script> </script>
+28 -18
View File
@@ -7,7 +7,7 @@
import CommandBar from '$lib/client/components/CommandBar.svelte'; import CommandBar from '$lib/client/components/CommandBar.svelte';
let { data } = $props(); let { data } = $props();
let { prefix, prefixes } = $derived(data); let { prefix, prefixes, tamClientID } = $derived(data);
let pageTitle = $derived(`${prefix.prefix} Drawing Form | TAM`); let pageTitle = $derived(`${prefix.prefix} Drawing Form | TAM`);
@@ -30,7 +30,7 @@
let itemsLength = $derived(items.length || 1); let itemsLength = $derived(items.length || 1);
let itemsBuffer = $derived(items.filter((i) => i.changed)); let itemsBuffer = $derived(items.filter((i) => i.changed));
const functions = { const functions = {
async getPage(){ async getPage() {
this.save(); this.save();
if (pager.idFrom > pager.idTo) { if (pager.idFrom > pager.idTo) {
[pager.idFrom, pager.idTo] = [pager.idTo, pager.idFrom]; [pager.idFrom, pager.idTo] = [pager.idTo, pager.idFrom];
@@ -38,17 +38,19 @@
if (pager.idTo - pager.idFrom > 300) { if (pager.idTo - pager.idFrom > 300) {
pager.idTo = pager.idFrom + 300; pager.idTo = pager.idFrom + 300;
} }
const res = await fetch(`/api/drawing/${prefix.prefix}/${pager.idFrom}/${pager.idTo}`); const res = await fetch(`/api/drawing/${prefix.prefix}/${pager.idFrom}/${pager.idTo}`, {
headers: { 'TAM-CLIENT-ID': tamClientID }
});
const resData = await res.json(); const resData = await res.json();
resData.map((i) => (i.changed = false)); resData.map((i) => (i.changed = false));
items = [...resData]; items = [...resData];
setTimeout(() => focusIdx(0)); setTimeout(() => focusIdx(0));
}, },
async save(){ async save() {
if (itemsBuffer.length > 0) { if (itemsBuffer.length > 0) {
const res = await fetch('/api/drawing', { const res = await fetch('/api/drawing', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'TAM-CLIENT-ID': tamClientID },
body: JSON.stringify(itemsBuffer) body: JSON.stringify(itemsBuffer)
}); });
if (res.ok) { if (res.ok) {
@@ -61,18 +63,24 @@
focusIdx(0); focusIdx(0);
}, 1); }, 1);
}, },
pagerFromUpdate(){ cancel() {
pager.idTo = pager.idFrom + (itemsLength - 1); if (itemsBuffer.length > 0) {
itemsBuffer.forEach((i) => (i.changed = false));
this.getPage();
}
}, },
prevPage(){ pagerFromUpdate() {
pager.idTo = pager.idFrom + (itemsLength - 1);
},
prevPage() {
((pager.idFrom -= itemsLength), (pager.idTo -= itemsLength)); ((pager.idFrom -= itemsLength), (pager.idTo -= itemsLength));
this.getPage(); this.getPage();
}, },
nextPage(){ nextPage() {
((pager.idFrom += itemsLength), (pager.idTo += itemsLength)); ((pager.idFrom += itemsLength), (pager.idTo += itemsLength));
this.getPage(); this.getPage();
}, },
nextLine(){ nextLine() {
if (items[nextIdx]) { if (items[nextIdx]) {
setTimeout(() => { setTimeout(() => {
focusIdx(nextIdx); focusIdx(nextIdx);
@@ -83,7 +91,7 @@
}, 1); }, 1);
} }
}, },
prevLine(){ prevLine() {
if (curIdx > 0) { if (curIdx > 0) {
setTimeout(() => { setTimeout(() => {
focusIdx(prevIdx); focusIdx(prevIdx);
@@ -94,7 +102,7 @@
}, 1); }, 1);
} }
}, },
dupDown(){ dupDown() {
if (items[nextIdx]) { if (items[nextIdx]) {
const buffer = { ...items[curIdx] }; const buffer = { ...items[curIdx] };
['prefix', 'b_id'].forEach((key) => delete buffer[key]); ['prefix', 'b_id'].forEach((key) => delete buffer[key]);
@@ -104,7 +112,7 @@
focusIdx(curIdx); focusIdx(curIdx);
} }
}, },
dupUp(){ dupUp() {
if (curIdx > 0) { if (curIdx > 0) {
const buffer = { ...items[curIdx] }; const buffer = { ...items[curIdx] };
['prefix', 'b_id'].forEach((key) => delete buffer[key]); ['prefix', 'b_id'].forEach((key) => delete buffer[key]);
@@ -114,7 +122,7 @@
focusIdx(curIdx); focusIdx(curIdx);
} }
}, },
copy(){ copy() {
if (items[curIdx]) { if (items[curIdx]) {
const buffer = { ...items[curIdx] }; const buffer = { ...items[curIdx] };
['prefix', 'b_id'].forEach((key) => delete buffer[key]); ['prefix', 'b_id'].forEach((key) => delete buffer[key]);
@@ -122,7 +130,7 @@
} }
focusIdx(curIdx); focusIdx(curIdx);
}, },
paste(){ paste() {
if (items[curIdx]) { if (items[curIdx]) {
const buffer = JSON.parse(window.localStorage.getItem('tam-drawing')); const buffer = JSON.parse(window.localStorage.getItem('tam-drawing'));
items[curIdx] = { ...items[curIdx], ...buffer, changed: true }; items[curIdx] = { ...items[curIdx], ...buffer, changed: true };
@@ -133,8 +141,8 @@
const headers = ['Basket ID', 'Description', 'Winning Ticket', 'Winner', 'Save?']; const headers = ['Basket ID', 'Description', 'Winning Ticket', 'Winner', 'Save?'];
if (browser) { if (browser) {
window.addEventListener('beforeunload', (e) => { window.addEventListener('beforeunload', () => {
if (itemsBuffer.length > 0) e.preventDefault(); if (itemsBuffer.length > 0) functions.save();
}); });
} }
</script> </script>
@@ -185,7 +193,9 @@
id="{idx}_first" id="{idx}_first"
oninput={async () => { oninput={async () => {
item.changed = true; item.changed = true;
const res = await fetch(`/api/tickets/${prefix.prefix}/${item.winning_ticket}`); const res = await fetch(`/api/tickets/${prefix.prefix}/${item.winning_ticket}`, {
headers: { 'TAM-CLIENT-ID': tamClientID }
});
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
[item.last_name, item.first_name, item.phone_number] = [ [item.last_name, item.first_name, item.phone_number] = [
@@ -4,7 +4,7 @@
import { tS, bS } from '$lib/client/styles'; import { tS, bS } from '$lib/client/styles';
let { data } = $props(); let { data } = $props();
let prefixes = $derived(data.prefixes); let { prefixes, tamClientID } = $derived(data);
let tableData = $state([]); let tableData = $state([]);
let currentTimeout = $state(); let currentTimeout = $state();
let lastRefreshed = $state(''); let lastRefreshed = $state('');
@@ -12,7 +12,7 @@
const loadCounts = async () => { const loadCounts = async () => {
const rtnData = {}; const rtnData = {};
const res = await fetch('/api/reports/counts'); const res = await fetch('/api/reports/counts', {headers: {'TAM-CLIENT-ID': tamClientID}});
if (res.ok) { if (res.ok) {
prefixes.forEach((p) => (rtnData[p.prefix] = { ...p })); prefixes.forEach((p) => (rtnData[p.prefix] = { ...p }));
const resData = await res.json(); const resData = await res.json();
+13 -11
View File
@@ -6,7 +6,7 @@
import TicketSearchBar from '$lib/client/components/TicketSearchBar.svelte'; import TicketSearchBar from '$lib/client/components/TicketSearchBar.svelte';
let { data } = $props(); let { data } = $props();
let { prefix, prefixes } = $derived(data); let { prefix, prefixes, tamClientID } = $derived(data);
let pageTitle = 'Ticket Search | TAM'; let pageTitle = 'Ticket Search | TAM';
@@ -34,20 +34,22 @@
let items = $state([]); let items = $state([]);
let itemsBuffer = $derived(items.filter((i) => i.changed)); let itemsBuffer = $derived(items.filter((i) => i.changed));
const functions = { const functions = {
async search(){ async search() {
const searchParams = new URLSearchParams({ ...searchForm }); const searchParams = new URLSearchParams({ ...searchForm });
const res = await fetch(`/api/search/tickets?${searchParams.toString()}`); const res = await fetch(`/api/search/tickets?${searchParams.toString()}`, {
headers: { 'TAM-CLIENT-ID': tamClientID }
});
if (res.ok) { if (res.ok) {
const resData = await res.json(); const resData = await res.json();
items = [...resData]; items = [...resData];
setTimeout(() => focusIdx(0), 1); setTimeout(() => focusIdx(0), 1);
} }
}, },
async save(){ async save() {
if (itemsBuffer.length > 0) { if (itemsBuffer.length > 0) {
const res = await fetch('/api/search/tickets', { const res = await fetch('/api/search/tickets', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'TAM-CLIENT-ID': tamClientID },
body: JSON.stringify(itemsBuffer) body: JSON.stringify(itemsBuffer)
}); });
if (res.ok) { if (res.ok) {
@@ -60,7 +62,7 @@
focusIdx(0); focusIdx(0);
}, 1); }, 1);
}, },
nextLine(){ nextLine() {
if (items[nextIdx]) { if (items[nextIdx]) {
setTimeout(() => { setTimeout(() => {
focusIdx(nextIdx); focusIdx(nextIdx);
@@ -71,7 +73,7 @@
}, 1); }, 1);
} }
}, },
prevLine(){ prevLine() {
if (curIdx > 0) { if (curIdx > 0) {
setTimeout(() => { setTimeout(() => {
focusIdx(prevIdx); focusIdx(prevIdx);
@@ -82,7 +84,7 @@
}, 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]);
@@ -92,7 +94,7 @@
focusIdx(curIdx); focusIdx(curIdx);
} }
}, },
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]);
@@ -102,7 +104,7 @@
focusIdx(curIdx); focusIdx(curIdx);
} }
}, },
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]);
@@ -110,7 +112,7 @@
} }
setTimeout(() => focusIdx(curIdx), 1); setTimeout(() => focusIdx(curIdx), 1);
}, },
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 };
+13 -1
View File
@@ -5,6 +5,7 @@
import HeaderBar from '$lib/client/components/HeaderBar.svelte'; import HeaderBar from '$lib/client/components/HeaderBar.svelte';
let { data } = $props(); let { data } = $props();
let { tamClientID } = $derived(data);
let settings = $state({}); let settings = $state({});
let status = $state({ let status = $state({
message: '', message: '',
@@ -73,6 +74,17 @@
<div>Venue Name:</div> <div>Venue Name:</div>
<input type="text" id="venue_name" class={iS.normal} bind:value={settings.venue_name} /> <input type="text" id="venue_name" class={iS.normal} bind:value={settings.venue_name} />
</div> </div>
<div class="flex flex-row gap-1 items-center">
<div>Disable Attribution:</div>
<button
class={bS.gray}
onclick={() => {
settings.disable_attrib
? (settings.disable_attrib = false)
: (settings.disable_attrib = true);
}}>{settings.disable_attrib ? 'Yes' : 'No'}</button
>
</div>
<div class="flex flex-row gap-1 items-center"> <div class="flex flex-row gap-1 items-center">
<button <button
class={bS.gray} class={bS.gray}
@@ -80,7 +92,7 @@
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', 'TAM-CLIENT-ID': tamClientID }
}); });
if (!res.ok) { if (!res.ok) {
status.message = `Error Code: ${res.status}`; status.message = `Error Code: ${res.status}`;
@@ -1,8 +1,9 @@
<script> <script>
import HeaderBar from '$lib/client/components/HeaderBar.svelte'; import HeaderBar from '$lib/client/components/HeaderBar.svelte';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { bS, iS } from '$lib/client/styles'; import { bS, iS, tS } from '$lib/client/styles';
let { data } = $props(); let { data } = $props();
let { tamClientID } = $derived(data);
const pageTitle = 'Auth Keys | TAM'; const pageTitle = 'Auth Keys | TAM';
@@ -14,10 +15,11 @@
let authKeys = $state([]); let authKeys = $state([]);
let newDesc = $state(''); let newDesc = $state('');
let curKey = $state(''); let curKey = $state('');
let errorStatus = $state('');
async function getKeys() { async function getKeys() {
const res = await fetch('/api/auth', { const res = await fetch('/api/auth', {
headers: { 'TAM-PWD': auth.pwd } headers: { 'TAM-PWD': auth.pwd, 'TAM-CLIENT-ID': tamClientID }
}); });
if (res.ok) { if (res.ok) {
auth.toggle = true; auth.toggle = true;
@@ -25,8 +27,8 @@
authKeys = [...resData]; authKeys = [...resData];
curKey = data.authKey; curKey = data.authKey;
} else { } else {
auth.pwd = 'Invalid Password!'; errorStatus = 'Invalid Password!';
setTimeout(() => (auth.pwd = ''), 3000); setTimeout(() => (errorStatus = ''), 7000);
} }
} }
</script> </script>
@@ -45,9 +47,22 @@
{#if !auth.toggle} {#if !auth.toggle}
<div class="flex flex-row gap-1 items-center py-1"> <div class="flex flex-row gap-1 items-center py-1">
<div>Password:</div> <div>Password:</div>
<input type="password" id="pwd_input" class={iS.normal} bind:value={auth.pwd} /> <input
type="password"
id="pwd_input"
class={iS.normal}
onkeydown={(event) => {
if (event.key == 'Enter') {
getKeys();
}
}}
bind:value={auth.pwd}
/>
<button class={bS.gray} onclick={getKeys}>Login</button> <button class={bS.gray} onclick={getKeys}>Login</button>
</div> </div>
<div class="py-1">
<p class={tS.red}>{errorStatus}</p>
</div>
{:else} {:else}
<table class="w-full my-1"> <table class="w-full my-1">
<thead class="text-left"> <thead class="text-left">
@@ -72,7 +87,10 @@
onclick={async () => { onclick={async () => {
const res = await fetch('/api/settings', { const res = await fetch('/api/settings', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
'TAM-CLIENT-ID': tamClientID
},
body: JSON.stringify({ remote_key: key.auth_key }) body: JSON.stringify({ remote_key: key.auth_key })
}); });
if (res.ok) { if (res.ok) {
@@ -86,7 +104,7 @@
onclick={async () => { onclick={async () => {
const res = await fetch(`/api/auth?key_to_del=${key.auth_key}`, { const res = await fetch(`/api/auth?key_to_del=${key.auth_key}`, {
method: 'DELETE', method: 'DELETE',
headers: { 'TAM-PWD': auth.pwd } headers: { 'TAM-PWD': auth.pwd, 'TAM-CLIENT-ID': tamClientID }
}); });
if (res.ok) { if (res.ok) {
setTimeout(() => { setTimeout(() => {
@@ -112,7 +130,11 @@
if (newDesc) { if (newDesc) {
const res = await fetch('/api/auth', { const res = await fetch('/api/auth', {
method: 'POST', method: 'POST',
headers: { 'TAM-PWD': auth.pwd, 'Content-Type': 'application/json' }, headers: {
'TAM-PWD': auth.pwd,
'Content-Type': 'application/json',
'TAM-CLIENT-ID': tamClientID
},
body: JSON.stringify({ description: newDesc }) body: JSON.stringify({ description: newDesc })
}); });
if (res.ok) { if (res.ok) {
@@ -6,7 +6,7 @@
const pageTitle = 'Backup and Restore | TAM'; const pageTitle = 'Backup and Restore | TAM';
let { data } = $props(); let { data } = $props();
let remoteServer = $derived(data.remoteServer); let { remoteServer, tamClientID } = $derived(data);
let uploadFile = $state(); let uploadFile = $state();
let contents = $state(''); let contents = $state('');
let results = $state(''); let results = $state('');
@@ -31,7 +31,7 @@
hour: String(now.getHours()).padStart(2, '0'), hour: String(now.getHours()).padStart(2, '0'),
minutes: String(now.getMinutes()).padStart(2, '0') minutes: String(now.getMinutes()).padStart(2, '0')
}; };
const res = await fetch(fetch_url); const res = await fetch(fetch_url, { headers: { 'TAM-CLIENT-ID': tamClientID } });
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
const jsonString = JSON.stringify(data, null, 2); const jsonString = JSON.stringify(data, null, 2);
@@ -58,7 +58,7 @@
setTimeout(async () => { setTimeout(async () => {
const res = await fetch(`/api/backuprestore/${target}`, { const res = await fetch(`/api/backuprestore/${target}`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'TAM-CLIENT-ID': tamClientID },
body: contents body: contents
}); });
if (res.ok) setResult('File uploaded successfully. Check to see if your data exists.'); if (res.ok) setResult('File uploaded successfully. Check to see if your data exists.');
@@ -67,7 +67,10 @@
} }
async function pushData(target) { async function pushData(target) {
const res = await fetch(`/api/backuprestore/push/${target}`, { method: 'HEAD' }); const res = await fetch(`/api/backuprestore/push/${target}`, {
method: 'HEAD',
headers: { 'TAM-CLIENT-ID': tamClientID }
});
const targetStr = target.charAt(0).toUpperCase() + target.slice(1); const targetStr = target.charAt(0).toUpperCase() + target.slice(1);
if (res.ok) { if (res.ok) {
setResult(`${targetStr} pushed successfully.`); setResult(`${targetStr} pushed successfully.`);
@@ -5,6 +5,7 @@
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
let { data } = $props(); let { data } = $props();
let { tamClientID } = $derived(data);
const pageTitle = 'Prefixes | TAM'; const pageTitle = 'Prefixes | TAM';
@@ -56,7 +57,7 @@
if (editPrefix.prefix) { if (editPrefix.prefix) {
const req = await fetch('/api/prefixes', { const req = await fetch('/api/prefixes', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'TAM-CLIENT-ID': tamClientID },
body: JSON.stringify([editPrefix]) body: JSON.stringify([editPrefix])
}); });
if (req.ok) window.location.reload(); if (req.ok) window.location.reload();
@@ -125,7 +126,8 @@
class={bS[prefix.color]} class={bS[prefix.color]}
onclick={async () => { onclick={async () => {
const res = await fetch(`/api/prefixes?p=${prefix.prefix}`, { const res = await fetch(`/api/prefixes?p=${prefix.prefix}`, {
method: 'DELETE' method: 'DELETE',
headers: { 'TAM-CLIENT-ID': tamClientID }
}); });
if (res.ok) window.location.reload(); if (res.ok) window.location.reload();
}}>Delete</button }}>Delete</button
+6 -3
View File
@@ -26,9 +26,12 @@
<thead class="sticky top-0"> <thead class="sticky top-0">
<tr class="print:hidden"> <tr class="print:hidden">
<td colspan="50"> <td colspan="50">
<HeaderBar> <HeaderBar />
<div>{pageTitle}</div> </td>
</HeaderBar> </tr>
<tr class="print:hidden">
<td colspan="50">
<h1 class="text-lg font-bold">{pageTitle}</h1>
</td> </td>
</tr> </tr>
<tr class="print:hidden"> <tr class="print:hidden">
@@ -7,7 +7,7 @@
import CommandBar from '$lib/client/components/CommandBar.svelte'; import CommandBar from '$lib/client/components/CommandBar.svelte';
let { data } = $props(); let { data } = $props();
let { prefix, prefixes } = $derived(data); let { prefix, prefixes, tamClientID } = $derived(data);
let pageTitle = $derived(`${prefix.prefix} Tickets | TAM`); let pageTitle = $derived(`${prefix.prefix} Tickets | TAM`);
@@ -38,7 +38,7 @@
if (pager.idTo - pager.idFrom > 300) { if (pager.idTo - pager.idFrom > 300) {
pager.idTo = pager.idFrom + 300; pager.idTo = pager.idFrom + 300;
} }
const res = await fetch(`/api/tickets/${prefix.prefix}/${pager.idFrom}/${pager.idTo}`); const res = await fetch(`/api/tickets/${prefix.prefix}/${pager.idFrom}/${pager.idTo}`, {headers: {'TAM-CLIENT-ID': tamClientID}});
const resData = await res.json(); const resData = await res.json();
resData.map((i) => (i.changed = false)); resData.map((i) => (i.changed = false));
items = [...resData]; items = [...resData];
@@ -51,7 +51,7 @@
if (itemsBuffer.length > 0) { if (itemsBuffer.length > 0) {
const res = await fetch('/api/tickets', { const res = await fetch('/api/tickets', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'TAM-CLIENT-ID': tamClientID },
body: JSON.stringify(itemsBuffer) body: JSON.stringify(itemsBuffer)
}); });
if (res.ok) { if (res.ok) {
@@ -64,6 +64,12 @@
focusIdx(0); focusIdx(0);
}, 1); }, 1);
}, },
cancel(){
if (itemsBuffer.length > 0) {
itemsBuffer.forEach((i) => (i.changed = false));
this.getPage();
};
},
prevPage(){ prevPage(){
((pager.idFrom -= itemsLength), (pager.idTo -= itemsLength)); ((pager.idFrom -= itemsLength), (pager.idTo -= itemsLength));
this.getPage(); this.getPage();
@@ -133,8 +139,8 @@
const headers = ['Ticket ID', 'First Name', 'Last Name', 'Phone Number', 'Pref', 'Save?']; const headers = ['Ticket ID', 'First Name', 'Last Name', 'Phone Number', 'Pref', 'Save?'];
if (browser) { if (browser) {
window.addEventListener('beforeunload', (e) => { window.addEventListener('beforeunload', () => {
if (itemsBuffer.length > 0) e.preventDefault(); if (itemsBuffer.length > 0) functions.save();
}); });
} }
</script> </script>
@@ -212,10 +218,10 @@
item.changed = true; item.changed = true;
}} }}
onkeydown={(e) => { onkeydown={(e) => {
if (e.key == 't') { if (e.key == 't' || e.key == 'T') {
if (item.pref != 'TEXT') item.changed = true; if (item.pref != 'TEXT') item.changed = true;
item.pref = 'TEXT'; item.pref = 'TEXT';
} else if (e.key == 'c') { } else if (e.key == 'c' || e.key == 'C') {
if (item.pref != 'CALL') item.changed = true; if (item.pref != 'CALL') item.changed = true;
item.pref = 'CALL'; item.pref = 'CALL';
} }
@@ -3,4 +3,4 @@
if [ ! -d "./data" ]; then if [ ! -d "./data" ]; then
mkdir ./data mkdir ./data
fi fi
HOST=127.0.0.1 PORT=3000 ./exe/bin/node ./build NODE_TLS_REJECT_UNAUTHORIZED=0 BODY_SIZE_LIMIT=Infinity HOST=localhost PORT=3000 ./exe/bin/node ./build
+40
View File
@@ -0,0 +1,40 @@
{ config, pkgs, ... }
{
networking.hosts = {
"192.168.1.1" = [ "tam.lan" ]; # Change the IP of this line to the IP of the TAM server if applicable.
};
users.users."tam" = {
isNormalUser = true;
description = "Ticket Auction Manager";
};
services.displayManager.sddm.settings.Autologin = {
Session = "plasma.desktop";
User = "tam";
};
environment.systemPackages = with pkgs; [
chromium
];
virtualisation.docker.enable = true;
virtualisation.oci-containers = {
backend = "docker";
containers."tam-client" = {
image = "dbob16/tam-client:0.1.0"; # Change image to local repo to avoid pull limits.
volumes = [
"tam-data:/data"
];
environment = {
HOST = "localhost";
PORT = "3000";
};
extraOptions = ["--network=host"]
};
};
}