Compare commits

10 Commits
42 changed files with 1357 additions and 861 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
deployments/
+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
```
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
*/__pycache__/
+9
View File
@@ -0,0 +1,9 @@
FROM python:3-slim
WORKDIR /app
COPY app/requirements.txt .
RUN pip install -r requirements.txt
COPY app/. .
RUN mkdir -p /data
ENV TAM_DATA_DIR=/data
ENV NODE_TLS_REJECT_UNAUTHORIZED=0
CMD ["fastapi", "run", "--port=8000"]
+6 -1
View File
@@ -1,7 +1,9 @@
import os
from dataclasses import dataclass from dataclasses import dataclass
import uvicorn
from db import init_db from db import init_db
from fastapi import FastAPI, Header from fastapi import FastAPI, Header
import os
from routers import imp_routers from routers import imp_routers
from system.auth import AuthRepo from system.auth import AuthRepo
@@ -29,3 +31,6 @@ def get_main_route(tam_key: str = Header("")):
return MainRoute(authenticated=AuthRepo().check_key(tam_key)) return MainRoute(authenticated=AuthRepo().check_key(tam_key))
imp_routers(app) imp_routers(app)
if __name__ == "__main__":
uvicorn.run(app, port=8000)
+1
View File
@@ -0,0 +1 @@
fastapi[standard]
+4
View File
@@ -0,0 +1,4 @@
*
!build/
!package.json
+9
View File
@@ -0,0 +1,9 @@
FROM node:24-slim
WORKDIR /app
COPY . .
RUN npm install --omit=dev
RUN mkdir -p /data
ENV TAM_DATA_DIR=/data
ENV NODE_TLS_REJECT_UNAUTHORIZED=0
ENV BODY_SIZE_LIMIT=Infinity
CMD ["node", "build"]
+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
+25 -2
View File
@@ -1,6 +1,9 @@
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 () => {
env.PUBLIC_TAM_CLIENT_ID = randomUUID();
await db.run(`CREATE TABLE IF NOT EXISTS baskets ( await db.run(`CREATE TABLE IF NOT EXISTS baskets (
prefix text, prefix text,
b_id integer, b_id integer,
@@ -31,11 +34,31 @@ export const init = async () => {
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);
} }
@@ -77,6 +77,11 @@
<button class={bS[prefix.color]} title="Alt + S" onclick={() => functions.save()} <button class={bS[prefix.color]} title="Alt + S" onclick={() => functions.save()}
>Save Marked</button >Save Marked</button
> >
{/if}
{#if functions.cancel}
<button class={bS[prefix.color]} title="Alt + S" onclick={() => functions.cancel()}
>Cancel Marked</button
>
{/if} {/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,6 +15,7 @@ export const GET = async () => {
export const POST = async ({ request }) => { export const POST = async ({ request }) => {
const reqData = await request.json(); const reqData = await request.json();
if (reqData.prefixes) {
for (let i = 0; i < reqData.prefixes.length; i += chunk_size) { for (let i = 0; i < reqData.prefixes.length; i += chunk_size) {
const chunk = reqData.prefixes.slice(i, i + chunk_size); const chunk = reqData.prefixes.slice(i, i + chunk_size);
await db await db
@@ -24,7 +25,9 @@ export const POST = async ({ request }) => {
target: prefixes.prefix, target: prefixes.prefix,
set: { color: sql`EXCLUDED.color`, weight: sql`EXCLUDED.weight` } set: { color: sql`EXCLUDED.color`, weight: sql`EXCLUDED.weight` }
}); });
} };
};
if (reqData.baskets) {
for (let i = 0; i < reqData.baskets.length; i += chunk_size) { for (let i = 0; i < reqData.baskets.length; i += chunk_size) {
const chunk = reqData.baskets.slice(i, i + chunk_size); const chunk = reqData.baskets.slice(i, i + chunk_size);
await db await db
@@ -38,7 +41,9 @@ export const POST = async ({ request }) => {
winning_ticket: sql`EXCLUDED.winning_ticket` winning_ticket: sql`EXCLUDED.winning_ticket`
} }
}); });
} };
};
if (reqData.tickets) {
for (let i = 0; i < reqData.tickets.length; i += chunk_size) { for (let i = 0; i < reqData.tickets.length; i += chunk_size) {
const chunk = reqData.tickets.slice(i, i + chunk_size); const chunk = reqData.tickets.slice(i, i + chunk_size);
await db await db
@@ -53,6 +58,7 @@ export const POST = async ({ request }) => {
pref: sql`EXCLUDED.pref` pref: sql`EXCLUDED.pref`
} }
}); });
} };
};
return json({ details: 'Data loaded successfully.' }); return json({ details: 'Data loaded successfully.' });
}; };
+34 -27
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;
} else {
pager.idTo = pager.idFrom + (itemsLength - 1); 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.`);
@@ -109,7 +112,9 @@
class="{iS.normal} rounded file:bg-gray-300 file:border file:border-black file:px-2 file:py-1 file:rounded" class="{iS.normal} rounded file:bg-gray-300 file:border file:border-black file:px-2 file:py-1 file:rounded"
bind:files={uploadFile} bind:files={uploadFile}
/> />
{#if !remoteServer}
<button class={bS.gray} onclick={() => fileUpload('local')}>Upload to Local</button> <button class={bS.gray} onclick={() => fileUpload('local')}>Upload to Local</button>
{/if}
{#if remoteServer} {#if remoteServer}
<button class={bS.gray} onclick={() => fileUpload('remote')}>Upload to Remote</button> <button class={bS.gray} onclick={() => fileUpload('remote')}>Upload to Remote</button>
{/if} {/if}
@@ -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">
+29 -27
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;
} else {
pager.idTo = pager.idFrom + (itemsLength - 1); 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
> >
+8
View File
@@ -0,0 +1,8 @@
# docker compose build
source ../../version.env
docker compose build
docker image tag dbob16/tam-client:latest dbob16/tam-client:${TAM_VERSION}
echo "HOST=127.0.0.1" > dist/prod.env
echo "PORT=3000" >> dist/prod.env
TAM_VERSION=${TAM_VERSION} envsubst < ./run-client.sh.template > ./dist/run-client.sh
chmod +x dist/run-client.sh
+9
View File
@@ -0,0 +1,9 @@
services:
tam-client:
build: ../../../client/.
image: dbob16/tam-client:latest
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
volumes:
- "./data:/data:Z"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
source ./prod.env
if [ -x "$(command -v docker)" ]; then
docker run -d --name tam-client --network host --env-file ./prod.env -v tam-data:/data --restart unless-stopped dbob16/tam-client:${TAM_VERSION}
elif [ -x "$(command -v podman)" ]; then
podman run -d --name tam-client --network host --env-file ./prod.env -v tam-data:/data --restart unless-stopped dbob16/tam-client:${TAM_VERSION}
fi
@@ -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
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
source ../../version.env
docker compose build
docker tag dbob16/tam-server:latest dbob16/tam-server:${TAM_VERSION}
docker tag dbob16/tam-rp:latest dbob16/tam-rp:${TAM_VERSION}
echo "TAM_PWD=changeme" > dist/prod.env
TAM_VERSION=${TAM_VERSION} envsubst '$TAM_VERSION' < ./compose.yml.template > ./dist/compose.yml
+15
View File
@@ -0,0 +1,15 @@
services:
tam-server:
build: ../../../api/.
image: dbob16/tam-server:latest
restart: unless-stopped
ports:
- "8000:8000"
tam-rp:
build: ../../../rp/.
image: dbob16/tam-rp:latest
restart: unless-stopped
environment:
- "TAM_SERVER=tam-server"
ports:
- "8443:8443"
@@ -0,0 +1,17 @@
services:
tam-server:
image: dbob16/tam-server:${TAM_VERSION}
restart: unless-stopped
environment:
- "TAM_PWD=${TAM_PWD}"
volumes:
- "./data:/data:Z"
ports:
- "8000:8000"
tam-rp:
image: dbob16/tam-rp:${TAM_VERSION}
restart: unless-stopped
environment:
- "TAM_SERVER=tam-server"
ports:
- "8443:8443"
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
if [ -x "$(command -v docker)" ]; then
docker compose --env-file ./prod.env up -d
elif [ -x "$(command -v podman)"]; then
podman compose --env-file ./prod.env up -d
fi
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
read -p "Enter new password: " NEW_PASSWORD
echo "TAM_PWD=${NEW_PASSWORD}" > prod.env
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
if [ -x "$(command -v docker)" ]; then
docker compose --env-file ./prod.env down
elif [ -x "$(command -v podman)" ]; then
podman compose --env-file ./prod.env down
fi
+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"]
};
};
}
+4
View File
@@ -0,0 +1,4 @@
*:8443 {
tls internal
reverse_proxy {$TAM_SERVER:localhost}:8000
}
+3
View File
@@ -0,0 +1,3 @@
FROM caddy:2
COPY Caddyfile /etc/caddy/Caddyfile
ENV TAM_SERVER=tam-server