Compare commits

..

10 Commits

39 changed files with 1690 additions and 713 deletions

View File

@@ -1,3 +1,4 @@
__pycache__/ __pycache__/
*/__pycache__/ */__pycache__/
data/ data/
build.sh

6
api/build.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
source ../deployment/common.sh
docker build -t dbob16/tam3-api:${tam3_version} .
docker tag dbob16/tam3-api:${tam3_version} dbob16/tam3-api:latest

View File

@@ -2,7 +2,6 @@
from fastapi import FastAPI from fastapi import FastAPI
from sys import argv from sys import argv
from exceptions import bad_key
from repos.api_keys import ApiKeyRepo from repos.api_keys import ApiKeyRepo
@@ -13,6 +12,7 @@ from routers.combined import combined_router
from routers.reports import report_router from routers.reports import report_router
from routers.backuprestore import backup_router from routers.backuprestore import backup_router
from routers.counts import counts_router from routers.counts import counts_router
from routers.search import search_router
if argv[1] == "run": if argv[1] == "run":
app = FastAPI(title="TAM3 API Server", docs_url=None, redoc_url=None) app = FastAPI(title="TAM3 API Server", docs_url=None, redoc_url=None)
@@ -32,3 +32,4 @@ app.include_router(combined_router)
app.include_router(report_router) app.include_router(report_router)
app.include_router(backup_router) app.include_router(backup_router)
app.include_router(counts_router) app.include_router(counts_router)
app.include_router(search_router)

16
api/repos/search.py Normal file
View File

@@ -0,0 +1,16 @@
from .template import Repo
from .tickets import Ticket
class SearchRepo(Repo):
def SearchTickets(
self, first_name: str = "", last_name: str = "", phone_number: str = ""
):
self.cur.execute(
'SELECT * FROM tickets WHERE first_name LIKE %s AND last_name LIKE %s AND phone_number LIKE %s',
(f"%{first_name}%", f"%{last_name}%", f"%{phone_number}%"),
)
records = self.cur.fetchall()
if not records:
return []
return [Ticket(*r) for r in records]

17
api/routers/search.py Normal file
View File

@@ -0,0 +1,17 @@
from exceptions import bad_key
from fastapi import APIRouter
from repos.api_keys import ApiKeyRepo
from repos.search import SearchRepo
search_router = APIRouter(prefix="/api/search")
@search_router.get("/tickets/")
def search_tickets(
api_key: str, first_name: str = "", last_name: str = "", phone_number: str = ""
):
if not ApiKeyRepo().check_api(api_key):
raise bad_key
return SearchRepo().SearchTickets(
first_name=first_name, last_name=last_name, phone_number=phone_number
)

1
db/.dockerignore Normal file
View File

@@ -0,0 +1 @@
build.sh

6
db/build.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
source ../deployment/common.sh
docker build -t dbob16/tam3-db:${tam3_version} .
docker tag dbob16/tam3-db:${tam3_version} dbob16/tam3-db:latest

View File

@@ -1,5 +1,7 @@
#!/bin/bash #!/bin/bash
tam3_version="0.3.0"
mkdir -p ~/.config/TAM3/data mkdir -p ~/.config/TAM3/data
read -p "Do you want to connect to a remote server? [y or n] " rmserver read -p "Do you want to connect to a remote server? [y or n] " rmserver
@@ -9,9 +11,9 @@ if [ $rmserver = "y" -o $rmserver = "Y" ]; then
read -p "Enter the protocol, server host/ip, and port like "https://ip_or_host:8443" w/o quotes: " serveraddr read -p "Enter the protocol, server host/ip, and port like "https://ip_or_host:8443" w/o quotes: " serveraddr
read -p "Paste in (Ctrl + Shift + V on most terminal emulators) or enter the api key you generated for your server: " serverapi read -p "Paste in (Ctrl + Shift + V on most terminal emulators) or enter the api key you generated for your server: " serverapi
if [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
docker run -d --name=tam3-webclient --restart=always -v ~/.config/TAM3/data:/data:rw,z -e TAM3_REMOTE=$serveraddr -e TAM3_REMOTE_KEY=$serverapi -e PUBLIC_TAM3_VENUE="$venuename" -p 127.0.0.1:8300:3000 docker.io/dbob16/tam3-webclient:0.2.0 docker run -d --name=tam3-webclient --restart=always -v ~/.config/TAM3/data:/data:rw,z -e TAM3_REMOTE=$serveraddr -e TAM3_REMOTE_KEY=$serverapi -e PUBLIC_TAM3_VENUE="$venuename" -p 127.0.0.1:8300:3000 docker.io/dbob16/tam3-webclient:${tam3_version}
elif [ -x "$(command -v podman)" ]; then elif [ -x "$(command -v podman)" ]; then
podman run -d --name=tam3-webclient --restart=always -v ~/.config/TAM3/data:/data:rw,z -e TAM3_REMOTE=$serveraddr -e TAM3_REMOTE_KEY=$serverapi -e PUBLIC_TAM3_VENUE="$venuename" -p 127.0.0.1:8300:3000 docker.io/dbob16/tam3-webclient:0.2.0 podman run -d --name=tam3-webclient --restart=always -v ~/.config/TAM3/data:/data:rw,z -e TAM3_REMOTE=$serveraddr -e TAM3_REMOTE_KEY=$serverapi -e PUBLIC_TAM3_VENUE="$venuename" -p 127.0.0.1:8300:3000 docker.io/dbob16/tam3-webclient:${tam3_version}
runin_podman="true" runin_podman="true"
else else
echo "Neither Docker nor Podman are installed. Please install whichever you prefer and try again." echo "Neither Docker nor Podman are installed. Please install whichever you prefer and try again."
@@ -19,9 +21,9 @@ exit 1
fi fi
else else
if [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
docker run -d --name=tam3-webclient --restart=always -v ~/.config/TAM3/data:/data:rw,z -e PUBLIC_TAM3_VENUE="$venuename" -p 127.0.0.1:8300:3000 docker.io/dbob16/tam3-webclient:0.2.0 docker run -d --name=tam3-webclient --restart=always -v ~/.config/TAM3/data:/data:rw,z -e PUBLIC_TAM3_VENUE="$venuename" -p 127.0.0.1:8300:3000 docker.io/dbob16/tam3-webclient:${tam3_version}
elif [ -x "$(command -v podman )" ]; then elif [ -x "$(command -v podman )" ]; then
podman run -d --name=tam3-webclient --restart=always -v ~/.config/TAM3/data:/data:rw,z -e PUBLIC_TAM3_VENUE="$venuename" -p 127.0.0.1:8300:3000 docker.io/dbob16/tam3-webclient:0.2.0 podman run -d --name=tam3-webclient --restart=always -v ~/.config/TAM3/data:/data:rw,z -e PUBLIC_TAM3_VENUE="$venuename" -p 127.0.0.1:8300:3000 docker.io/dbob16/tam3-webclient:${tam3_version}
runin_podman="true" runin_podman="true"
else else
echo "Neither Docker nor Podman are installed. Please install whichever you prefer and try again." echo "Neither Docker nor Podman are installed. Please install whichever you prefer and try again."

View File

@@ -0,0 +1,5 @@
#!/bin/bash
source ../common.sh
tar cvzf tam3-webclient-full_${tam3_version}.tar.gz client-load.sh client-launch.sh tam3-webclient.tar.gz

View File

@@ -0,0 +1,5 @@
#!/bin/bash
source ../common.sh
docker save dbob16/tam3-webclient:${tam3_version} | gzip > tam3-webclient.tar.gz

3
deployment/common.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
export tam3_version="0.3.0"

View File

@@ -1,6 +1,6 @@
services: services:
tam3-db: tam3-db:
image: docker.io/dbob16/tam3-db:0.2.0 image: docker.io/dbob16/tam3-db:${TAM3_VERSION}
restart: always restart: always
environment: environment:
MARIADB_RANDOM_ROOT_PASSWORD: 1 MARIADB_RANDOM_ROOT_PASSWORD: 1
@@ -16,7 +16,7 @@ services:
timeout: 5s timeout: 5s
retries: 3 retries: 3
tam3-api: tam3-api:
image: docker.io/dbob16/tam3-api:0.2.0 image: docker.io/dbob16/tam3-api:${TAM3_VERSION}
restart: always restart: always
environment: environment:
TAM3_DATA_PATH: /data TAM3_DATA_PATH: /data

View File

@@ -0,0 +1,10 @@
read -p "Enter the key that you want to delete: " apikey
if [ -x "$(command -v docker)" ]; then
docker compose exec tam3-api /app/key.py delete $apikey
elif [ -x "$(command -v podman)" ]; then
podman compose exec tam3-api /app/key.py delete $apikey
else
echo "Neither Docker nor Podman are installed. Please install whichever you prefer, then try again."
exit 1
fi

View File

@@ -0,0 +1,5 @@
#!/bin/bash
source ../common.sh
tar -cvzf tam3-remote-server_${tam3_version}.tar.gz compose.yml delete-key.sh generate-key.sh list-keys.sh start-server.sh

View File

@@ -4,6 +4,7 @@ gen_password=$(cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)
echo "DB_LOCATION=./tam3-db" > .env echo "DB_LOCATION=./tam3-db" > .env
echo "DB_PASSWORD=${gen_password}" >> .env echo "DB_PASSWORD=${gen_password}" >> .env
echo "TAM3_VERSION=0.3.0" >> .env
if [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
docker compose up -d docker compose up -d

View File

@@ -0,0 +1,16 @@
#!/bin/bash
source ../common.sh
if [ -x "$(command -v docker)" ]; then
docker save dbob16/tam3-api:${tam3_version} | gzip > tam3-api.tar.gz
docker save dbob16/tam3-db:${tam3_version} | gzip > tam3-db.tar.gz
elif [ -x "$(command -v podman)" ]; then
podman save dbob16/tam3-api:${tam3_version} | gzip > tam3-api.tar.gz
podman save dbob16/tam3-db:${tam3_version} | gzip > tam3-db.tar.gz
else
echo "Neither Docker nor Podman are installed. Please install whichever you prefer and try again."
exit 1
fi
tar -cvzf tam3-server-offline-images_${tam3_version}.tar.gz tam3-api.tar.gz tam3-db.tar.gz server-load.sh

View File

@@ -1,6 +1,6 @@
services: services:
tam3-db: tam3-db:
image: docker.io/dbob16/tam3-db:0.2.0 image: docker.io/dbob16/tam3-db:${TAM3_VERSION}
restart: always restart: always
environment: environment:
MARIADB_RANDOM_ROOT_PASSWORD: 1 MARIADB_RANDOM_ROOT_PASSWORD: 1
@@ -16,7 +16,7 @@ services:
timeout: 5s timeout: 5s
retries: 3 retries: 3
tam3-api: tam3-api:
image: docker.io/dbob16/tam3-api:0.2.0 image: docker.io/dbob16/tam3-api:0.2.0${TAM3_VERSION}
restart: always restart: always
environment: environment:
TAM3_DATA_PATH: /data TAM3_DATA_PATH: /data

View File

@@ -0,0 +1,10 @@
read -p "Enter the key that you want to delete: " apikey
if [ -x "$(command -v docker)" ]; then
docker compose exec tam3-api /app/key.py delete $apikey
elif [ -x "$(command -v podman)" ]; then
podman compose exec tam3-api /app/key.py delete $apikey
else
echo "Neither Docker nor Podman are installed. Please install whichever you prefer, then try again."
exit 1
fi

View File

@@ -0,0 +1,5 @@
#!/bin/bash
source ../common.sh
tar -cvzf tam3-remote-server-secure_${tam3_version}.tar.gz compose.yml delete-key.sh generate-key.sh list-keys.sh start-server.sh nginx/

View File

@@ -8,6 +8,7 @@ gen_password=$(cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)
echo "DB_LOCATION=./tam3-db" > .env echo "DB_LOCATION=./tam3-db" > .env
echo "DB_PASSWORD=${gen_password}" >> .env echo "DB_PASSWORD=${gen_password}" >> .env
echo "TAM3_VERSION=0.3.0"
if [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
docker compose up -d docker compose up -d

View File

@@ -1,2 +1,3 @@
/node_modules/ /node_modules/
*.db *.db
build.sh

View File

@@ -1,10 +1,16 @@
FROM docker.io/node:lts-alpine AS build FROM docker.io/node:lts-alpine AS build
RUN mkdir /data ENV NODE_ENV=production
WORKDIR /app
COPY . . WORKDIR /app
RUN npm install && npm run build COPY package.json package-lock.json ./
RUN npm ci
RUN mkdir /data
COPY build/ ./build/
COPY drizzle.config.js .
COPY drizzle/ ./drizzle/
ENV DATABASE_URL=file:/data/local.db ENV DATABASE_URL=file:/data/local.db
ENV SETTINGS_PATH=/data/settings.json ENV SETTINGS_PATH=/data/settings.json

8
webapp/build.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
npm run build
source ../deployment/common.sh
docker build -t dbob16/tam3-webclient:${tam3_version} .
docker tag dbob16/tam3-webclient:${tam3_version} dbob16/tam3-webclient:latest

692
webapp/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,18 +14,16 @@
"db:studio": "drizzle-kit studio" "db:studio": "drizzle-kit studio"
}, },
"devDependencies": { "devDependencies": {
"@libsql/client": "^0.14.0",
"@sveltejs/adapter-node": "^5.2.12", "@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.22.0", "@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0", "@sveltejs/vite-plugin-svelte": "^6.0.0",
"@types/node": "^22", "@types/node": "^22",
"drizzle-kit": "^0.30.2",
"drizzle-orm": "^0.40.0",
"svelte": "^5.0.0", "svelte": "^5.0.0",
"vite": "^7.0.4" "vite": "^7.0.4"
}, },
"dependencies": { "dependencies": {
"hotkeys-js": "^3.13.15", "hotkeys-js": "^3.13.15",
"drizzle-orm": "^0.40.0",
"drizzle-kit": "^0.30.2", "drizzle-kit": "^0.30.2",
"@libsql/client": "^0.14.0" "@libsql/client": "^0.14.0"
} }

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="/manifest.json">
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">

View File

@@ -11,6 +11,8 @@
if (browser) { if (browser) {
hotkeys.filter = function(event) {return true} hotkeys.filter = function(event) {return true}
hotkeys('alt+q', function(event) {const target = document.getElementById("range_start"); if (target) {target.focus()}})
hotkeys('alt+w', function(event) {const target = document.getElementById("range_end"); if (target) {target.focus()}})
hotkeys('alt+n', function(event) {event.preventDefault(); functions.nextPage(); return false}); hotkeys('alt+n', function(event) {event.preventDefault(); functions.nextPage(); return false});
hotkeys('alt+b', function(event) {event.preventDefault(); functions.prevPage(); return false}); hotkeys('alt+b', function(event) {event.preventDefault(); functions.prevPage(); return false});
hotkeys('alt+j', function(event) {event.preventDefault(); functions.duplicateDown(); return false}); hotkeys('alt+j', function(event) {event.preventDefault(); functions.duplicateDown(); return false});
@@ -26,9 +28,9 @@
<div id="formheader" class="{prefix.color}" bind:offsetHeight={headerHeight}> <div id="formheader" class="{prefix.color}" bind:offsetHeight={headerHeight}>
<div class="flex-row-space tb-margin"> <div class="flex-row-space tb-margin">
<div class="flex-row"> <div class="flex-row">
<input type="number" bind:value={pagerForm.id_from}> <input type="number" id="range_start" onfocus={(e) => e.target.select()} bind:value={pagerForm.id_from}>
<div style="font-size: 22pt">-</div> <div style="font-size: 22pt">-</div>
<input type="number" bind:value={pagerForm.id_to}> <input type="number" id="range_end" onfocus={(e) => e.target.select()} bind:value={pagerForm.id_to}>
<button class="styled" onclick={() => { <button class="styled" onclick={() => {
if (Math.abs(pagerForm.id_to - pagerForm.id_from) > 800) { if (Math.abs(pagerForm.id_to - pagerForm.id_from) > 800) {
pagerForm.id_to = pagerForm.id_from + 799; pagerForm.id_to = pagerForm.id_from + 799;

View File

@@ -0,0 +1,163 @@
<script>
import { browser } from "$app/environment";
import hotkeys from "hotkeys-js";
let {
searchForm = $bindable(),
headerHeight = $bindable(),
functions,
} = $props();
if (browser) {
hotkeys.filter = function (event) {
return true;
};
hotkeys("alt+n", function (event) {
event.preventDefault();
functions.nextPage();
return false;
});
hotkeys("alt+b", function (event) {
event.preventDefault();
functions.prevPage();
return false;
});
hotkeys("alt+j", function (event) {
event.preventDefault();
functions.duplicateDown();
return false;
});
hotkeys("alt+u", function (event) {
event.preventDefault();
functions.duplicateUp();
return false;
});
hotkeys("alt+l", function (event) {
event.preventDefault();
functions.gotoNext();
return false;
});
hotkeys("alt+o", function (event) {
event.preventDefault();
functions.gotoPrev();
return false;
});
hotkeys("alt+c", function (event) {
event.preventDefault();
functions.copy();
return false;
});
hotkeys("alt+v", function (event) {
event.preventDefault();
functions.paste();
return false;
});
hotkeys("alt+s", function (event) {
event.preventDefault();
functions.saveAll();
return false;
});
}
</script>
<div id="formheader" bind:offsetHeight={headerHeight}>
<div class="flex-row-space tb-margin">
<div class="flex-row">
<div class="column">
<div>First Name</div>
<input
type="text"
style="width: 20ch"
bind:value={searchForm.first_name}
/>
</div>
<div class="column">
<div>Last Name</div>
<input
type="text"
style="width: 20ch"
bind:value={searchForm.last_name}
/>
</div>
<div class="column">
<div>Phone Number</div>
<input
type="text"
style="width: 20ch"
bind:value={searchForm.phone_number}
/>
</div>
<button
class="styled"
onclick={() => {
if (
searchForm.first_name ||
searchForm.last_name ||
searchForm.phone_number
) {
functions.refreshPage();
}
}}>Refresh</button
>
</div>
</div>
<div class="flex-row-space tb-margin">
<div class="flex-row">
<button
class="styled"
title="Alt + J"
tabindex="-1"
onclick={functions.duplicateDown}>Duplicate Down</button
>
<button
class="styled"
title="Alt + U"
tabindex="-1"
onclick={functions.duplicateUp}>Duplicate Up</button
>
<button
class="styled"
title="Alt + L"
tabindex="-1"
onclick={functions.gotoNext}>Next</button
>
<button
class="styled"
title="Alt + O"
tabindex="-1"
onclick={functions.gotoPrev}>Previous</button
>
<button
class="styled"
title="Alt + C"
tabindex="-1"
onclick={functions.copy}>Copy</button
>
<button
class="styled"
title="Alt + V"
tabindex="-1"
onclick={functions.paste}>Paste</button
>
</div>
<div class="flex-row">
<button
class="styled"
title="Alt + S"
tabindex="-1"
onclick={functions.saveAll}>Save All</button
>
</div>
</div>
</div>
<style>
#formheader {
position: sticky;
top: 0;
padding-bottom: 0.25rem;
border-bottom: solid 1px #000000;
background-color: #ffffff;
z-index: 100;
}
</style>

View File

@@ -1,93 +1,142 @@
<script> <script>
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import { env } from "$env/dynamic/public"; import { env } from "$env/dynamic/public";
import hotkeys from "hotkeys-js"; import { setContext } from "svelte";
import favicon from "$lib/assets/favicon.svg" import hotkeys from "hotkeys-js";
import favicon from "$lib/assets/favicon.svg";
let { data } = $props(); let { data } = $props();
const all_prefixes = [...data.prefixes]; const all_prefixes = $derived(data.prefixes);
let prefix_name = $state(""); let prefix_name = $state("");
let current_prefix = $state({name: "", color: "", weight: 0}); let current_prefix = $state({ name: "", color: "", weight: 0 });
let admin_mode = $state(false); let admin_mode = $state(false);
const venue = env.PUBLIC_TAM3_VENUE || "TAM3"; const venue = env.PUBLIC_TAM3_VENUE || "TAM3";
$effect(() => { $effect(() => {
const new_prefix = all_prefixes.find((prefix) => prefix.name === prefix_name); const new_prefix = all_prefixes.find(
if (new_prefix) { (prefix) => prefix.name === prefix_name,
current_prefix = {...new_prefix}; );
if (new_prefix) {
current_prefix = { ...new_prefix };
}
});
if (browser) {
hotkeys.filter = function (event) {
return true;
};
hotkeys("alt+a", function (event) {
event.preventDefault();
admin_mode = !admin_mode;
return false;
});
setTimeout(() => {
if (all_prefixes[0]) {
prefix_name = all_prefixes[0].name;
}
}, 100);
} }
})
if (browser) {
document.title = `${venue} - Main Menu`;
hotkeys.filter = function(event) {return true};
hotkeys('alt+a', function(event) {event.preventDefault(); admin_mode = !admin_mode; return false;});
setTimeout(() => {
if (all_prefixes[0]) {
prefix_name = all_prefixes[0].name;
}
}, 100);
};
</script> </script>
<svelte:head>
<title>{venue} - Main Menu</title>
</svelte:head>
<div class="main-menu"> <div class="main-menu">
<div class="flex-row"> <div class="flex-row">
<img src="{favicon}" alt="TAM3 Icon - Red ticket with TAM3 on it" class="icon"> <img
<h1>{venue} - Main Menu</h1> src={favicon}
</div> alt="TAM3 Icon - Red ticket with TAM3 on it"
{#if all_prefixes.length > 0} class="icon"
<div class="universal-reports flex-row tb-margin"> />
<a href="/counts" target="_blank" class="styled">Counts</a> <h1>{venue} - Main Menu</h1>
</div>
<div>
<h2>Current Prefix: {current_prefix.name}</h2>
</div>
<div class="change_title">
<h2>Change Prefix:</h2>
</div>
<div class="prefix-selector flex-row">
{#each all_prefixes as prefix}
<div class="{prefix.color} p025{prefix.name === prefix_name ? " active" : ""}">
<button class="styled" onclick={() => {
prefix_name = prefix.name;
}}>{prefix.name}</button>
</div> </div>
{/each} {#if all_prefixes.length > 0}
</div> <div class="universal-reports flex-row tb-margin">
<div><h2>Forms:</h2></div> <a href="/counts" target="_blank" class="styled">Counts</a>
<div class="flex-row {current_prefix.color}"> <a href="/sheets" target="_blank" class="styled">Print Sheets</a>
<a href="/tickets/{current_prefix.name}/" target="_blank" class="styled">Tickets</a> </div>
<a href="/baskets/{current_prefix.name}/" target="_blank" class="styled">Baskets</a> <div>
<a href="/drawing/{current_prefix.name}/" target="_blank" class="styled">Drawing</a> <h2>Current Prefix: {current_prefix.name}</h2>
</div> </div>
<div><h2>Reports:</h2></div> <div class="change_title">
<div class="flex-row {current_prefix.color}"> <h2>Change Prefix:</h2>
<a href="/reports/byname/{current_prefix.name}/" target="_blank" class="styled">By Name</a> </div>
<a href="/reports/bybasket/{current_prefix.name}/" target="_blank" class="styled">By Basket ID</a> <div class="prefix-selector flex-row">
</div> {#each all_prefixes as prefix}
{:else} <div
<p>There aren't any prefixes available, please create them.</p> class="{prefix.color} p025{prefix.name === prefix_name
{/if} ? ' active'
: ''}"
>
<button
class="styled"
onclick={() => {
prefix_name = prefix.name;
}}>{prefix.name}</button
>
</div>
{/each}
</div>
<div><h2>Forms:</h2></div>
<div class="flex-row {current_prefix.color}">
<a
href="/tickets/{current_prefix.name}/"
target="_blank"
class="styled">Tickets</a
>
<a
href="/baskets/{current_prefix.name}/"
target="_blank"
class="styled">Baskets</a
>
<a
href="/drawing/{current_prefix.name}/"
target="_blank"
class="styled">Drawing</a
>
</div>
<div><h2>Reports:</h2></div>
<div class="flex-row {current_prefix.color}">
<a
href="/reports/byname/{current_prefix.name}/"
target="_blank"
class="styled">By Name</a
>
<a
href="/reports/bybasket/{current_prefix.name}/"
target="_blank"
class="styled">By Basket ID</a
>
</div>
{:else}
<p>There aren't any prefixes available, please create them.</p>
{/if}
</div> </div>
{#if admin_mode} {#if admin_mode}
<div><h2>Admin Mode:</h2></div> <div><h2>Admin Mode:</h2></div>
<div class="flex-row"> <div class="flex-row">
<a href="/prefixes" target="_blank" class="styled">Prefix Editor</a> <a href="/prefixes" target="_blank" class="styled">Prefix Editor</a>
<a href="/backuprestore" target="_blank" class="styled">Backup/Restore</a> <a href="/search/tickets" target="_blank" class="styled"
<a href="/settings" target="_blank" class="styled">Settings</a> >Search Tickets</a
</div> >
<a href="/backuprestore" target="_blank" class="styled"
>Backup/Restore</a
>
<a href="/settings" target="_blank" class="styled">Settings</a>
</div>
{/if} {/if}
<div class="status tb-margin"> <div class="status tb-margin">
{data.status} {data.status}
</div> </div>
<div class="annotation"> <div class="annotation">
<p>Ticket Auction Manager 3 by Dilan Gilluly</p> <p>Ticket Auction Manager 3 by Dilan Gilluly</p>
</div> </div>
<style> <style>
img.icon { img.icon {
max-width: 150px; max-width: 150px;
} }
</style> </style>

View File

@@ -0,0 +1,49 @@
import { readSettings } from "$lib/server/settings";
import { db } from "$lib/server/db/index.js";
import { tickets } from "$lib/server/db/schema.js";
import { and, like, sql } from "drizzle-orm";
export async function GET({ url }) {
const env = readSettings();
const sFirstName = url.searchParams.get("first_name") || "",
sLastName = url.searchParams.get("last_name") || "",
sPhoneNumber = url.searchParams.get("phone_number") || "";
if (env.TAM3_REMOTE) {
const searchParams = new URLSearchParams({
api_key: env.TAM3_REMOTE_KEY,
first_name: sFirstName,
last_name: sLastName,
phone_number: sPhoneNumber,
});
const res = await fetch(
`${env.TAM3_REMOTE}/api/search/tickets/?${searchParams.toString()}`,
);
if (!res.ok) {
return new Response(JSON.stringify([]), {
status: res.status,
statusText: res.statusText,
});
}
const data = await res.json();
return new Response(JSON.stringify(data), {
status: 200,
statusText: "Fetched successfully",
headers: { "Content-Type": "application/json" },
});
} else {
const results = await db
.select()
.from(tickets)
.where(
and(
like(tickets.first_name, `%${sFirstName}%`),
like(tickets.last_name, `%${sLastName}%`),
like(tickets.phone_number, `%${sPhoneNumber}%`),
),
);
return new Response(JSON.stringify(results), {
headers: { "Content-Type": "application/json" },
});
}
}

View File

@@ -1,36 +1,41 @@
<script> <script>
import { browser } from '$app/environment'; import { browser } from "$app/environment";
import FormHeader from '$lib/components/FormHeader.svelte'; import FormHeader from "$lib/components/FormHeader.svelte";
import hotkeys from 'hotkeys-js'; import hotkeys from "hotkeys-js";
const { data } = $props(); const { data } = $props();
const prefix = {...data.prefix}; const prefix = $derived(data.prefix);
let pagerForm = $state({id_from: 0, id_to: 0}); let pagerForm = $state({ id_from: 0, id_to: 0 });
let current_idx = $state(0); let current_idx = $state(0);
let next_idx = $derived(current_idx+1); let next_idx = $derived(current_idx + 1);
let prev_idx = $derived(current_idx-1) let prev_idx = $derived(current_idx - 1);
let current_baskets = $state([]); let current_baskets = $state([]);
let copy_buffer = $state({prefix: prefix.name, b_id: 0, description: "", donors: "", winning_ticket: 0}); let copy_buffer = $state({});
let headerHeight = $state() let headerHeight = $state();
function changeFocus(idx) { function changeFocus(idx) {
const focusDe = document.getElementById(`${idx}_de`); const focusDe = document.getElementById(`${idx}_de`);
if (focusDe) { if (focusDe) {
focusDe.select(); focusDe.select();
focusDe.scrollIntoView({block: "center"}); focusDe.scrollIntoView({ block: "center" });
} }
} }
const functions = { const functions = {
refreshPage: async () => { refreshPage: async () => {
if (current_baskets.filter(basket => basket.changed === true).length > 0) { if (
functions.saveAll() current_baskets.filter((basket) => basket.changed === true)
.length > 0
) {
functions.saveAll();
} }
const res = await fetch(`/api/baskets/${prefix.name}/${pagerForm.id_from}/${pagerForm.id_to}`); const res = await fetch(
`/api/baskets/${prefix.name}/${pagerForm.id_from}/${pagerForm.id_to}`,
);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
current_baskets = [...data]; current_baskets = [...data];
setTimeout(() => changeFocus(0), 100) setTimeout(() => changeFocus(0), 100);
} }
}, },
prevPage: () => { prevPage: () => {
@@ -47,7 +52,12 @@
}, },
duplicateDown: () => { duplicateDown: () => {
if (current_baskets[next_idx]) { if (current_baskets[next_idx]) {
current_baskets[next_idx] = {...current_baskets[current_idx], b_id: current_baskets[next_idx].b_id, winning_ticket: current_baskets[next_idx].winning_ticket, changed: true}; current_baskets[next_idx] = {
...current_baskets[current_idx],
b_id: current_baskets[next_idx].b_id,
winning_ticket: current_baskets[next_idx].winning_ticket,
changed: true,
};
changeFocus(next_idx); changeFocus(next_idx);
} else { } else {
changeFocus(next_idx); changeFocus(next_idx);
@@ -55,7 +65,12 @@
}, },
duplicateUp: () => { duplicateUp: () => {
if (prev_idx >= 0) { if (prev_idx >= 0) {
current_baskets[prev_idx] = {...current_baskets[current_idx], b_id: current_baskets[prev_idx].b_id, winning_ticket: current_baskets[prev_idx].winning_ticket, changed: true}; current_baskets[prev_idx] = {
...current_baskets[current_idx],
b_id: current_baskets[prev_idx].b_id,
winning_ticket: current_baskets[prev_idx].winning_ticket,
changed: true,
};
changeFocus(prev_idx); changeFocus(prev_idx);
} else { } else {
changeFocus(prev_idx); changeFocus(prev_idx);
@@ -76,35 +91,56 @@
} }
}, },
copy: () => { copy: () => {
copy_buffer = {...current_baskets[current_idx]}; copy_buffer = { ...current_baskets[current_idx] };
}, },
paste: () => { paste: () => {
current_baskets[current_idx] = {...copy_buffer, b_id: current_baskets[current_idx].b_id, changed: true} if (Object.keys(copy_buffer).length !== 0) {
current_baskets[current_idx] = {
...copy_buffer,
b_id: current_baskets[current_idx].b_id,
changed: true,
};
}
changeFocus(current_idx);
}, },
saveAll: async () => { saveAll: async () => {
const to_save = current_baskets.filter((basket) => basket.changed === true); const to_save = current_baskets.filter(
const res = await fetch(`/api/baskets`, {body: JSON.stringify(to_save), method: 'POST', headers: {'Content-Type': 'application/json'}}); (basket) => basket.changed === true,
);
const res = await fetch(`/api/baskets`, {
body: JSON.stringify(to_save),
method: "POST",
headers: { "Content-Type": "application/json" },
});
if (res.ok) { if (res.ok) {
for (let basket of current_baskets) {basket.changed = false}; for (let basket of current_baskets) {
basket.changed = false;
}
changeFocus(0); changeFocus(0);
}; }
} },
} };
if (browser) { if (browser) {
document.title = `${prefix.name} Basket Entry` window.addEventListener("beforeunload", function (e) {
window.addEventListener("beforeunload", function(e) { if (
if (current_baskets.filter(basket => basket.changed === true).length > 0) { current_baskets.filter((basket) => basket.changed === true)
.length > 0
) {
e.preventDefault(); e.preventDefault();
} }
}) });
} }
</script> </script>
<svelte:head>
<title>{prefix.name} Basket Entry</title>
</svelte:head>
<h1>{prefix.name} Basket Entry</h1> <h1>{prefix.name} Basket Entry</h1>
<FormHeader {prefix} {functions} bind:pagerForm bind:headerHeight /> <FormHeader {prefix} {functions} bind:pagerForm bind:headerHeight />
<table> <table>
<thead style="top: {headerHeight+2}px"> <thead style="top: {headerHeight + 2}px">
<tr> <tr>
<th style="width: 12ch">Basket ID</th> <th style="width: 12ch">Basket ID</th>
<th>Description</th> <th>Description</th>
@@ -114,12 +150,32 @@
</thead> </thead>
<tbody> <tbody>
{#each current_baskets as basket, idx} {#each current_baskets as basket, idx}
<tr onfocusin={() => current_idx = idx}> <tr onfocusin={() => (current_idx = idx)}>
<td>{basket.b_id}</td> <td>{basket.b_id}</td>
<td><input type="text" id="{idx}_de" onchange={() => basket.changed = true} bind:value={basket.description}></td> <td
<td><input type="text" id="{idx}_do" onchange={() => basket.changed = true} bind:value={basket.donors}></td> ><input
<td><button tabindex="-1" onclick={() => basket.changed = !basket.changed}>{basket.changed ? "Y" : "N"}</button></td> type="text"
</tr> id="{idx}_de"
onchange={() => (basket.changed = true)}
bind:value={basket.description}
/></td
>
<td
><input
type="text"
id="{idx}_do"
onchange={() => (basket.changed = true)}
bind:value={basket.donors}
/></td
>
<td
><button
tabindex="-1"
onclick={() => (basket.changed = !basket.changed)}
>{basket.changed ? "Y" : "N"}</button
></td
>
</tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>
@@ -148,7 +204,8 @@
background: transparent; background: transparent;
border: solid 1px #000000; border: solid 1px #000000;
} }
input, button { input,
button {
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;

View File

@@ -1,19 +1,27 @@
<script> <script>
import { browser } from '$app/environment'; import { browser } from "$app/environment";
const { data } = $props(); const { data } = $props();
const counts = data.counts; const counts = $derived(data.counts);
const prefixes = data.prefixes; function copyPrefixes() {
return [...data.prefixes];
}
const prefixes = $state(copyPrefixes());
let colormap = {}; let colormap = {};
for (let prefix of prefixes) {colormap[prefix.name] = prefix.color} for (let prefix of prefixes) {
colormap[prefix.name] = prefix.color;
}
if (browser) { if (browser) {
document.title = "Counts of tickets entered";
setTimeout(() => window.location.reload(true), 60000); setTimeout(() => window.location.reload(true), 60000);
} }
</script> </script>
<svelte:head>
<title>Counts of tickets entered</title>
</svelte:head>
<h1>Counts of tickets entered</h1> <h1>Counts of tickets entered</h1>
<table> <table>
<thead> <thead>
@@ -25,11 +33,11 @@
</thead> </thead>
<tbody> <tbody>
{#each counts as count} {#each counts as count}
<tr class={colormap[count.prefix]}> <tr class={colormap[count.prefix]}>
<td>{count.prefix}</td> <td>{count.prefix}</td>
<td>{parseInt(count.total_sold).toLocaleString()}</td> <td>{parseInt(count.total_sold).toLocaleString()}</td>
<td>{parseInt(count.unique_sold).toLocaleString()}</td> <td>{parseInt(count.unique_sold).toLocaleString()}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>

View File

@@ -1,32 +1,37 @@
<script> <script>
import { browser } from '$app/environment'; import { browser } from "$app/environment";
import FormHeader from '$lib/components/FormHeader.svelte'; import FormHeader from "$lib/components/FormHeader.svelte";
import { focusElement } from '$lib/focusElement.js'; import { focusElement } from "$lib/focusElement.js";
const { data } = $props(); const { data } = $props();
const prefix = {...data.prefix}; const prefix = $derived(data.prefix);
let pagerForm = $state({id_from: 0, id_to: 0}); let pagerForm = $state({ id_from: 0, id_to: 0 });
let current_idx = $state(0); let current_idx = $state(0);
let next_idx = $derived(current_idx+1); let next_idx = $derived(current_idx + 1);
let prev_idx = $derived(current_idx-1); let prev_idx = $derived(current_idx - 1);
let current_drawings = $state([]); let current_drawings = $state([]);
let copy_buffer = $state({prefix: prefix.name, b_id: 1, winning_ticket: 0, winner: ", "}); let copy_buffer = $state({});
let headerHeight = $state() let headerHeight = $state();
function changeFocus(idx) { function changeFocus(idx) {
const focusWt = document.getElementById(`${idx}_wt`); const focusWt = document.getElementById(`${idx}_wt`);
if (focusWt) { if (focusWt) {
focusWt.select(); focusWt.select();
focusWt.scrollIntoView({block: "center"}); focusWt.scrollIntoView({ block: "center" });
} }
} }
const functions = { const functions = {
refreshPage: async () => { refreshPage: async () => {
if (current_drawings.filter(drawing => drawing.changed === true).length > 0) { if (
functions.saveAll() current_drawings.filter((drawing) => drawing.changed === true)
.length > 0
) {
functions.saveAll();
} }
const res = await fetch(`/api/combined/${prefix.name}/${pagerForm.id_from}/${pagerForm.id_to}`); const res = await fetch(
`/api/combined/${prefix.name}/${pagerForm.id_from}/${pagerForm.id_to}`,
);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
current_drawings = [...data]; current_drawings = [...data];
@@ -47,7 +52,11 @@
}, },
duplicateDown: () => { duplicateDown: () => {
if (current_drawings[next_idx]) { if (current_drawings[next_idx]) {
current_drawings[next_idx] = {...current_drawings[current_idx], b_id: current_drawings[next_idx].b_id, changed: true}; current_drawings[next_idx] = {
...current_drawings[current_idx],
b_id: current_drawings[next_idx].b_id,
changed: true,
};
changeFocus(next_idx); changeFocus(next_idx);
} else { } else {
changeFocus(current_idx); changeFocus(current_idx);
@@ -55,7 +64,11 @@
}, },
duplicateUp: () => { duplicateUp: () => {
if (prev_idx >= 0) { if (prev_idx >= 0) {
current_drawings[prev_idx] = {...current_drawing[current_idx], b_id: current_drawings[prev_idx].b_id, changed: true}; current_drawings[prev_idx] = {
...current_drawing[current_idx],
b_id: current_drawings[prev_idx].b_id,
changed: true,
};
changeFocus(prev_idx); changeFocus(prev_idx);
} else { } else {
changeFocus(current_idx); changeFocus(current_idx);
@@ -76,35 +89,56 @@
} }
}, },
copy: () => { copy: () => {
copy_buffer = {...current_drawings[current_idx]}; copy_buffer = { ...current_drawings[current_idx] };
}, },
paste: () => { paste: () => {
current_drawings[current_idx] = {...copy_buffer, b_id: current_drawings[current_idx], changed: true}; if (Object.keys(copy_buffer).length !== 0) {
current_drawings[current_idx] = {
...copy_buffer,
b_id: current_drawings[current_idx],
changed: true,
};
}
changeFocus(current_idx);
}, },
saveAll: async () => { saveAll: async () => {
const to_save = current_drawings.filter((drawing) => drawing.changed === true); const to_save = current_drawings.filter(
const res = await fetch("/api/combined", {body: JSON.stringify(to_save), method: 'POST', headers: {'Content-Type': 'application/json'}}); (drawing) => drawing.changed === true,
);
const res = await fetch("/api/combined", {
body: JSON.stringify(to_save),
method: "POST",
headers: { "Content-Type": "application/json" },
});
if (res.ok) { if (res.ok) {
for (let drawing of current_drawings) {drawing.changed = false}; for (let drawing of current_drawings) {
drawing.changed = false;
}
changeFocus(0); changeFocus(0);
} }
} },
} };
if (browser) { if (browser) {
document.title = `${prefix.name} Drawing Form` window.addEventListener("beforeunload", function (e) {
window.addEventListener("beforeunload", function(e) { if (
if (current_drawings.filter(drawing => drawing.changed === true).length > 0) { current_drawings.filter((drawing) => drawing.changed === true)
.length > 0
) {
e.preventDefault(); e.preventDefault();
} }
}); });
} }
</script> </script>
<svelte:head>
<title>{prefix.name} Drawing Form</title>
</svelte:head>
<h1>{prefix.name} Drawing Form</h1> <h1>{prefix.name} Drawing Form</h1>
<FormHeader {prefix} {functions} bind:pagerForm bind:headerHeight /> <FormHeader {prefix} {functions} bind:pagerForm bind:headerHeight />
<table> <table>
<thead style="top: {headerHeight+2}px"> <thead style="top: {headerHeight + 2}px">
<tr> <tr>
<th style="width: 12ch">Basket ID</th> <th style="width: 12ch">Basket ID</th>
<th style="width: 20ch">Winning Number</th> <th style="width: 20ch">Winning Number</th>
@@ -114,19 +148,32 @@
</thead> </thead>
<tbody> <tbody>
{#each current_drawings as drawing, idx} {#each current_drawings as drawing, idx}
<tr onfocusin={() => current_idx = idx}> <tr onfocusin={() => (current_idx = idx)}>
<td>{drawing.b_id}</td> <td>{drawing.b_id}</td>
<td><input type="number" id="{idx}_wt" bind:value={drawing.winning_ticket} onfocus={focusElement} onchange={async () => { <td
drawing.changed = true; ><input
const res = await fetch(`/api/tickets/${prefix.name}/${drawing.winning_ticket}`); type="number"
if (res.ok) { id="{idx}_wt"
const t_data = await res.json() bind:value={drawing.winning_ticket}
drawing.winner = `${t_data.last_name}, ${t_data.first_name}` onfocus={focusElement}
} onchange={async () => {
}}></td> drawing.changed = true;
<td>{drawing.winner}</td> const res = await fetch(
<td><button tabindex="-1">{drawing.changed ? "Y" : "N"}</button></td> `/api/tickets/${prefix.name}/${drawing.winning_ticket}`,
</tr> );
if (res.ok) {
const t_data = await res.json();
drawing.winner = `${t_data.last_name}, ${t_data.first_name}`;
}
}}
/></td
>
<td>{drawing.winner}</td>
<td
><button tabindex="-1">{drawing.changed ? "Y" : "N"}</button
></td
>
</tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>
@@ -155,7 +202,8 @@
background: transparent; background: transparent;
border: solid 1px #000000; border: solid 1px #000000;
} }
input, button { input,
button {
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;

View File

@@ -1,33 +1,53 @@
<script> <script>
import { env } from '$env/dynamic/public'; import { env } from "$env/dynamic/public";
import { browser } from '$app/environment'; import { browser } from "$app/environment";
const { data } = $props(); const { data } = $props();
const prefix = {...data.prefix}; const prefix = $derived(data.prefix);
const report_data = data.report; const report_data = $derived(data.report);
let show_data = $state([...report_data]); function copyReportData() {
let report_subject = $state("All Preferences"); return [...report_data];
if (browser) {
document.title = `${prefix.name} Report By Basket ID`
} }
let show_data = $state(copyReportData());
let report_subject = $state("All Preferences");
</script> </script>
<svelte:head>
<title>{prefix.name} Report By Basket ID</title>
</svelte:head>
<div id="reportheader"> <div id="reportheader">
<div class="flex-row-space {prefix.color}"> <div class="flex-row-space {prefix.color}">
<div class="flex-row"> <div class="flex-row">
<button class="styled" onclick={() => { <button
show_data = [...report_data]; class="styled"
report_subject = "All Preferences"; onclick={() => {
}}>All Preferences</button> show_data = copyReportData();
<button class="styled" onclick={() => { report_subject = "All Preferences";
show_data = [...report_data.filter((entry) => entry.preference === "CALL")]; }}>All Preferences</button
report_subject = "CALL Preference" >
}}>Call</button> <button
<button class="styled" onclick={() => { class="styled"
show_data = [...report_data.filter((entry) => entry.preference === "TEXT")]; onclick={() => {
report_subject = "TEXT Preference"; show_data = [
}}>Text</button> ...report_data.filter(
(entry) => entry.preference === "CALL",
),
];
report_subject = "CALL Preference";
}}>Call</button
>
<button
class="styled"
onclick={() => {
show_data = [
...report_data.filter(
(entry) => entry.preference === "TEXT",
),
];
report_subject = "TEXT Preference";
}}>Text</button
>
</div> </div>
<div class="flex-row"> <div class="flex-row">
<button class="styled" onclick={() => window.print()}>Print</button> <button class="styled" onclick={() => window.print()}>Print</button>
@@ -35,35 +55,37 @@
</div> </div>
</div> </div>
<table> <table>
<thead> <thead>
<tr> <tr>
<th colspan="90"><h1>{prefix.name} - Report - {report_subject}</h1></th> <th colspan="90"
</tr> ><h1>{prefix.name} - Report - {report_subject}</h1></th
<tr> >
<th>Basket ID</th> </tr>
<th>Description</th> <tr>
<th>Ticket #</th> <th>Basket ID</th>
<th>Winner Name</th> <th>Description</th>
<th>Phone Number</th> <th>Ticket #</th>
</tr> <th>Winner Name</th>
</thead> <th>Phone Number</th>
<tbody> </tr>
{#each show_data as report_entry} </thead>
<tr> <tbody>
<td>{report_entry.b_id}</td> {#each show_data as report_entry}
<td>{report_entry.description}</td> <tr>
<td>{report_entry.winning_ticket}</td> <td>{report_entry.b_id}</td>
<td>{report_entry.winner_name}</td> <td>{report_entry.description}</td>
<td>{report_entry.phone_number}</td> <td>{report_entry.winning_ticket}</td>
</tr> <td>{report_entry.winner_name}</td>
{/each} <td>{report_entry.phone_number}</td>
</tbody> </tr>
<tfoot> {/each}
<tr> </tbody>
<td colspan="3">{env.PUBLIC_TAM3_VENUE || ""}</td> <tfoot>
<td colspan="2" style="text-align: right">TAM3 by Dilan Gilluly</td> <tr>
</tr> <td colspan="3">{env.PUBLIC_TAM3_VENUE || ""}</td>
</tfoot> <td colspan="2" style="text-align: right">TAM3 by Dilan Gilluly</td>
</tr>
</tfoot>
</table> </table>
<style> <style>
@@ -76,7 +98,6 @@
border: solid 1px black; border: solid 1px black;
padding: 0.2rem; padding: 0.2rem;
} }
} }
table tbody tr:nth-child(2n) { table tbody tr:nth-child(2n) {
background-color: #dddddd; background-color: #dddddd;

View File

@@ -1,33 +1,53 @@
<script> <script>
import { env } from '$env/dynamic/public'; import { env } from "$env/dynamic/public";
import { browser } from '$app/environment'; import { browser } from "$app/environment";
const { data } = $props(); const { data } = $props();
const prefix = {...data.prefix}; const prefix = $derived(data.prefix);
const report_data = data.report; const report_data = $derived(data.report);
let show_data = $state([...report_data]); function copyReportData() {
let report_subject = $state("All Preferences"); return [...report_data];
if (browser) {
document.title = `${prefix.name} Report By Name`
} }
let show_data = $state(copyReportData());
let report_subject = $state("All Preferences");
</script> </script>
<svelte:head>
<title>{prefix.name} Report By Name</title>
</svelte:head>
<div id="reportheader"> <div id="reportheader">
<div class="flex-row-space {prefix.color}"> <div class="flex-row-space {prefix.color}">
<div class="flex-row"> <div class="flex-row">
<button class="styled" onclick={() => { <button
show_data = [...report_data]; class="styled"
report_subject = "All Preferences"; onclick={() => {
}}>All Preferences</button> show_data = copyReportData();
<button class="styled" onclick={() => { report_subject = "All Preferences";
show_data = [...report_data.filter((entry) => entry.preference === "CALL")]; }}>All Preferences</button
report_subject = "CALL Preference" >
}}>Call</button> <button
<button class="styled" onclick={() => { class="styled"
show_data = [...report_data.filter((entry) => entry.preference === "TEXT")]; onclick={() => {
report_subject = "TEXT Preference"; show_data = [
}}>Text</button> ...report_data.filter(
(entry) => entry.preference === "CALL",
),
];
report_subject = "CALL Preference";
}}>Call</button
>
<button
class="styled"
onclick={() => {
show_data = [
...report_data.filter(
(entry) => entry.preference === "TEXT",
),
];
report_subject = "TEXT Preference";
}}>Text</button
>
</div> </div>
<div class="flex-row"> <div class="flex-row">
<button class="styled" onclick={() => window.print()}>Print</button> <button class="styled" onclick={() => window.print()}>Print</button>
@@ -35,35 +55,37 @@
</div> </div>
</div> </div>
<table> <table>
<thead> <thead>
<tr> <tr>
<th colspan="90"><h1>{prefix.name} - Report - {report_subject}</h1></th> <th colspan="90"
</tr> ><h1>{prefix.name} - Report - {report_subject}</h1></th
<tr> >
<th>Winner Name</th> </tr>
<th>Phone Number</th> <tr>
<th>Basket ID</th> <th>Winner Name</th>
<th>Ticket #</th> <th>Phone Number</th>
<th>Description</th> <th>Basket ID</th>
</tr> <th>Ticket #</th>
</thead> <th>Description</th>
<tbody> </tr>
{#each show_data as report_entry} </thead>
<tr> <tbody>
<td>{report_entry.winner_name}</td> {#each show_data as report_entry}
<td>{report_entry.phone_number}</td> <tr>
<td>{report_entry.b_id}</td> <td>{report_entry.winner_name}</td>
<td>{report_entry.winning_ticket}</td> <td>{report_entry.phone_number}</td>
<td>{report_entry.description}</td> <td>{report_entry.b_id}</td>
</tr> <td>{report_entry.winning_ticket}</td>
{/each} <td>{report_entry.description}</td>
</tbody> </tr>
<tfoot> {/each}
<tr> </tbody>
<td colspan="3">{env.PUBLIC_TAM3_VENUE || ""}</td> <tfoot>
<td colspan="2" style="text-align: right">TAM3 by Dilan Gilluly</td> <tr>
</tr> <td colspan="3">{env.PUBLIC_TAM3_VENUE || ""}</td>
</tfoot> <td colspan="2" style="text-align: right">TAM3 by Dilan Gilluly</td>
</tr>
</tfoot>
</table> </table>
<style> <style>
@@ -76,7 +98,6 @@
border: solid 1px black; border: solid 1px black;
padding: 0.2rem; padding: 0.2rem;
} }
} }
table tbody tr:nth-child(2n) { table tbody tr:nth-child(2n) {
background-color: #dddddd; background-color: #dddddd;

View File

@@ -0,0 +1,248 @@
<script>
import { browser } from "$app/environment";
import SearchHeader from "$lib/components/SearchHeader.svelte";
import { focusElement } from "$lib/focusElement.js";
let searchForm = $state({
first_name: "",
last_name: "",
phone_number: "",
});
let current_idx = $state(0);
let next_idx = $derived(current_idx + 1);
let prev_idx = $derived(current_idx - 1);
let current_tickets = $state([]);
let copy_buffer = $state({
prefix: "",
t_id: 0,
first_name: "",
last_name: "",
phone_number: "",
preference: "CALL",
changed: true,
});
let headerHeight = $state();
function changeFocus(idx) {
const focusFn = document.getElementById(`${idx}_fn`);
if (focusFn) {
focusFn.select();
}
}
const functions = {
refreshPage: async () => {
if (
current_tickets.filter((ticket) => ticket.changed === true)
.length > 0
) {
functions.saveAll();
}
const searchParams = new URLSearchParams({ ...searchForm });
const res = await fetch(
`/api/search/tickets?${searchParams.toString()}`,
);
if (res.ok) {
const data = await res.json();
current_tickets = [...data];
setTimeout(() => changeFocus(0), 100);
}
},
prevPage: () => {
const diff = current_tickets.length;
pagerForm.id_from = pagerForm.id_from - diff;
pagerForm.id_to = pagerForm.id_to - diff;
functions.refreshPage();
},
nextPage: () => {
const diff = current_tickets.length;
pagerForm.id_from = pagerForm.id_from + diff;
pagerForm.id_to = pagerForm.id_to + diff;
functions.refreshPage();
},
duplicateDown: () => {
if (current_tickets[next_idx]) {
current_tickets[next_idx] = {
...current_tickets[current_idx],
prefix: current_tickets[next_idx].prefix,
t_id: current_tickets[next_idx].t_id,
changed: true,
};
changeFocus(next_idx);
} else {
changeFocus(current_idx);
}
},
duplicateUp: () => {
if (prev_idx >= 0) {
current_tickets[prev_idx] = {
...current_tickets[current_idx],
prefix: current_tickets[prev_idx].prefix,
t_id: current_tickets[prev_idx].t_id,
changed: true,
};
changeFocus(prev_idx);
} else {
changeFocus(current_idx);
}
},
gotoNext: () => {
if (current_tickets[next_idx]) {
changeFocus(next_idx);
} else {
changeFocus(current_idx);
}
},
gotoPrev: () => {
if (prev_idx >= 0) {
changeFocus(prev_idx);
} else {
changeFocus(current_idx);
}
},
copy: () => {
copy_buffer = { ...current_tickets[current_idx] };
changeFocus(current_idx);
},
paste: () => {
current_tickets[current_idx] = {
...copy_buffer,
prefix: current_tickets[current_idx].prefix,
t_id: current_tickets[current_idx].t_id,
changed: true,
};
changeFocus(current_idx);
},
saveAll: async () => {
const to_save = current_tickets.filter(
(ticket) => ticket.changed === true,
);
const res = await fetch(`/api/tickets`, {
body: JSON.stringify(to_save),
method: "POST",
headers: { "Content-Type": "application/json" },
});
if (res.ok) {
for (let ticket of current_tickets) {
ticket.changed = false;
}
changeFocus(0);
}
},
};
if (browser) {
document.title = `Ticket Search`;
window.addEventListener("beforeunload", function (e) {
if (
current_tickets.filter((ticket) => ticket.changed === true)
.length > 0
) {
e.preventDefault();
}
});
}
</script>
<h1>Ticket Search</h1>
<SearchHeader {functions} bind:searchForm bind:headerHeight />
<table>
<thead style="top: {headerHeight + 2}px">
<tr>
<th>Prefix</th>
<th style="width: 12ch">Ticket ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Phone Number</th>
<th>Preference</th>
<th>Changed</th>
</tr>
</thead>
<tbody>
{#each current_tickets as ticket, idx}
<tr onfocusin={() => (current_idx = idx)}>
<td>{ticket.prefix}</td>
<td>{ticket.t_id}</td>
<td
><input
id="{idx}_fn"
type="text"
bind:value={ticket.first_name}
onfocus={focusElement}
onchange={() => (ticket.changed = true)}
/></td
>
<td
><input
id="{idx}_ln"
type="text"
bind:value={ticket.last_name}
onfocus={focusElement}
onchange={() => (ticket.changed = true)}
/></td
>
<td
><input
id="{idx}_pn"
type="text"
bind:value={ticket.phone_number}
onfocus={focusElement}
onchange={() => (ticket.changed = true)}
/></td
>
<td
><select
id="{idx}_pr"
style="width: 100%"
bind:value={ticket.preference}
onfocus={focusElement}
onchange={() => (ticket.changed = true)}
>
<option value="CALL">Call</option>
<option value="TEXT">Text</option>
</select></td
>
<td
><button
tabindex="-1"
onclick={() => (ticket.changed = !ticket.changed)}
>{ticket.changed ? "Y" : "N"}</button
></td
>
</tr>
{/each}
</tbody>
</table>
<style>
table {
width: 100%;
thead {
background-color: #ffffff;
position: sticky;
z-index: 100;
}
th {
text-align: left;
border: solid 1px #000000;
}
tbody tr:nth-child(2n) {
background-color: #eeeeee;
}
tbody tr:focus-within td:first-child {
font-weight: bold;
border-top: solid 1px;
border-bottom: solid 1px;
}
input {
background: transparent;
border: solid 1px #000000;
}
input,
button {
display: block;
box-sizing: border-box;
width: 100%;
}
}
</style>

View File

@@ -1,27 +1,39 @@
<script> <script>
import { browser } from '$app/environment'; import { browser } from "$app/environment";
const { data } = $props(); const { data } = $props();
let stagingSettings = $state({...data.settings}); function copySettings() {
let status = $state("") return { ...data.settings };
}
let stagingSettings = $state(copySettings());
let status = $state("");
async function saveChanges() { async function saveChanges() {
const res = await fetch('/api/settings', {method: 'POST', body: JSON.stringify(stagingSettings), headers: {'Content-Type': 'application/json'}}); const res = await fetch("/api/settings", {
method: "POST",
body: JSON.stringify(stagingSettings),
headers: { "Content-Type": "application/json" },
});
if (res.ok) { if (res.ok) {
status = "Changes saved successfully"; status = "Changes saved successfully";
setTimeout(() => {status = ""}, 5000); setTimeout(() => {
status = "";
}, 5000);
} else { } else {
status = "Error saving changes, check config file path, make sure folder exists"; status =
setTimeout(() => {status = ""}, 5000); "Error saving changes, check config file path, make sure folder exists";
setTimeout(() => {
status = "";
}, 5000);
} }
} }
function cancelChanges() { function cancelChanges() {
stagingSettings = {...data.settings}; stagingSettings = { ...data.settings };
} }
if (browser) { if (browser) {
document.title = "TAM3 - Settings" document.title = "TAM3 - Settings";
} }
</script> </script>
@@ -31,11 +43,16 @@
<h2>Remote Server</h2> <h2>Remote Server</h2>
<div><strong>Address:</strong></div> <div><strong>Address:</strong></div>
<div><em>For example: https://ip_or_hostname:8443</em></div> <div><em>For example: https://ip_or_hostname:8443</em></div>
<div><input type="text" bind:value={stagingSettings.TAM3_REMOTE}></div> <div><input type="text" bind:value={stagingSettings.TAM3_REMOTE} /></div>
<div><strong>API Key:</strong></div> <div><strong>API Key:</strong></div>
<div class="flex-row"> <div class="flex-row">
<input type="password" bind:value={stagingSettings.TAM3_REMOTE_KEY}> <input type="password" bind:value={stagingSettings.TAM3_REMOTE_KEY} />
<button class="styled" onclick={() => {stagingSettings.TAM3_REMOTE_KEY = ""}}>Clear</button> <button
class="styled"
onclick={() => {
stagingSettings.TAM3_REMOTE_KEY = "";
}}>Clear</button
>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,131 @@
<script>
let formData = $state({ startNumber: 0, endNumber: 0, perPage: 20 });
let currentRows = $state([]);
function selectOnFocus(e) {
e.target.select();
}
function loadRows() {
currentRows = ["Loading"]
setTimeout(() => {
currentRows = [];
for (let i = formData.startNumber; i <= formData.endNumber; i++) {
currentRows = [...currentRows, i];
}
}, 1)
}
</script>
<svelte:head>
<title>Print Ticket Sheets</title>
</svelte:head>
<div id="header">
<h1>Print Ticket Sheets</h1>
<div class="flex-row-space">
<div class="flex-row">
<div>
<div>Start Number</div>
<input
type="number"
onfocus={selectOnFocus}
bind:value={formData.startNumber}
/>
</div>
<div>
<div>Ending Number</div>
<input
type="number"
onfocus={selectOnFocus}
bind:value={formData.endNumber}
/>
</div>
<div>
<div>Per Page</div>
<input
type="number"
onfocus={selectOnFocus}
bind:value={formData.perPage}
/>
</div>
<div>
<button class="styled" onclick={loadRows}>Load</button>
</div>
</div>
<div class="flex-row">
<button class="styled" onclick={() => window.print()}>Print</button>
</div>
</div>
</div>
<table id="main_table">
<colgroup>
<col style="width: 15%" />
<col style="width: 40%" />
<col style="width: 35%" />
<col style="width: 10%" />
</colgroup>
<thead>
<tr>
<th>Ticket #</th>
<th>Name</th>
<th>Phone Number</th>
<th>Text?</th>
</tr>
</thead>
<tbody style="height: 100%">
{#each currentRows as row, idx}
{#if idx !== 0 && idx % formData.perPage === 0}
<tr class="pagebreak"></tr>
{/if}
<tr>
<td><strong>{row}</strong></td>
<td></td>
<td></td>
<td></td>
</tr>
{/each}
</tbody>
</table>
<style>
#main_table {
width: 100%;
height: 100%;
text-align: left;
table-layout: fixed;
th,
td {
border: solid black 1px;
white-space: nowrap;
overflow: hidden;
}
}
@media print {
#header {
display: none;
}
#main_table {
table-layout: fixed;
}
#main_table tbody {
font-size: 16pt;
}
#main_table tbody tr {
height: 32pt;
}
#main_table tbody tr.pagebreak {
break-after: page;
}
@page:after {
content: "My Text";
}
}
</style>

View File

@@ -1,16 +1,16 @@
<script> <script>
import { browser } from '$app/environment'; import { browser } from "$app/environment";
import FormHeader from '$lib/components/FormHeader.svelte'; import FormHeader from "$lib/components/FormHeader.svelte";
import { focusElement } from '$lib/focusElement.js'; import { focusElement } from "$lib/focusElement.js";
const { data } = $props(); const { data } = $props();
let prefix = {...data.prefix}; const prefix = $derived(data.prefix);
let pagerForm = $state({id_from: 0, id_to: 0}); let pagerForm = $state({ id_from: 0, id_to: 0 });
let current_idx = $state(0); let current_idx = $state(0);
let next_idx = $derived(current_idx+1); let next_idx = $derived(current_idx + 1);
let prev_idx = $derived(current_idx-1); let prev_idx = $derived(current_idx - 1);
let current_tickets = $state([]); let current_tickets = $state([]);
let copy_buffer = $state({prefix: prefix.name, t_id: 0, first_name: "", last_name: "", phone_number: "", preference: "CALL", changed: true}); let copy_buffer = $state({});
let headerHeight = $state(); let headerHeight = $state();
function changeFocus(idx) { function changeFocus(idx) {
@@ -22,42 +22,55 @@
const functions = { const functions = {
refreshPage: async () => { refreshPage: async () => {
if (current_tickets.filter(ticket => ticket.changed === true).length > 0) { if (
current_tickets.filter((ticket) => ticket.changed === true)
.length > 0
) {
functions.saveAll(); functions.saveAll();
}; }
const res = await fetch(`/api/tickets/${prefix.name}/${pagerForm.id_from}/${pagerForm.id_to}`); const res = await fetch(
`/api/tickets/${prefix.name}/${pagerForm.id_from}/${pagerForm.id_to}`,
);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
current_tickets = [...data]; current_tickets = [...data];
setTimeout(() => changeFocus(0), 100); setTimeout(() => changeFocus(0), 100);
}; }
}, },
prevPage: () => { prevPage: () => {
const diff = current_tickets.length; const diff = current_tickets.length;
pagerForm.id_from = pagerForm.id_from - diff; pagerForm.id_from = pagerForm.id_from - diff;
pagerForm.id_to = pagerForm.id_to - diff; pagerForm.id_to = pagerForm.id_to - diff;
functions.refreshPage() functions.refreshPage();
}, },
nextPage: () => { nextPage: () => {
const diff = current_tickets.length; const diff = current_tickets.length;
pagerForm.id_from = pagerForm.id_from + diff; pagerForm.id_from = pagerForm.id_from + diff;
pagerForm.id_to = pagerForm.id_to + diff; pagerForm.id_to = pagerForm.id_to + diff;
functions.refreshPage() functions.refreshPage();
}, },
duplicateDown: () => { duplicateDown: () => {
if (current_tickets[next_idx]) { if (current_tickets[next_idx]) {
current_tickets[next_idx] = {...current_tickets[current_idx], t_id: current_tickets[next_idx].t_id, changed: true}; current_tickets[next_idx] = {
...current_tickets[current_idx],
t_id: current_tickets[next_idx].t_id,
changed: true,
};
changeFocus(next_idx); changeFocus(next_idx);
} else { } else {
changeFocus(current_idx) changeFocus(current_idx);
} }
}, },
duplicateUp: () => { duplicateUp: () => {
if (prev_idx >= 0) { if (prev_idx >= 0) {
current_tickets[prev_idx] = {...current_tickets[current_idx], t_id: current_tickets[prev_idx].t_id, changed: true}; current_tickets[prev_idx] = {
...current_tickets[current_idx],
t_id: current_tickets[prev_idx].t_id,
changed: true,
};
changeFocus(prev_idx); changeFocus(prev_idx);
} else { } else {
changeFocus(current_idx) changeFocus(current_idx);
} }
}, },
gotoNext: () => { gotoNext: () => {
@@ -75,37 +88,58 @@
} }
}, },
copy: () => { copy: () => {
copy_buffer = {...current_tickets[current_idx]}; copy_buffer = { ...current_tickets[current_idx] };
changeFocus(current_idx); changeFocus(current_idx);
disablePaste = false;
}, },
paste: () => { paste: () => {
current_tickets[current_idx] = {...copy_buffer, t_id: current_tickets[current_idx].t_id, changed: true}; if (Object.keys(copy_buffer).length !== 0) {
current_tickets[current_idx] = {
...copy_buffer,
t_id: current_tickets[current_idx].t_id,
changed: true,
};
}
changeFocus(current_idx); changeFocus(current_idx);
}, },
saveAll: async () => { saveAll: async () => {
const to_save = current_tickets.filter((ticket) => ticket.changed === true); const to_save = current_tickets.filter(
const res = await fetch(`/api/tickets`, {body: JSON.stringify(to_save), method: 'POST', headers: {'Content-Type': 'application/json'}}); (ticket) => ticket.changed === true,
);
const res = await fetch(`/api/tickets`, {
body: JSON.stringify(to_save),
method: "POST",
headers: { "Content-Type": "application/json" },
});
if (res.ok) { if (res.ok) {
for (let ticket of current_tickets) {ticket.changed = false}; for (let ticket of current_tickets) {
ticket.changed = false;
}
changeFocus(0); changeFocus(0);
} }
} },
}; };
if (browser) { if (browser) {
document.title = `${prefix.name} Ticket Entry`; window.addEventListener("beforeunload", function (e) {
window.addEventListener("beforeunload", function(e) { if (
if (current_tickets.filter(ticket => ticket.changed === true).length > 0) { current_tickets.filter((ticket) => ticket.changed === true)
.length > 0
) {
e.preventDefault(); e.preventDefault();
} }
}); });
} }
</script> </script>
<svelte:head>
<title>{prefix.name} Ticket Entry</title>
</svelte:head>
<h1>{prefix.name} Ticket Entry</h1> <h1>{prefix.name} Ticket Entry</h1>
<FormHeader {prefix} {functions} bind:pagerForm bind:headerHeight /> <FormHeader {prefix} {functions} bind:pagerForm bind:headerHeight />
<table> <table>
<thead style="top: {headerHeight+2}px"> <thead style="top: {headerHeight + 2}px">
<tr> <tr>
<th style="width: 12ch">Ticket ID</th> <th style="width: 12ch">Ticket ID</th>
<th>First Name</th> <th>First Name</th>
@@ -117,17 +151,55 @@
</thead> </thead>
<tbody> <tbody>
{#each current_tickets as ticket, idx} {#each current_tickets as ticket, idx}
<tr onfocusin={() => current_idx = idx}> <tr onfocusin={() => (current_idx = idx)}>
<td>{ticket.t_id}</td> <td>{ticket.t_id}</td>
<td><input id="{idx}_fn" type="text" bind:value={ticket.first_name} onfocus={focusElement} onchange={() => ticket.changed = true}></td> <td
<td><input id="{idx}_ln" type="text" bind:value={ticket.last_name} onfocus={focusElement} onchange={() => ticket.changed = true}></td> ><input
<td><input id="{idx}_pn" type="text" bind:value={ticket.phone_number} onfocus={focusElement} onchange={() => ticket.changed = true}></td> id="{idx}_fn"
<td><select id="{idx}_pr" style="width: 100%" bind:value={ticket.preference} onfocus={focusElement} onchange={() => ticket.changed = true}> type="text"
<option value="CALL">Call</option> bind:value={ticket.first_name}
<option value="TEXT">Text</option> onfocus={focusElement}
</select></td> onchange={() => (ticket.changed = true)}
<td><button tabindex="-1" onclick={() => ticket.changed = !ticket.changed}>{ticket.changed ? "Y" : "N"}</button></td> /></td
</tr> >
<td
><input
id="{idx}_ln"
type="text"
bind:value={ticket.last_name}
onfocus={focusElement}
onchange={() => (ticket.changed = true)}
/></td
>
<td
><input
id="{idx}_pn"
type="text"
bind:value={ticket.phone_number}
onfocus={focusElement}
onchange={() => (ticket.changed = true)}
/></td
>
<td
><select
id="{idx}_pr"
style="width: 100%"
bind:value={ticket.preference}
onfocus={focusElement}
onchange={() => (ticket.changed = true)}
>
<option value="CALL">Call</option>
<option value="TEXT">Text</option>
</select></td
>
<td
><button
tabindex="-1"
onclick={() => (ticket.changed = !ticket.changed)}
>{ticket.changed ? "Y" : "N"}</button
></td
>
</tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>
@@ -156,7 +228,8 @@
background: transparent; background: transparent;
border: solid 1px #000000; border: solid 1px #000000;
} }
input, button { input,
button {
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;