diff --git a/api/app/data/search.py b/api/app/data/search.py new file mode 100644 index 0000000..f6ad5fe --- /dev/null +++ b/api/app/data/search.py @@ -0,0 +1,37 @@ +from fastapi import APIRouter, Header +from .tickets import Ticket +from db import RepoTemplate +from system.auth import AuthRepo + +chunk_size = 300 + +def wild_enc(str_in: str): + return f"%{str_in}%" + +class SearchRepo(RepoTemplate): + def ticket_search(self, first_name: str, last_name: str, phone_number: str): + self.cur.execute("SELECT * FROM tickets WHERE first_name LIKE ? AND last_name LIKE ? AND phone_number LIKE ? ORDER BY prefix, t_id", (wild_enc(first_name), wild_enc(last_name), wild_enc(phone_number))) + results = self.cur.fetchall() + return [Ticket(*r) for r in results] + def post_tickets(self, ts: list[Ticket]): + for i in range(0, len(ts), chunk_size): + chunk = ts[i:i+chunk_size] + for t in chunk: + self.cur.execute("""INSERT INTO tickets VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (prefix, t_id) + DO UPDATE SET first_name = EXCLUDED.first_name, last_name = EXCLUDED.last_name, + phone_number = EXCLUDED.phone_number, pref = EXCLUDED.pref""", + (t.prefix, t.t_id, t.first_name, t.last_name, t.phone_number, t.pref)) + self.conn.commit() + return ts + +search_router = APIRouter(prefix="/api/search") + +@search_router.get("/tickets") +def search_tickets(first_name: str = "", last_name: str = "", phone_number: str = "", tam_key: str = Header("")): + AuthRepo().verify_key(tam_key) + return SearchRepo().ticket_search(first_name, last_name, phone_number) + +@search_router.post("/tickets") +def post_tickets(ts: list[Ticket], tam_key: str = Header("")): + AuthRepo().verify_key(tam_key) + return SearchRepo().post_tickets(ts) diff --git a/api/app/routers.py b/api/app/routers.py index 25806fe..52176af 100644 --- a/api/app/routers.py +++ b/api/app/routers.py @@ -5,6 +5,7 @@ from data.tickets import tickets_router from data.baskets import basket_router from data.drawing import drawing_router from data.reports import reports_router +from data.search import search_router def imp_routers(app: FastAPI): app.include_router(auth_router) @@ -13,3 +14,4 @@ def imp_routers(app: FastAPI): app.include_router(basket_router) app.include_router(drawing_router) app.include_router(reports_router) + app.include_router(search_router) diff --git a/client/.env.example b/client/.env.example index b86d245..6142df5 100644 --- a/client/.env.example +++ b/client/.env.example @@ -1,2 +1 @@ -# Drizzle -DATABASE_URL=local.db +TAM_DATA_DIR=./data diff --git a/client/drizzle/0000_init.sql b/client/drizzle/0000_init.sql index f3fa0c3..759894b 100644 --- a/client/drizzle/0000_init.sql +++ b/client/drizzle/0000_init.sql @@ -1,4 +1,4 @@ -CREATE TABLE `baskets` ( +CREATE TABLE IF NOT EXISTS `baskets` ( `prefix` text, `b_id` integer, `description` text, @@ -7,13 +7,13 @@ CREATE TABLE `baskets` ( PRIMARY KEY(`prefix`, `b_id`) ); --> statement-breakpoint -CREATE TABLE `prefixes` ( +CREATE TABLE IF NOT EXISTS `prefixes` ( `prefix` text PRIMARY KEY NOT NULL, `color` text, `weight` integer ); --> statement-breakpoint -CREATE TABLE `tickets` ( +CREATE TABLE IF NOT EXISTS `tickets` ( `prefix` text, `t_id` integer, `first_name` text, @@ -23,18 +23,18 @@ CREATE TABLE `tickets` ( PRIMARY KEY(`prefix`, `t_id`) ); --> statement-breakpoint -CREATE VIEW `drawing` AS SELECT b.prefix, b.b_id, b.description, b.winning_ticket, t.last_name, t.first_name, t.phone_number +CREATE VIEW IF NOT EXISTS `drawing` AS SELECT b.prefix, b.b_id, b.description, b.winning_ticket, t.last_name, t.first_name, t.phone_number FROM baskets b LEFT JOIN tickets t ON b.prefix = t.prefix AND b.winning_ticket = t.t_id ORDER BY b.prefix, b.b_id;--> statement-breakpoint -CREATE VIEW `report_by_basket` AS SELECT b.prefix, b.b_id, b.description, b.donors, b.winning_ticket, t.last_name, t.first_name, t.phone_number, t.pref +CREATE VIEW IF NOT EXISTS `report_by_basket` AS SELECT b.prefix, b.b_id, b.description, b.donors, b.winning_ticket, t.last_name, t.first_name, t.phone_number, t.pref FROM baskets b LEFT JOIN tickets t on b.prefix = t.prefix AND b.winning_ticket = t.t_id ORDER BY b.prefix, b.b_id;--> statement-breakpoint -CREATE VIEW `report_by_name` AS SELECT t.last_name, t.first_name, t.phone_number, t.pref, b.prefix, b.b_id, b.description, b.donors, b.winning_ticket +CREATE VIEW IF NOT EXISTS `report_by_name` AS SELECT t.last_name, t.first_name, t.phone_number, t.pref, b.prefix, b.b_id, b.description, b.donors, b.winning_ticket FROM baskets b LEFT JOIN tickets t ON b.prefix = t.prefix AND b.winning_ticket = t.t_id ORDER BY t.last_name, t.first_name, t.phone_number, b.prefix, b.b_id;--> statement-breakpoint -CREATE VIEW `report_counts` AS SELECT prefix, COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))) AS unique_buyers, COUNT(*) AS total_buys +CREATE VIEW IF NOT EXISTS `report_counts` AS SELECT prefix, COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))) AS unique_buyers, COUNT(*) AS total_buys FROM tickets GROUP BY prefix UNION ALL SELECT 'Total', COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))), COUNT(*) - FROM tickets; \ No newline at end of file + FROM tickets; diff --git a/client/src/hooks.server.js b/client/src/hooks.server.js new file mode 100644 index 0000000..c1380b2 --- /dev/null +++ b/client/src/hooks.server.js @@ -0,0 +1,41 @@ +import { db } from "$lib/server/db"; + +export const init = async () => { + await db.run(`CREATE TABLE IF NOT EXISTS baskets ( + prefix text, + b_id integer, + description text, + donors text, + winning_ticket integer, + PRIMARY KEY(prefix, b_id) + )`); + await db.run(`CREATE TABLE IF NOT EXISTS prefixes ( + prefix text PRIMARY KEY NOT NULL, + color text, + weight integer + )`); + await db.run(`CREATE TABLE IF NOT EXISTS tickets ( + prefix text, + t_id integer, + first_name text, + last_name text, + phone_number text, + pref text, + PRIMARY KEY(prefix, t_id) + )`); + await db.run(`CREATE VIEW IF NOT EXISTS drawing AS SELECT b.prefix, b.b_id, b.description, b.winning_ticket, t.last_name, t.first_name, t.phone_number + FROM baskets b LEFT JOIN tickets t ON b.prefix = t.prefix AND b.winning_ticket = t.t_id + ORDER BY b.prefix, b.b_id`); + await db.run(`CREATE VIEW IF NOT EXISTS report_by_basket AS SELECT b.prefix, b.b_id, b.description, b.donors, b.winning_ticket, t.last_name, t.first_name, t.phone_number, t.pref + FROM baskets b LEFT JOIN tickets t on b.prefix = t.prefix AND b.winning_ticket = t.t_id + ORDER BY b.prefix, b.b_id`); + await db.run(`CREATE VIEW IF NOT EXISTS report_by_name AS SELECT t.last_name, t.first_name, t.phone_number, t.pref, b.prefix, b.b_id, b.description, b.donors, b.winning_ticket + FROM baskets b LEFT JOIN tickets t ON b.prefix = t.prefix AND b.winning_ticket = t.t_id + ORDER BY t.last_name, t.first_name, t.phone_number, b.prefix, b.b_id`) + await db.run(`CREATE VIEW IF NOT EXISTS report_counts AS SELECT prefix, COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))) AS unique_buyers, COUNT(*) AS total_buys + FROM tickets + GROUP BY prefix + UNION ALL + SELECT 'Total', COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))), COUNT(*) + FROM tickets`); +} diff --git a/client/src/lib/client/components/TicketSearchBar.svelte b/client/src/lib/client/components/TicketSearchBar.svelte new file mode 100644 index 0000000..907d753 --- /dev/null +++ b/client/src/lib/client/components/TicketSearchBar.svelte @@ -0,0 +1,60 @@ + + +
+
+
First Name:
+ e.target.select()} + /> +
Last Name:
+ e.target.select()} + /> +
Phone Number:
+ e.target.select()} + /> + {#if functions.search} + + {/if} +
+
diff --git a/client/src/lib/client/styles.js b/client/src/lib/client/styles.js index 4296ce2..4bbe64d 100644 --- a/client/src/lib/client/styles.js +++ b/client/src/lib/client/styles.js @@ -1,12 +1,12 @@ export const tS = { - gray: 'text-gray-700 font-bold', - white: 'font-bold', - blue: 'text-blue-700 font-bold', - yellow: 'text-yellow-600 font-bold', - green: 'text-green-700 font-bold', - orange: 'text-orange-700 font-bold', - purple: 'text-purple-700 font-bold', - red: 'text-red-700 font-bold' + gray: 'text-gray-700', + white: 'text-black', + blue: 'text-blue-700', + yellow: 'text-yellow-600', + green: 'text-green-700', + orange: 'text-orange-700', + purple: 'text-purple-700', + red: 'text-red-700' }; export const bS = { diff --git a/client/src/lib/server/db/schema.js b/client/src/lib/server/db/schema.js index 88f762b..345745c 100644 --- a/client/src/lib/server/db/schema.js +++ b/client/src/lib/server/db/schema.js @@ -76,12 +76,13 @@ export const reportByBasket = sqliteView('report_by_basket', { ORDER BY b.prefix, b.b_id`); export const reportCounts = sqliteView('report_counts', { - prefix: text('prefix'), - unique_buyers: integer('unique_buyers'), - total_buys: integer('total_buys') -}).as(sql`SELECT prefix, COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))) AS unique_buyers, COUNT(*) AS total_buys + prefix: text('prefix'), + unique_buyers: integer('unique_buyers'), + total_buys: integer('total_buys') +}) + .as(sql`SELECT prefix, COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))) AS unique_buyers, COUNT(*) AS total_buys FROM tickets GROUP BY prefix UNION ALL SELECT 'Total', COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))), COUNT(*) - FROM tickets`) + FROM tickets`); diff --git a/client/src/routes/+page.svelte b/client/src/routes/+page.svelte index f7012bb..348cfe8 100644 --- a/client/src/routes/+page.svelte +++ b/client/src/routes/+page.svelte @@ -1,5 +1,5 @@ - {pageTitle} + {pageTitle}
- - -

{pageTitle}

- - - - - - - - - - {#each tableData as line (line.prefix)} - - - - - - {/each} - -
PrefixUnique BuyersTotal Buys
{line.prefix}{line.unique_buyers || 0}{line.total_buys || 0}
-
- - -
Last refreshed: {lastRefreshed}
-
+ +

{pageTitle}

+ + + + + + + + + + {#each tableData as line (line.prefix)} + + + + + + {/each} + +
PrefixUnique BuyersTotal Buys
{line.prefix}{line.unique_buyers || 0}{line.total_buys || 0}
+
+ + +
Last refreshed: {lastRefreshed}
+
diff --git a/client/src/routes/search/tickets/+page.server.js b/client/src/routes/search/tickets/+page.server.js new file mode 100644 index 0000000..2a0e806 --- /dev/null +++ b/client/src/routes/search/tickets/+page.server.js @@ -0,0 +1,10 @@ +export const load = async ({ fetch }) => { + const prefix = { + prefix: '', + color: 'gray', + weight: 0 + }; + const res = await fetch('/api/prefixes'); + const prefixes = await res.json(); + return { prefix, prefixes }; +}; diff --git a/client/src/routes/search/tickets/+page.svelte b/client/src/routes/search/tickets/+page.svelte new file mode 100644 index 0000000..5888cae --- /dev/null +++ b/client/src/routes/search/tickets/+page.svelte @@ -0,0 +1,233 @@ + + + + {pageTitle} + + + + + + + + + {#each headers as header (header)} + + {/each} + + + + {#each items as item, idx (item.t_id)} + { + changeIdx(idx); + e.target.scrollIntoView({ block: 'center' }); + }} + > + + + + + + + + + {:else} + + + + {/each} + +
+ +

{pageTitle}

+ +
{header}
{item.prefix}{item.t_id} (item.changed = true)} + bind:value={item.first_name} + /> (item.changed = true)} + bind:value={item.last_name} + /> (item.changed = true)} + bind:value={item.phone_number} + />
+ No rows loaded. Please use the pager at the top to put in the first, then last number on + the sheet, click Go, and that should load in the sheet. +
diff --git a/client/src/routes/settings/+page.svelte b/client/src/routes/settings/+page.svelte index cf438e0..f637778 100644 --- a/client/src/routes/settings/+page.svelte +++ b/client/src/routes/settings/+page.svelte @@ -23,30 +23,36 @@
- -
Settings Sections:
- {#if data.settings.remote_server} - Auth Keys - {/if} - Prefixes -
+ +
Settings Sections:
+ {#if data.settings.remote_server} + Auth Keys + {/if} + Prefixes +

{pageTitle}

Remote Mode:

Remote Server:
- +
Remote Port:
- +
Remote TLS:
@@ -64,7 +70,7 @@
Venue Name:
- +
- +

{status.message}

diff --git a/client/src/routes/settings/auth-keys/+page.svelte b/client/src/routes/settings/auth-keys/+page.svelte index d8a90ed..d77520a 100644 --- a/client/src/routes/settings/auth-keys/+page.svelte +++ b/client/src/routes/settings/auth-keys/+page.svelte @@ -14,6 +14,21 @@ let authKeys = $state([]); let newDesc = $state(''); let curKey = $state(''); + + async function getKeys() { + const res = await fetch('/api/auth', { + headers: { 'TAM-PWD': auth.pwd } + }); + if (res.ok) { + auth.toggle = true; + const resData = await res.json(); + authKeys = [...resData]; + curKey = data.authKey; + } else { + auth.pwd = 'Invalid Password!'; + setTimeout(() => (auth.pwd = ''), 3000); + } + } @@ -21,33 +36,17 @@
- - Back to Settings -
Other Settings:
- Prefixes -
+ + Back to Settings +
Other Settings:
+ Prefixes +

{pageTitle}

{#if !auth.toggle}
Password:
- - + +
{:else} @@ -103,7 +102,9 @@ {/each} - + {:else} - - - + + + {/each}
New + import { bS, iS } from '$lib/client/styles'; + import HeaderBar from '$lib/client/components/HeaderBar.svelte'; + + const pageTitle = 'Print Ticket Sheets | TAM'; + + let inputs = $state({ start: 0, end: 0, perPage: 20, ticketType: '' }); + + let items = $state([]); + + function runSheet() { + items = []; + for (let i = inputs.start; i <= inputs.end; i++) items = [...items, i]; + } + + function resetSheet() { + items = []; + } + + + + {pageTitle} + + + + + + + + + + + {#if inputs.ticketType} + + + + {/if} + + + + + + + + + {#each items as i, idx (i)} + + + + + + + {:else} + + + + {/each} + +
+ +
{pageTitle}
+
+
+
+
Range:
+ +
-
+ +
Per Page:
+ +
Ticket Type:
+ + + +
+

{inputs.ticketType} Ticket Sales

Ticket #NamePhone NumberText?
{i}
No rows created. Please use the form above to create rows.
diff --git a/client/src/routes/tickets/[prefix]/+page.svelte b/client/src/routes/tickets/[prefix]/+page.svelte index c8e5d8f..7685621 100644 --- a/client/src/routes/tickets/[prefix]/+page.svelte +++ b/client/src/routes/tickets/[prefix]/+page.svelte @@ -149,8 +149,7 @@ {#each prefixes as p (p.prefix)} {p.prefix}{p.prefix} {/each} @@ -231,11 +230,12 @@ >
- No rows loaded. Please use the pager at the top to put in the first, then last number on the sheet, click Go, and that should load in the sheet. -
+ No rows loaded. Please use the pager at the top to put in the first, then last number on + the sheet, click Go, and that should load in the sheet. +