nightly - 2025-09-24
This commit is contained in:
@@ -9,3 +9,11 @@ Goals for this project:
|
||||
- Also have a desktop app available (eventually, maybe Electron or Tauri based)
|
||||
|
||||
**This is under _active_ development**
|
||||
|
||||
## Setting up
|
||||
|
||||
After cloning, cd'ing into webapp, and running `npm install` followed by `npm run dev` there's one thing you have to do before testing the rest.
|
||||
|
||||
That is making prefixes.
|
||||
|
||||
On the main menu press "Alt (or Option) + a" to toggle admin mode. Then click Prefix editor to open the form to edit prefixes.
|
||||
12
webapp/package-lock.json
generated
12
webapp/package-lock.json
generated
@@ -7,6 +7,9 @@
|
||||
"": {
|
||||
"name": "webapp",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"hotkeys-js": "^3.13.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@libsql/client": "^0.14.0",
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
@@ -2164,6 +2167,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hotkeys-js": {
|
||||
"version": "3.13.15",
|
||||
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.15.tgz",
|
||||
"integrity": "sha512-gHh8a/cPTCpanraePpjRxyIlxDFrIhYqjuh01UHWEwDpglJKCnvLW8kqSx5gQtOuSsJogNZXLhOdbSExpgUiqg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
|
||||
@@ -23,5 +23,8 @@
|
||||
"drizzle-orm": "^0.40.0",
|
||||
"svelte": "^5.0.0",
|
||||
"vite": "^7.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"hotkeys-js": "^3.13.15"
|
||||
}
|
||||
}
|
||||
|
||||
46
webapp/src/lib/components/FormHeader.svelte
Normal file
46
webapp/src/lib/components/FormHeader.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script>
|
||||
let {
|
||||
prefix,
|
||||
pagerForm = $bindable(),
|
||||
functions
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div id="formheader" class="{prefix.color}">
|
||||
<div class="flex-row-space tb-margin">
|
||||
<div class="flex-row">
|
||||
<input type="number" bind:value={pagerForm.id_from}>
|
||||
<div style="font-size: 22pt">-</div>
|
||||
<input type="number" bind:value={pagerForm.id_to}>
|
||||
<button class="styled" onclick={functions.refreshPage}>Refresh</button>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<button class="styled" tabindex="-1" onclick={functions.prevPage}>Prev Page</button>
|
||||
<button class="styled" tabindex="-1" onclick={functions.nextPage}>Next Page</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-row-space tb-margin">
|
||||
<div class="flex-row">
|
||||
<button class="styled" tabindex="-1" onclick={functions.duplicateDown}>Duplicate Down</button>
|
||||
<button class="styled" tabindex="-1" onclick={functions.duplicateUp}>Duplicate Up</button>
|
||||
<button class="styled" tabindex="-1" onclick={functions.gotoNext}>Next</button>
|
||||
<button class="styled" tabindex="-1" onclick={functions.gotoPrev}>Previous</button>
|
||||
<button class="styled" tabindex="-1" onclick={functions.copy}>Copy</button>
|
||||
<button class="styled" tabindex="-1" onclick={functions.paste}>Paste</button>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<button class="styled" 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>
|
||||
@@ -19,9 +19,22 @@ a.styled:hover, button.styled:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.tb-margin {
|
||||
padding-top: 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.flex-row-space {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
<script>
|
||||
import { browser } from "$app/environment";
|
||||
import { env } from "$env/dynamic/public";
|
||||
import hotkeys from "hotkeys-js";
|
||||
|
||||
let { data } = $props();
|
||||
let prefix_name = $state("");
|
||||
let all_prefixes = $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);
|
||||
@@ -15,12 +19,19 @@
|
||||
|
||||
if (browser) {
|
||||
all_prefixes = [...data.prefixes];
|
||||
document.title = "TAM3 - Main Menu"
|
||||
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>
|
||||
|
||||
<div class="main-menu">
|
||||
<h1>TAM3 - Main Menu</h1>
|
||||
<h1>{venue} - Main Menu</h1>
|
||||
<div class="prefix-selector">
|
||||
<select style="width: 100%; box-sizing: border-box;" bind:value={prefix_name}>
|
||||
{#each all_prefixes as prefix}
|
||||
@@ -34,6 +45,21 @@
|
||||
<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="/report/byname/{current_prefix.name}/" target="_blank" class="styled">By Name</a>
|
||||
<a href="/report/bybasket/{current_prefix.name}/" target="_blank" class="styled">By Basket ID</a>
|
||||
</div>
|
||||
{#if admin_mode}
|
||||
<div><h2>Admin Mode:</h2></div>
|
||||
<div class="flex-row {current_prefix.color}">
|
||||
<a href="/prefixes" target="_blank" class="styled">Prefix Editor</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="annotation">
|
||||
<p>Ticket Auction Manager 3 by Dilan Gilluly</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function GET({ params }) {
|
||||
} else {
|
||||
const data = await db.select().from(prefixes).where(eq(prefixes.name, name));
|
||||
if (data[0]) {
|
||||
return new Response(JSON.stringify(data[0]), {status: 200, statusText: "Prefix loaded successfully."})
|
||||
return new Response(JSON.stringify(data[0]), {status: 200, statusText: "Prefix loaded successfully.", headers: {'Content-Type': 'application/json'}})
|
||||
} else {
|
||||
return new Response(JSON.stringify({status: "Issue loading prefix"}), {status: 404, statusText: "Prefix not found."})
|
||||
}
|
||||
|
||||
@@ -2,19 +2,46 @@ import { db } from "$lib/server/db";
|
||||
import { tickets } from "$lib/server/db/schema";
|
||||
import { env } from "$env/dynamic/private";
|
||||
|
||||
export async function GET({ params }) {
|
||||
export async function GET() {
|
||||
if (env.TAM3_REMOTE) {
|
||||
const res = await fetch(`${env.TAM3_REMOTE}/api/tickets/?api_key=${env.TAM3_REMOTE_KEY}`);
|
||||
if (!res.ok) {
|
||||
return new Response(JSON.stringify({details: "Couldn't fetch tickets."}), {
|
||||
status: res.status,
|
||||
statusText: res.statusText
|
||||
});
|
||||
statusText: res.statusText,
|
||||
headers: {'Content-Type': 'applicaiton/json'}
|
||||
})
|
||||
};
|
||||
const data = await res.json();
|
||||
return new Response(JSON.stringify(data), {status: 200, statusText: "Tickets fetched successfully."})
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
statusText: "Tickets fetched successfully.",
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
} else {
|
||||
const data = await db.select().from(tickets);
|
||||
return new Response(JSON.stringify(data), {status: 200, statusText: "Tickets loaded successfully."})
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
statusText: "Tickets loaded successfully.",
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
export async function POST({ request }) {
|
||||
const i_tickets = await request.json();
|
||||
for (let ticket of i_tickets) {
|
||||
await db.insert(tickets).values({prefix: ticket.prefix, t_id: ticket.t_id, first_name: ticket.first_name, last_name: ticket.last_name, phone_number: ticket.phone_number, preference: ticket.preference})
|
||||
.onConflictDoUpdate({target: [tickets.prefix, tickets.t_id], set: {first_name: ticket.first_name, last_name: ticket.last_name, phone_number: ticket.phone_number, preference: ticket.preference}});
|
||||
};
|
||||
if (env.TAM3_REMOTE) {
|
||||
const res = await fetch(`${env.TAM3_REMOTE}/api/tickets/?api_key=${env.TAM3_REMOTE_KEY}`, {
|
||||
body: JSON.stringify([...i_tickets]), method: 'POST', headers: {'Content-Type': 'application/json'}
|
||||
});
|
||||
if (!res.ok) {
|
||||
return new Response(JSON.stringify({details: "Issue posting tickets to remote."}), {status: res.status, statusText: res.statusText})
|
||||
};
|
||||
const data = await res.json();
|
||||
};
|
||||
return new Response(JSON.stringify({details: "Posted tickets successfully."}), {status: 200, statusText: "Posted tickets successfully."})
|
||||
}
|
||||
6
webapp/src/routes/tickets/[prefix]/+page.js
Normal file
6
webapp/src/routes/tickets/[prefix]/+page.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export async function load({ fetch, params }) {
|
||||
const { prefix } = await params;
|
||||
const res = await fetch(`/api/prefixes/${prefix}`);
|
||||
const prefix_data = await res.json();
|
||||
return {prefix: prefix_data}
|
||||
}
|
||||
160
webapp/src/routes/tickets/[prefix]/+page.svelte
Normal file
160
webapp/src/routes/tickets/[prefix]/+page.svelte
Normal file
@@ -0,0 +1,160 @@
|
||||
<script>
|
||||
import { browser } from '$app/environment';
|
||||
import FormHeader from '$lib/components/FormHeader.svelte';
|
||||
import hotkeys from 'hotkeys-js';
|
||||
|
||||
const { data } = $props();
|
||||
let prefix = {...data.prefix};
|
||||
let pagerForm = $state({id_from: 0, id_to: 0});
|
||||
let current_idx = $state(0);
|
||||
let current_tickets = $state([]);
|
||||
let copy_buffer = $state({prefix: prefix.name, t_id: 0, first_name: "", last_name: "", phone_number: "", preference: "CALL", changed: true});
|
||||
|
||||
function changeFocus(idx) {
|
||||
const focusFn = document.getElementById(`${idx}_fn`);
|
||||
if (focusFn) {
|
||||
focusFn.focus();
|
||||
}
|
||||
}
|
||||
|
||||
const functions = {
|
||||
refreshPage: async () => {
|
||||
if (current_tickets.length > 0) {
|
||||
functions.saveAll();
|
||||
};
|
||||
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 = pagerForm.id_to - pagerForm.id_from + 1;
|
||||
pagerForm.id_from = pagerForm.id_from - diff;
|
||||
pagerForm.id_to = pagerForm.id_to - diff;
|
||||
functions.refreshPage()
|
||||
},
|
||||
nextPage: () => {
|
||||
const diff = pagerForm.id_to - pagerForm.id_from + 1;
|
||||
pagerForm.id_from = pagerForm.id_from + diff;
|
||||
pagerForm.id_to = pagerForm.id_to + diff;
|
||||
functions.refreshPage()
|
||||
},
|
||||
duplicateDown: () => {
|
||||
const next_idx = current_idx + 1;
|
||||
if (current_tickets[next_idx]) {
|
||||
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)
|
||||
}
|
||||
},
|
||||
duplicateUp: () => {
|
||||
const prev_idx = current_idx - 1;
|
||||
if (prev_idx >= 0) {
|
||||
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)
|
||||
}
|
||||
},
|
||||
gotoNext: () => {
|
||||
const next_idx = current_idx + 1;
|
||||
if (current_tickets[next_idx]) {
|
||||
changeFocus(next_idx);
|
||||
} else {
|
||||
changeFocus(current_idx);
|
||||
}
|
||||
},
|
||||
gotoPrev: () => {
|
||||
const prev_idx = current_idx - 1;
|
||||
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, 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) {
|
||||
current_tickets.map((ticket) => ticket.changed = false);
|
||||
changeFocus(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (browser) {
|
||||
document.title = `${prefix.name} Ticket Entry`
|
||||
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>
|
||||
|
||||
<h1>{prefix.name} Ticket Entry</h1>
|
||||
<FormHeader {prefix} {functions} bind:pagerForm />
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<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.t_id}</td>
|
||||
<td><input id="{idx}_fn" type="text" bind:value={ticket.first_name} onchange={() => ticket.changed = true}></td>
|
||||
<td><input id="{idx}_ln" type="text" bind:value={ticket.last_name} onchange={() => ticket.changed = true}></td>
|
||||
<td><input id="{idx}_pn" type="text" bind:value={ticket.phone_number} onchange={() => ticket.changed = true}></td>
|
||||
<td><select id="{idx}_pr" style="width: 100%" bind:value={ticket.preference} 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%;
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
tbody tr:nth-child(2n) {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
input {
|
||||
background: transparent;
|
||||
border: solid 1px #000000;
|
||||
}
|
||||
input, button {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user