Compare commits
10 Commits
195d2caf52
...
079155d8a5
| Author | SHA1 | Date | |
|---|---|---|---|
| 079155d8a5 | |||
| d4715f5a03 | |||
| 2738ac6e89 | |||
| 0279ce7078 | |||
| 287e89efa0 | |||
| ec0396b065 | |||
| 70b781cf27 | |||
| 767eb8a339 | |||
| 9067108798 | |||
| 34f0559709 |
@@ -1,3 +1,4 @@
|
||||
__pycache__/
|
||||
*/__pycache__/
|
||||
data/
|
||||
build.sh
|
||||
|
||||
6
api/build.sh
Executable file
6
api/build.sh
Executable 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
|
||||
@@ -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
16
api/repos/search.py
Normal 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
17
api/routers/search.py
Normal 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
1
db/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
build.sh
|
||||
6
db/build.sh
Executable file
6
db/build.sh
Executable 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
|
||||
@@ -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."
|
||||
|
||||
5
deployment/client/client-package.sh
Executable file
5
deployment/client/client-package.sh
Executable 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
|
||||
5
deployment/client/client-save.sh
Executable file
5
deployment/client/client-save.sh
Executable 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
3
deployment/common.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
export tam3_version="0.3.0"
|
||||
@@ -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
|
||||
|
||||
10
deployment/remote_server/delete-key.sh
Executable file
10
deployment/remote_server/delete-key.sh
Executable 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
|
||||
5
deployment/remote_server/package.sh
Executable file
5
deployment/remote_server/package.sh
Executable 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
|
||||
@@ -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
|
||||
|
||||
16
deployment/remote_server_offline_images/server-save.sh
Executable file
16
deployment/remote_server_offline_images/server-save.sh
Executable 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
|
||||
@@ -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
|
||||
|
||||
10
deployment/remote_server_secure/delete-key.sh
Executable file
10
deployment/remote_server_secure/delete-key.sh
Executable 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
|
||||
5
deployment/remote_server_secure/package.sh
Executable file
5
deployment/remote_server_secure/package.sh
Executable 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/
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
/node_modules/
|
||||
*.db
|
||||
build.sh
|
||||
|
||||
@@ -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
8
webapp/build.sh
Executable 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
692
webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
163
webapp/src/lib/components/SearchHeader.svelte
Normal file
163
webapp/src/lib/components/SearchHeader.svelte
Normal 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>
|
||||
@@ -1,93 +1,142 @@
|
||||
<script>
|
||||
import { browser } from "$app/environment";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import hotkeys from "hotkeys-js";
|
||||
import favicon from "$lib/assets/favicon.svg"
|
||||
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";
|
||||
|
||||
let { data } = $props();
|
||||
const all_prefixes = [...data.prefixes];
|
||||
let prefix_name = $state("");
|
||||
let current_prefix = $state({name: "", color: "", weight: 0});
|
||||
let admin_mode = $state(false);
|
||||
const venue = env.PUBLIC_TAM3_VENUE || "TAM3";
|
||||
let { data } = $props();
|
||||
const all_prefixes = $derived(data.prefixes);
|
||||
let prefix_name = $state("");
|
||||
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);
|
||||
if (new_prefix) {
|
||||
current_prefix = {...new_prefix};
|
||||
$effect(() => {
|
||||
const new_prefix = all_prefixes.find(
|
||||
(prefix) => prefix.name === prefix_name,
|
||||
);
|
||||
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>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
</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 class="flex-row">
|
||||
<img
|
||||
src={favicon}
|
||||
alt="TAM3 Icon - Red ticket with TAM3 on it"
|
||||
class="icon"
|
||||
/>
|
||||
<h1>{venue} - Main Menu</h1>
|
||||
</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}
|
||||
{#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>
|
||||
</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>
|
||||
{/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>
|
||||
{#if admin_mode}
|
||||
<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="/settings" target="_blank" class="styled">Settings</a>
|
||||
</div>
|
||||
<div><h2>Admin Mode:</h2></div>
|
||||
<div class="flex-row">
|
||||
<a href="/prefixes" target="_blank" class="styled">Prefix Editor</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>
|
||||
{/if}
|
||||
|
||||
<div class="status tb-margin">
|
||||
{data.status}
|
||||
{data.status}
|
||||
</div>
|
||||
|
||||
<div class="annotation">
|
||||
<p>Ticket Auction Manager 3 by Dilan Gilluly</p>
|
||||
<p>Ticket Auction Manager 3 by Dilan Gilluly</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
img.icon {
|
||||
max-width: 150px;
|
||||
}
|
||||
img.icon {
|
||||
max-width: 150px;
|
||||
}
|
||||
</style>
|
||||
|
||||
49
webapp/src/routes/api/search/tickets/+server.js
Normal file
49
webapp/src/routes/api/search/tickets/+server.js
Normal 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" },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
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'}});
|
||||
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};
|
||||
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,12 +150,32 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each current_baskets as basket, 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>
|
||||
</tr>
|
||||
<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
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -148,7 +204,8 @@
|
||||
background: transparent;
|
||||
border: solid 1px #000000;
|
||||
}
|
||||
input, button {
|
||||
input,
|
||||
button {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
@@ -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>
|
||||
@@ -25,11 +33,11 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each counts as count}
|
||||
<tr class={colormap[count.prefix]}>
|
||||
<td>{count.prefix}</td>
|
||||
<td>{parseInt(count.total_sold).toLocaleString()}</td>
|
||||
<td>{parseInt(count.unique_sold).toLocaleString()}</td>
|
||||
</tr>
|
||||
<tr class={colormap[count.prefix]}>
|
||||
<td>{count.prefix}</td>
|
||||
<td>{parseInt(count.total_sold).toLocaleString()}</td>
|
||||
<td>{parseInt(count.unique_sold).toLocaleString()}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -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,19 +148,32 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each current_drawings as drawing, 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 () => {
|
||||
drawing.changed = true;
|
||||
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}`
|
||||
}
|
||||
}}></td>
|
||||
<td>{drawing.winner}</td>
|
||||
<td><button tabindex="-1">{drawing.changed ? "Y" : "N"}</button></td>
|
||||
</tr>
|
||||
<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 () => {
|
||||
drawing.changed = true;
|
||||
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}`;
|
||||
}
|
||||
}}
|
||||
/></td
|
||||
>
|
||||
<td>{drawing.winner}</td>
|
||||
<td
|
||||
><button tabindex="-1">{drawing.changed ? "Y" : "N"}</button
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -155,7 +202,8 @@
|
||||
background: transparent;
|
||||
border: solid 1px #000000;
|
||||
}
|
||||
input, button {
|
||||
input,
|
||||
button {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
@@ -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];
|
||||
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")];
|
||||
report_subject = "TEXT Preference";
|
||||
}}>Text</button>
|
||||
<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",
|
||||
),
|
||||
];
|
||||
report_subject = "TEXT Preference";
|
||||
}}>Text</button
|
||||
>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<button class="styled" onclick={() => window.print()}>Print</button>
|
||||
@@ -35,35 +55,37 @@
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="90"><h1>{prefix.name} - Report - {report_subject}</h1></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Basket ID</th>
|
||||
<th>Description</th>
|
||||
<th>Ticket #</th>
|
||||
<th>Winner Name</th>
|
||||
<th>Phone Number</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each show_data as report_entry}
|
||||
<tr>
|
||||
<td>{report_entry.b_id}</td>
|
||||
<td>{report_entry.description}</td>
|
||||
<td>{report_entry.winning_ticket}</td>
|
||||
<td>{report_entry.winner_name}</td>
|
||||
<td>{report_entry.phone_number}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</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>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="90"
|
||||
><h1>{prefix.name} - Report - {report_subject}</h1></th
|
||||
>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Basket ID</th>
|
||||
<th>Description</th>
|
||||
<th>Ticket #</th>
|
||||
<th>Winner Name</th>
|
||||
<th>Phone Number</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each show_data as report_entry}
|
||||
<tr>
|
||||
<td>{report_entry.b_id}</td>
|
||||
<td>{report_entry.description}</td>
|
||||
<td>{report_entry.winning_ticket}</td>
|
||||
<td>{report_entry.winner_name}</td>
|
||||
<td>{report_entry.phone_number}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</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>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
@@ -76,7 +98,6 @@
|
||||
border: solid 1px black;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
}
|
||||
table tbody tr:nth-child(2n) {
|
||||
background-color: #dddddd;
|
||||
|
||||
@@ -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];
|
||||
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")];
|
||||
report_subject = "TEXT Preference";
|
||||
}}>Text</button>
|
||||
<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",
|
||||
),
|
||||
];
|
||||
report_subject = "TEXT Preference";
|
||||
}}>Text</button
|
||||
>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<button class="styled" onclick={() => window.print()}>Print</button>
|
||||
@@ -35,35 +55,37 @@
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="90"><h1>{prefix.name} - Report - {report_subject}</h1></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Winner Name</th>
|
||||
<th>Phone Number</th>
|
||||
<th>Basket ID</th>
|
||||
<th>Ticket #</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each show_data as report_entry}
|
||||
<tr>
|
||||
<td>{report_entry.winner_name}</td>
|
||||
<td>{report_entry.phone_number}</td>
|
||||
<td>{report_entry.b_id}</td>
|
||||
<td>{report_entry.winning_ticket}</td>
|
||||
<td>{report_entry.description}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</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>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="90"
|
||||
><h1>{prefix.name} - Report - {report_subject}</h1></th
|
||||
>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Winner Name</th>
|
||||
<th>Phone Number</th>
|
||||
<th>Basket ID</th>
|
||||
<th>Ticket #</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each show_data as report_entry}
|
||||
<tr>
|
||||
<td>{report_entry.winner_name}</td>
|
||||
<td>{report_entry.phone_number}</td>
|
||||
<td>{report_entry.b_id}</td>
|
||||
<td>{report_entry.winning_ticket}</td>
|
||||
<td>{report_entry.description}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</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>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
@@ -76,7 +98,6 @@
|
||||
border: solid 1px black;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
}
|
||||
table tbody tr:nth-child(2n) {
|
||||
background-color: #dddddd;
|
||||
|
||||
248
webapp/src/routes/search/tickets/+page.svelte
Normal file
248
webapp/src/routes/search/tickets/+page.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
131
webapp/src/routes/sheets/+page.svelte
Normal file
131
webapp/src/routes/sheets/+page.svelte
Normal 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>
|
||||
@@ -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,17 +151,55 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each current_tickets as ticket, 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}>
|
||||
<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>
|
||||
<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)}
|
||||
>
|
||||
<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>
|
||||
@@ -156,7 +228,8 @@
|
||||
background: transparent;
|
||||
border: solid 1px #000000;
|
||||
}
|
||||
input, button {
|
||||
input,
|
||||
button {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user