9 Commits
25 changed files with 1299 additions and 911 deletions

No files matched your search

+69 -2
View File
@@ -2,6 +2,73 @@
This is Ticket Auction Manager. A project I (Dilan Gilluly) am working on as a hobby project. It's main scope is to manage in person penny socials or benefit auctions. This is Ticket Auction Manager. A project I (Dilan Gilluly) am working on as a hobby project. It's main scope is to manage in person penny socials or benefit auctions.
## Under Development The remote server (api directory) is written in FastAPI and Python and the client (client directory) is written in Sveltekit.
This project is **NOT COMPLETE** and is under **active development**. Also I am not a programmer by profession so please forgive any beginner mistakes I make. Features:
- **Forms**: Facilitates the entry of data throughout the platform. The main goal of this is to allow one to manage in-person benefit auctions for non-profit causes.
- **Ticket Form**: Enter the names and phone numbers of ticket purchasers, as well as their contact preference, which the default is controllable via the Settings screen.
- **Basket Form**: Optionally, basket/item descriptions can be added as well as who the donor(s) are for each one. The descriptions appear on the reports later on.
- **Drawing Form**: Use this form to enter the winning ticket numbers. The form automatically looks up if an entry exists when a field is changed and will popluate the information next to it if it does.
- **Reports**: Reports are automatically generated with one click to avoid line shifts or other issues which may arise during compilation.
- **By Name Report**: This report orders the lines by the last name of each winner, then first name, phone number, and finally basket number.
- **By Basket Report**: Orders winners by basket number.
- **Counts Report**: Displays counts of ticket sales by prefix as well as totals.
- **Settings**:
- **Settings section**: The Admin/settings section can be accessed on the main menu by pressing Alt(option)+A. The combination toggles it so if you want it to go away again, just press Alt(option)+A again.
- **Settings page**: Allows you to control the base options of the operation. Including remote mode (leave the Remote Server field blank for standalone(offline) mode or put in a remote server to connect to a server), remote port, TLS, default contact preference, and the name of the venue/benefit (appears on main menu as well as reports).
- **Auth Keys**: Allows you to manage auth keys if it's in remote mode. To do so, you need to know the auth password on the server.
- **Prefixes**: Allows you to add or change prefixes which are available on the main menu to be able to access the respective forms for each prefix. Note, **you need to add prefixes through this form after first installation of either client or server to be able to access forms and reports.**
- **Remote Mode**: Remote mode, which is configurable in the Settings screen, by putting in a remote server address/hostname, allows data to be synched across multiple computers for large scale operations.
## Cloning the repo
To clone the repo you just need to run the git clone command to clone it to a directory of your choosing. I generally try to push to a few different git providers just so that if one has an outage, it allows you to go with another one for the time being. Replace 'yourrepofolder' at the end with the folder/dir of your choosing.
Gitlab:
`git clone https://www.gitlab.com/Ticket-Auction-Manager/tam yourrepofolder`
Github:
`git clone https://www.github.com/Ticket-Auction-Manager/tam yourrepofolder`
My own git server:
`git clone https://git.dilangilluly.us/Ticket-Auction-Manager/tam yourrepofolder`
## Installing dependencies
Server:
(needs Python installed, and a virtual environment activated depending on OS distro and Python version)
```
cd api
pip install -r app/requirements.txt
```
Client:
(needs pnpm installed)
```
cd client
pnpm install
```
## Running dev instances
Server:
```
cd api
sh start_dev.sh
```
Client:
```
cd client
pnpm dev
```
+13 -30
View File
@@ -1,42 +1,25 @@
# sv # Ticket Auction Manager (Client)
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). The contents of this directory of the Ticket Auction Manager repo pertains to just the client side. Which is a web-based client interface, allowing users to use Ticket Auction Manager.
## Creating a project Due to dev resources already being in the main repo, I have decided to make this dedicated to users instead of developers.
If you're seeing this, you've probably already done this step. Congrats! ## Running TAM
```sh You have some choice when it comes to running TAM, using Docker or through a portable Node environment (coming soon!).
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration: ### Docker
```sh Docker images are available under the dbob16/tam-client repo within Docker Hub. To pull the latest image:
# recreate this project
pnpm dlx sv@0.16.1 create --template minimal --no-types --add prettier eslint tailwindcss="plugins:none" sveltekit-adapter="adapter:node" drizzle="database:sqlite+sqlite:better-sqlite3" --install pnpm ./client
```
## Developing `docker pull dbob16/tam-client:latest`
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: Then to run it:
```sh `docker run -d --name tam-client --network host --restart unless-stopped -e HOST=127.0.0.1 -e PORT=3000 -v tam-data:/data dbob16/tam-client:latest`
npm run dev
# or start the server and open the app in a new browser tab It should then be available in a web browser at `http://localhost:3000` on the computer it is being run on. You can bookmark or make shortcuts to it for user convenience.
npm run dev -- --open
```
## Building #### Port Conflict
To create a production version of your app: If there is a conflict with the port when you go to start TAM, you can change the port by changing the number in the `-e PORT=3000` to another integer. For instance, if you have to change it to port 3001, you can change it to `-e PORT=3001` and then navigate to `http://localhost:3001` instead.
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
+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>
@@ -40,9 +40,14 @@
onclick={(e) => { onclick={(e) => {
e.target.select(); e.target.select();
}} }}
onchange={() => { 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.' });
}; };
+36 -29
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`);
@@ -27,35 +27,36 @@
let pager = $state({ idFrom: 0, idTo: 0 }); let pager = $state({ idFrom: 0, idTo: 0 });
let items = $state([]); let items = $state([]);
let itemsLength = $derived.by(() => (items.length ? items.length : 0)); 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 = {
getPage: async () => { async getPage() {
functions.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];
} }
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() {
if (itemsLength === 0) { pager.idTo = pager.idFrom + (itemsLength - 1);
pager.idTo = pager.idFrom;
} else {
pager.idTo = pager.idFrom + (itemsLength - 1);
}
}, },
save: async () => { 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) {
@@ -68,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));
functions.getPage(); this.getPage();
}, },
nextPage: () => { nextPage() {
((pager.idFrom += itemsLength), (pager.idTo += itemsLength)); ((pager.idFrom += itemsLength), (pager.idTo += itemsLength));
functions.getPage(); this.getPage();
}, },
nextLine: () => { nextLine() {
if (items[nextIdx]) { if (items[nextIdx]) {
setTimeout(() => { setTimeout(() => {
focusIdx(nextIdx); focusIdx(nextIdx);
@@ -87,7 +94,7 @@
}, 1); }, 1);
} }
}, },
prevLine: () => { prevLine() {
if (curIdx > 0) { if (curIdx > 0) {
setTimeout(() => { setTimeout(() => {
focusIdx(prevIdx); focusIdx(prevIdx);
@@ -98,27 +105,27 @@
}, 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]);
items[nextIdx] = { ...items[nextIdx], ...buffer, changed: true }; items[nextIdx] = { ...items[nextIdx], ...buffer, changed: true };
functions.nextLine(); this.nextLine();
} else { } else {
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]);
items[prevIdx] = { ...items[prevIdx], ...buffer, changed: true }; items[prevIdx] = { ...items[prevIdx], ...buffer, changed: true };
functions.prevLine(); this.prevLine();
} else { } else {
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]);
@@ -126,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 };
@@ -137,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>
@@ -186,7 +193,7 @@
type="text" type="text"
class="{iS.normal} w-full" class="{iS.normal} w-full"
id="{idx}_first" id="{idx}_first"
onchangecapture={() => (item.changed = true)} oninput={() => (item.changed = true)}
bind:value={item.description} bind:value={item.description}
/></td /></td
> >
@@ -195,7 +202,7 @@
type="text" type="text"
class="{iS.normal} w-full" class="{iS.normal} w-full"
id="{idx}_second" id="{idx}_second"
onchangecapture={() => (item.changed = true)} oninput={() => (item.changed = true)}
bind:value={item.donors} bind:value={item.donors}
/></td /></td
> >
+36 -30
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`);
@@ -27,35 +27,30 @@
let pager = $state({ idFrom: 0, idTo: 0 }); let pager = $state({ idFrom: 0, idTo: 0 });
let items = $state([]); let items = $state([]);
let itemsLength = $derived.by(() => (items.length ? items.length : 0)); 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 = {
getPage: async () => { async getPage() {
functions.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];
} }
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));
}, },
pagerFromUpdate(){ async save() {
if (itemsLength === 0) {
pager.idTo = pager.idFrom;
} else {
pager.idTo = pager.idFrom + (itemsLength - 1);
}
},
save: async () => {
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) {
@@ -68,15 +63,24 @@
focusIdx(0); focusIdx(0);
}, 1); }, 1);
}, },
prevPage: () => { cancel() {
if (itemsBuffer.length > 0) {
itemsBuffer.forEach((i) => (i.changed = false));
this.getPage();
}
},
pagerFromUpdate() {
pager.idTo = pager.idFrom + (itemsLength - 1);
},
prevPage() {
((pager.idFrom -= itemsLength), (pager.idTo -= itemsLength)); ((pager.idFrom -= itemsLength), (pager.idTo -= itemsLength));
functions.getPage(); this.getPage();
}, },
nextPage: () => { nextPage() {
((pager.idFrom += itemsLength), (pager.idTo += itemsLength)); ((pager.idFrom += itemsLength), (pager.idTo += itemsLength));
functions.getPage(); this.getPage();
}, },
nextLine: () => { nextLine() {
if (items[nextIdx]) { if (items[nextIdx]) {
setTimeout(() => { setTimeout(() => {
focusIdx(nextIdx); focusIdx(nextIdx);
@@ -87,7 +91,7 @@
}, 1); }, 1);
} }
}, },
prevLine: () => { prevLine() {
if (curIdx > 0) { if (curIdx > 0) {
setTimeout(() => { setTimeout(() => {
focusIdx(prevIdx); focusIdx(prevIdx);
@@ -98,27 +102,27 @@
}, 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]);
items[nextIdx] = { ...items[nextIdx], ...buffer, changed: true }; items[nextIdx] = { ...items[nextIdx], ...buffer, changed: true };
functions.nextLine(); this.nextLine();
} else { } else {
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]);
items[prevIdx] = { ...items[prevIdx], ...buffer, changed: true }; items[prevIdx] = { ...items[prevIdx], ...buffer, changed: true };
functions.prevLine(); this.prevLine();
} else { } else {
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]);
@@ -126,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 };
@@ -137,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>
@@ -187,9 +191,11 @@
type="number" type="number"
class="{iS.normal} w-full" class="{iS.normal} w-full"
id="{idx}_first" id="{idx}_first"
onchangecapture={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();
+18 -16
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 = {
search: async () => { 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);
} }
}, },
save: async () => { 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,27 +84,27 @@
}, 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(); this.nextLine();
} else { } else {
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]);
items[prevIdx] = { ...items[prevIdx], ...buffer, changed: true }; items[prevIdx] = { ...items[prevIdx], ...buffer, changed: true };
functions.prevLine(); this.prevLine();
} else { } else {
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 };
@@ -171,7 +173,7 @@
type="text" type="text"
class="{iS.normal} w-full" class="{iS.normal} w-full"
id="{idx}_first" id="{idx}_first"
onchangecapture={() => (item.changed = true)} oninput={() => (item.changed = true)}
bind:value={item.first_name} bind:value={item.first_name}
/></td /></td
> >
@@ -180,7 +182,7 @@
type="text" type="text"
class="{iS.normal} w-full" class="{iS.normal} w-full"
id="{idx}_second" id="{idx}_second"
onchangecapture={() => (item.changed = true)} oninput={() => (item.changed = true)}
bind:value={item.last_name} bind:value={item.last_name}
/></td /></td
> >
@@ -189,7 +191,7 @@
type="text" type="text"
class="{iS.normal} w-full" class="{iS.normal} w-full"
id="{idx}_third" id="{idx}_third"
onchangecapture={() => (item.changed = true)} oninput={() => (item.changed = true)}
bind:value={item.phone_number} bind:value={item.phone_number}
/></td /></td
> >
+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">
+32 -30
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} Tickets | TAM`); let pageTitle = $derived(`${prefix.prefix} Tickets | TAM`);
@@ -27,35 +27,31 @@
let pager = $state({ idFrom: 0, idTo: 0 }); let pager = $state({ idFrom: 0, idTo: 0 });
let items = $state([]); let items = $state([]);
let itemsLength = $derived.by(() => (items.length ? items.length : 0)); 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 = {
getPage: async () => { async getPage(){
functions.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];
} }
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];
setTimeout(() => focusIdx(0)); setTimeout(() => focusIdx(0));
}, },
pagerFromUpdate(){ pagerFromUpdate(){
if (itemsLength === 0) { pager.idTo = pager.idFrom + (itemsLength - 1);
pager.idTo = pager.idFrom;
} else {
pager.idTo = pager.idFrom + (itemsLength - 1);
}
}, },
save: async () => { async save(){
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) {
@@ -68,15 +64,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));
functions.getPage(); this.getPage();
}, },
nextPage: () => { nextPage(){
((pager.idFrom += itemsLength), (pager.idTo += itemsLength)); ((pager.idFrom += itemsLength), (pager.idTo += itemsLength));
functions.getPage(); this.getPage();
}, },
nextLine: () => { nextLine(){
if (items[nextIdx]) { if (items[nextIdx]) {
setTimeout(() => { setTimeout(() => {
focusIdx(nextIdx); focusIdx(nextIdx);
@@ -87,7 +89,7 @@
}, 1); }, 1);
} }
}, },
prevLine: () => { prevLine(){
if (curIdx > 0) { if (curIdx > 0) {
setTimeout(() => { setTimeout(() => {
focusIdx(prevIdx); focusIdx(prevIdx);
@@ -98,17 +100,17 @@
}, 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(); this.nextLine();
} else { } else {
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]);
@@ -118,7 +120,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]);
@@ -126,7 +128,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 };
@@ -137,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>
@@ -186,7 +188,7 @@
type="text" type="text"
class="{iS.normal} w-full" class="{iS.normal} w-full"
id="{idx}_first" id="{idx}_first"
onchangecapture={() => (item.changed = true)} oninput={() => (item.changed = true)}
bind:value={item.first_name} bind:value={item.first_name}
/></td /></td
> >
@@ -195,7 +197,7 @@
type="text" type="text"
class="{iS.normal} w-full" class="{iS.normal} w-full"
id="{idx}_second" id="{idx}_second"
onchangecapture={() => (item.changed = true)} oninput={() => (item.changed = true)}
bind:value={item.last_name} bind:value={item.last_name}
/></td /></td
> >
@@ -204,7 +206,7 @@
type="text" type="text"
class="{iS.normal} w-full" class="{iS.normal} w-full"
id="{idx}_third" id="{idx}_third"
onchangecapture={() => (item.changed = true)} oninput={() => (item.changed = true)}
bind:value={item.phone_number} bind:value={item.phone_number}
/></td /></td
> >
@@ -216,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';
} }
+1 -2
View File
@@ -2,8 +2,7 @@
source ../../version.env source ../../version.env
docker compose build docker compose build
docker image tag dbob16/tam-client:latest dbob16/tam-client:${TAM_VERSION} docker image tag dbob16/tam-client:latest dbob16/tam-client:${TAM_VERSION}
mkdir -p dist/images echo "HOST=127.0.0.1" > dist/prod.env
echo "HOST=127.0.0.1" >> dist/prod.env
echo "PORT=3000" >> dist/prod.env echo "PORT=3000" >> dist/prod.env
TAM_VERSION=${TAM_VERSION} envsubst < ./run-client.sh.template > ./dist/run-client.sh TAM_VERSION=${TAM_VERSION} envsubst < ./run-client.sh.template > ./dist/run-client.sh
chmod +x dist/run-client.sh chmod +x dist/run-client.sh
@@ -0,0 +1,6 @@
#!/bin/sh
if [ ! -d "./data" ]; then
mkdir ./data
fi
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"]
};
};
}