Compare commits

...

10 Commits

39 changed files with 1690 additions and 713 deletions

View File

@@ -1,3 +1,4 @@
__pycache__/
*/__pycache__/
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 sys import argv
from exceptions import bad_key
from repos.api_keys import ApiKeyRepo
@@ -13,6 +12,7 @@ from routers.combined import combined_router
from routers.reports import report_router
from routers.backuprestore import backup_router
from routers.counts import counts_router
from routers.search import search_router
if argv[1] == "run":
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(backup_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
tam3_version="0.3.0"
mkdir -p ~/.config/TAM3/data
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 "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
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
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"
else
echo "Neither Docker nor Podman are installed. Please install whichever you prefer and try again."
@@ -19,9 +21,9 @@ exit 1
fi
else
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
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"
else
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:
tam3-db:
image: docker.io/dbob16/tam3-db:0.2.0
image: docker.io/dbob16/tam3-db:${TAM3_VERSION}
restart: always
environment:
MARIADB_RANDOM_ROOT_PASSWORD: 1
@@ -16,7 +16,7 @@ services:
timeout: 5s
retries: 3
tam3-api:
image: docker.io/dbob16/tam3-api:0.2.0
image: docker.io/dbob16/tam3-api:${TAM3_VERSION}
restart: always
environment:
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_PASSWORD=${gen_password}" >> .env
echo "TAM3_VERSION=0.3.0" >> .env
if [ -x "$(command -v docker)" ]; then
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:
tam3-db:
image: docker.io/dbob16/tam3-db:0.2.0
image: docker.io/dbob16/tam3-db:${TAM3_VERSION}
restart: always
environment:
MARIADB_RANDOM_ROOT_PASSWORD: 1
@@ -16,7 +16,7 @@ services:
timeout: 5s
retries: 3
tam3-api:
image: docker.io/dbob16/tam3-api:0.2.0
image: docker.io/dbob16/tam3-api:0.2.0${TAM3_VERSION}
restart: always
environment:
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_PASSWORD=${gen_password}" >> .env
echo "TAM3_VERSION=0.3.0"
if [ -x "$(command -v docker)" ]; then
docker compose up -d

View File

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

View File

@@ -1,10 +1,16 @@
FROM docker.io/node:lts-alpine AS build
RUN mkdir /data
WORKDIR /app
ENV NODE_ENV=production
COPY . .
RUN npm install && npm run build
WORKDIR /app
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 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"
},
"devDependencies": {
"@libsql/client": "^0.14.0",
"@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@types/node": "^22",
"drizzle-kit": "^0.30.2",
"drizzle-orm": "^0.40.0",
"svelte": "^5.0.0",
"vite": "^7.0.4"
},
"dependencies": {
"hotkeys-js": "^3.13.15",
"drizzle-orm": "^0.40.0",
"drizzle-kit": "^0.30.2",
"@libsql/client": "^0.14.0"
}

View File

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

View File

@@ -11,6 +11,8 @@
if (browser) {
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+b', function(event) {event.preventDefault(); functions.prevPage(); 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 class="flex-row-space tb-margin">
<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>
<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={() => {
if (Math.abs(pagerForm.id_to - pagerForm.id_from) > 800) {
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,43 +1,60 @@
<script>
import { browser } from "$app/environment";
import { env } from "$env/dynamic/public";
import { setContext } from "svelte";
import hotkeys from "hotkeys-js";
import favicon from "$lib/assets/favicon.svg"
import favicon from "$lib/assets/favicon.svg";
let { data } = $props();
const all_prefixes = [...data.prefixes];
const all_prefixes = $derived(data.prefixes);
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);
const venue = env.PUBLIC_TAM3_VENUE || "TAM3";
$effect(() => {
const new_prefix = all_prefixes.find((prefix) => prefix.name === prefix_name);
const new_prefix = all_prefixes.find(
(prefix) => prefix.name === prefix_name,
);
if (new_prefix) {
current_prefix = {...new_prefix};
current_prefix = { ...new_prefix };
}
})
});
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;});
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>
<svelte:head>
<title>{venue} - Main Menu</title>
</svelte:head>
<div class="main-menu">
<div class="flex-row">
<img src="{favicon}" alt="TAM3 Icon - Red ticket with TAM3 on it" class="icon">
<img
src={favicon}
alt="TAM3 Icon - Red ticket with TAM3 on it"
class="icon"
/>
<h1>{venue} - Main Menu</h1>
</div>
{#if all_prefixes.length > 0}
<div class="universal-reports flex-row tb-margin">
<a href="/counts" target="_blank" class="styled">Counts</a>
<a href="/sheets" target="_blank" class="styled">Print Sheets</a>
</div>
<div>
<h2>Current Prefix: {current_prefix.name}</h2>
@@ -47,35 +64,67 @@
</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={() => {
<div
class="{prefix.color} p025{prefix.name === prefix_name
? ' active'
: ''}"
>
<button
class="styled"
onclick={() => {
prefix_name = prefix.name;
}}>{prefix.name}</button>
}}>{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>
<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>
<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>
{#if admin_mode}
<div><h2>Admin Mode:</h2></div>
<div><h2>Admin Mode:</h2></div>
<div class="flex-row">
<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"
>Search Tickets</a
>
<a href="/backuprestore" target="_blank" class="styled"
>Backup/Restore</a
>
<a href="/settings" target="_blank" class="styled">Settings</a>
</div>
</div>
{/if}
<div class="status tb-margin">

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

View File

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

View File

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

View File

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

View File

@@ -1,33 +1,53 @@
<script>
import { env } from '$env/dynamic/public';
import { browser } from '$app/environment';
import { env } from "$env/dynamic/public";
import { browser } from "$app/environment";
const { data } = $props();
const prefix = {...data.prefix};
const report_data = data.report;
let show_data = $state([...report_data]);
let report_subject = $state("All Preferences");
if (browser) {
document.title = `${prefix.name} Report By Name`
const prefix = $derived(data.prefix);
const report_data = $derived(data.report);
function copyReportData() {
return [...report_data];
}
let show_data = $state(copyReportData());
let report_subject = $state("All Preferences");
</script>
<svelte:head>
<title>{prefix.name} Report By Name</title>
</svelte:head>
<div id="reportheader">
<div class="flex-row-space {prefix.color}">
<div class="flex-row">
<button class="styled" onclick={() => {
show_data = [...report_data];
<button
class="styled"
onclick={() => {
show_data = copyReportData();
report_subject = "All Preferences";
}}>All Preferences</button>
<button class="styled" onclick={() => {
show_data = [...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")];
}}>All Preferences</button
>
<button
class="styled"
onclick={() => {
show_data = [
...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>
}}>Text</button
>
</div>
<div class="flex-row">
<button class="styled" onclick={() => window.print()}>Print</button>
@@ -35,9 +55,11 @@
</div>
</div>
<table>
<thead>
<thead>
<tr>
<th colspan="90"><h1>{prefix.name} - Report - {report_subject}</h1></th>
<th colspan="90"
><h1>{prefix.name} - Report - {report_subject}</h1></th
>
</tr>
<tr>
<th>Winner Name</th>
@@ -46,8 +68,8 @@
<th>Ticket #</th>
<th>Description</th>
</tr>
</thead>
<tbody>
</thead>
<tbody>
{#each show_data as report_entry}
<tr>
<td>{report_entry.winner_name}</td>
@@ -57,13 +79,13 @@
<td>{report_entry.description}</td>
</tr>
{/each}
</tbody>
<tfoot>
</tbody>
<tfoot>
<tr>
<td colspan="3">{env.PUBLIC_TAM3_VENUE || ""}</td>
<td colspan="2" style="text-align: right">TAM3 by Dilan Gilluly</td>
</tr>
</tfoot>
</tfoot>
</table>
<style>
@@ -76,7 +98,6 @@
border: solid 1px black;
padding: 0.2rem;
}
}
table tbody tr:nth-child(2n) {
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>
import { browser } from '$app/environment';
import { browser } from "$app/environment";
const { data } = $props();
let stagingSettings = $state({...data.settings});
let status = $state("")
function copySettings() {
return { ...data.settings };
}
let stagingSettings = $state(copySettings());
let status = $state("");
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) {
status = "Changes saved successfully";
setTimeout(() => {status = ""}, 5000);
setTimeout(() => {
status = "";
}, 5000);
} else {
status = "Error saving changes, check config file path, make sure folder exists";
setTimeout(() => {status = ""}, 5000);
status =
"Error saving changes, check config file path, make sure folder exists";
setTimeout(() => {
status = "";
}, 5000);
}
}
function cancelChanges() {
stagingSettings = {...data.settings};
stagingSettings = { ...data.settings };
}
if (browser) {
document.title = "TAM3 - Settings"
document.title = "TAM3 - Settings";
}
</script>
@@ -31,11 +43,16 @@
<h2>Remote Server</h2>
<div><strong>Address:</strong></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 class="flex-row">
<input type="password" bind:value={stagingSettings.TAM3_REMOTE_KEY}>
<button class="styled" onclick={() => {stagingSettings.TAM3_REMOTE_KEY = ""}}>Clear</button>
<input type="password" bind:value={stagingSettings.TAM3_REMOTE_KEY} />
<button
class="styled"
onclick={() => {
stagingSettings.TAM3_REMOTE_KEY = "";
}}>Clear</button
>
</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>
import { browser } from '$app/environment';
import FormHeader from '$lib/components/FormHeader.svelte';
import { focusElement } from '$lib/focusElement.js';
import { browser } from "$app/environment";
import FormHeader from "$lib/components/FormHeader.svelte";
import { focusElement } from "$lib/focusElement.js";
const { data } = $props();
let prefix = {...data.prefix};
let pagerForm = $state({id_from: 0, id_to: 0});
const prefix = $derived(data.prefix);
let pagerForm = $state({ id_from: 0, id_to: 0 });
let current_idx = $state(0);
let next_idx = $derived(current_idx+1);
let prev_idx = $derived(current_idx-1);
let next_idx = $derived(current_idx + 1);
let prev_idx = $derived(current_idx - 1);
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();
function changeFocus(idx) {
@@ -22,42 +22,55 @@
const functions = {
refreshPage: async () => {
if (current_tickets.filter(ticket => ticket.changed === true).length > 0) {
if (
current_tickets.filter((ticket) => ticket.changed === true)
.length > 0
) {
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) {
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()
functions.refreshPage();
},
nextPage: () => {
const diff = current_tickets.length;
pagerForm.id_from = pagerForm.id_from + diff;
pagerForm.id_to = pagerForm.id_to + diff;
functions.refreshPage()
functions.refreshPage();
},
duplicateDown: () => {
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);
} else {
changeFocus(current_idx)
changeFocus(current_idx);
}
},
duplicateUp: () => {
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);
} else {
changeFocus(current_idx)
changeFocus(current_idx);
}
},
gotoNext: () => {
@@ -75,37 +88,58 @@
}
},
copy: () => {
copy_buffer = {...current_tickets[current_idx]};
copy_buffer = { ...current_tickets[current_idx] };
changeFocus(current_idx);
disablePaste = false;
},
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);
},
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'}});
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};
for (let ticket of current_tickets) {
ticket.changed = false;
}
changeFocus(0);
}
}
},
};
if (browser) {
document.title = `${prefix.name} Ticket Entry`;
window.addEventListener("beforeunload", function(e) {
if (current_tickets.filter(ticket => ticket.changed === true).length > 0) {
window.addEventListener("beforeunload", function (e) {
if (
current_tickets.filter((ticket) => ticket.changed === true)
.length > 0
) {
e.preventDefault();
}
});
}
</script>
<svelte:head>
<title>{prefix.name} Ticket Entry</title>
</svelte:head>
<h1>{prefix.name} Ticket Entry</h1>
<FormHeader {prefix} {functions} bind:pagerForm bind:headerHeight />
<table>
<thead style="top: {headerHeight+2}px">
<thead style="top: {headerHeight + 2}px">
<tr>
<th style="width: 12ch">Ticket ID</th>
<th>First Name</th>
@@ -117,16 +151,54 @@
</thead>
<tbody>
{#each current_tickets as ticket, idx}
<tr onfocusin={() => current_idx = idx}>
<tr onfocusin={() => (current_idx = idx)}>
<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}>
<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>
</select></td
>
<td
><button
tabindex="-1"
onclick={() => (ticket.changed = !ticket.changed)}
>{ticket.changed ? "Y" : "N"}</button
></td
>
</tr>
{/each}
</tbody>
@@ -156,7 +228,8 @@
background: transparent;
border: solid 1px #000000;
}
input, button {
input,
button {
display: block;
box-sizing: border-box;
width: 100%;