diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 0000000..f047aa2 --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,2 @@ +__pycache__/ +*/__pycache__/ \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..cc0880d --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3 + +WORKDIR /app +COPY requirements.txt requirements.txt +RUN pip install --no-cache-dir -r requirements.txt +COPY . . + +CMD ["fastapi", "run", "main.py"] \ No newline at end of file diff --git a/api/main.py b/api/main.py index 2732bc3..97dd065 100644 --- a/api/main.py +++ b/api/main.py @@ -9,6 +9,7 @@ from routers.baskets import basket_router from routers.combined import combined_router from routers.reports import report_router from routers.backuprestore import backup_router +from routers.counts import counts_router if argv[1] == "run": app = FastAPI(title="TAM3 API Server", docs_url=None, redoc_url=None) @@ -20,4 +21,5 @@ app.include_router(ticket_router) app.include_router(basket_router) app.include_router(combined_router) app.include_router(report_router) -app.include_router(backup_router) \ No newline at end of file +app.include_router(backup_router) +app.include_router(counts_router) \ No newline at end of file diff --git a/api/routers/counts.py b/api/routers/counts.py new file mode 100644 index 0000000..61b2546 --- /dev/null +++ b/api/routers/counts.py @@ -0,0 +1,28 @@ +from fastapi import APIRouter +from dataclasses import dataclass + +from exceptions import bad_key +from repos.template import Repo +from repos.api_keys import ApiKeyRepo + +@dataclass +class Count: + prefix: str + total_sold: int + unique_sold: int + +class CountRepo(Repo): + def get_counts(self): + self.cur.execute("SELECT * FROM counts") + results = self.cur.fetchall() + return [Count(*r) for r in results] + + + +counts_router = APIRouter(prefix="/api/counts") + +@counts_router.get("/") +def get_ticket_counts(api_key: str): + if not ApiKeyRepo().check_api(api_key): + raise bad_key + return CountRepo().get_counts() \ No newline at end of file diff --git a/db/Dockerfile b/db/Dockerfile new file mode 100644 index 0000000..cb4ee5b --- /dev/null +++ b/db/Dockerfile @@ -0,0 +1,3 @@ +FROM mariadb:lts + +COPY schema.sql /docker-entrypoint-initdb.d/tam3-schema.sql \ No newline at end of file diff --git a/db/compose.yml b/db/compose.yml index 8dc8695..e0d8450 100644 --- a/db/compose.yml +++ b/db/compose.yml @@ -1,6 +1,6 @@ services: db: - image: mariadb:lts + image: dbob16/tam3-db:0.0.1 restart: always hostname: mariadb container_name: mariadb @@ -11,7 +11,6 @@ services: MARIADB_PASSWORD: tam3 volumes: - "tam3-db:/var/lib/mysql" - - "./schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro" ports: - 127.0.0.1:3306:3306 adminer: diff --git a/webapp/.dockerignore b/webapp/.dockerignore new file mode 100644 index 0000000..d57428f --- /dev/null +++ b/webapp/.dockerignore @@ -0,0 +1,2 @@ +/node_modules/ +*.db \ No newline at end of file diff --git a/webapp/Dockerfile b/webapp/Dockerfile new file mode 100644 index 0000000..559b13e --- /dev/null +++ b/webapp/Dockerfile @@ -0,0 +1,23 @@ +FROM node:lts-alpine AS build + +WORKDIR /app +COPY . . +RUN npm install && npm run build + +FROM node:lts-alpine AS prod + +WORKDIR /data +WORKDIR /app + +ENV DATABASE_URL=file:/data/local.db + +COPY --from=build /app/build/. build/. +COPY --from=build /app/package.json /app/drizzle.config.js . +COPY --from=build /app/drizzle drizzle +COPY deploy/start-server.sh start-server.sh + +RUN npm install --production && npm install drizzle-kit + +EXPOSE 3000 + +CMD ["sh", "start-server.sh"] \ No newline at end of file diff --git a/webapp/deploy/start-server.sh b/webapp/deploy/start-server.sh new file mode 100755 index 0000000..99c3d80 --- /dev/null +++ b/webapp/deploy/start-server.sh @@ -0,0 +1,2 @@ +npx drizzle-kit migrate +node build \ No newline at end of file diff --git a/webapp/drizzle/0000_init.sql b/webapp/drizzle/0000_init.sql new file mode 100644 index 0000000..2a131c3 --- /dev/null +++ b/webapp/drizzle/0000_init.sql @@ -0,0 +1,39 @@ +CREATE TABLE `baskets` ( + `prefix` text, + `b_id` integer, + `description` text DEFAULT '', + `donors` text DEFAULT '', + `winning_ticket` integer DEFAULT 0, + PRIMARY KEY(`prefix`, `b_id`) +); +--> statement-breakpoint +CREATE TABLE `prefixes` ( + `name` text PRIMARY KEY NOT NULL, + `color` text, + `weight` integer DEFAULT 0 +); +--> statement-breakpoint +CREATE TABLE `tickets` ( + `prefix` text, + `t_id` integer, + `first_name` text DEFAULT '', + `last_name` text DEFAULT '', + `phone_number` text DEFAULT '', + `preference` text DEFAULT 'CALL', + PRIMARY KEY(`prefix`, `t_id`) +); +--> statement-breakpoint +CREATE VIEW `combined` AS SELECT b.prefix, b.b_id, b.winning_ticket, CONCAT(t.last_name, ', ', t.first_name) AS winner + 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 `counts` AS SELECT prefix, COUNT(*) AS total_sold, COUNT(DISTINCT CONCAT(first_name,last_name,phone_number)) AS unique_sold + FROM tickets + GROUP BY prefix + UNION ALL + SELECT 'Total' AS prefix, COUNT(*) AS total_sold, COUNT(DISTINCT CONCAT(first_name,last_name,phone_number)) AS unique_sold + FROM tickets;;--> statement-breakpoint +CREATE VIEW `report` AS SELECT b.prefix, CONCAT(t.last_name, ', ', t.first_name) AS winner_name, t.phone_number, t.preference, b.b_id, b.winning_ticket, b.description + FROM baskets b LEFT JOIN tickets t + ON b.prefix = t.prefix AND b.winning_ticket = t.t_id + ORDER BY b.prefix, winner_name, t.phone_number, t.preference, b.b_id; \ No newline at end of file diff --git a/webapp/drizzle/meta/0000_snapshot.json b/webapp/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..1277737 --- /dev/null +++ b/webapp/drizzle/meta/0000_snapshot.json @@ -0,0 +1,290 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b981beaa-405c-48f1-8006-35f66d264062", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "baskets": { + "name": "baskets", + "columns": { + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "b_id": { + "name": "b_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "donors": { + "name": "donors", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "winning_ticket": { + "name": "winning_ticket", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "baskets_prefix_b_id_pk": { + "columns": [ + "prefix", + "b_id" + ], + "name": "baskets_prefix_b_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "prefixes": { + "name": "prefixes", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tickets": { + "name": "tickets", + "columns": { + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "t_id": { + "name": "t_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "preference": { + "name": "preference", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CALL'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tickets_prefix_t_id_pk": { + "columns": [ + "prefix", + "t_id" + ], + "name": "tickets_prefix_t_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": { + "combined": { + "columns": { + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "b_id": { + "name": "b_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "winning_ticket": { + "name": "winning_ticket", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "winner": { + "name": "winner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "name": "combined", + "isExisting": false, + "definition": "SELECT b.prefix, b.b_id, b.winning_ticket, CONCAT(t.last_name, ', ', t.first_name) AS winner\n\tFROM baskets b LEFT JOIN tickets t\n\tON b.prefix = t.prefix AND b.winning_ticket = t.t_id\n\tORDER BY b.prefix, b.b_id" + }, + "counts": { + "columns": { + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "total_sold": { + "name": "total_sold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unique_sold": { + "name": "unique_sold", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "name": "counts", + "isExisting": false, + "definition": "SELECT prefix, COUNT(*) AS total_sold, COUNT(DISTINCT CONCAT(first_name,last_name,phone_number)) AS unique_sold\n\tFROM tickets\n\tGROUP BY prefix\n\tUNION ALL\n\tSELECT 'Total' AS prefix, COUNT(*) AS total_sold, COUNT(DISTINCT CONCAT(first_name,last_name,phone_number)) AS unique_sold\n\tFROM tickets;" + }, + "report": { + "columns": { + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "winner_name": { + "name": "winner_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "preference": { + "name": "preference", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "b_id": { + "name": "b_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "winning_ticket": { + "name": "winning_ticket", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "name": "report", + "isExisting": false, + "definition": "SELECT b.prefix, CONCAT(t.last_name, ', ', t.first_name) AS winner_name, t.phone_number, \tt.preference, b.b_id, b.winning_ticket, b.description\n\tFROM baskets b LEFT JOIN tickets t\n\tON b.prefix = t.prefix AND b.winning_ticket = t.t_id\n\tORDER BY b.prefix, winner_name, t.phone_number, t.preference, b.b_id" + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/webapp/drizzle/meta/_journal.json b/webapp/drizzle/meta/_journal.json new file mode 100644 index 0000000..3421f7e --- /dev/null +++ b/webapp/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1759085800774, + "tag": "0000_init", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/webapp/package.json b/webapp/package.json index 4e72130..4187848 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "hotkeys-js": "^3.13.15", + "drizzle-kit": "^0.30.2", "@libsql/client": "^0.14.0" } } diff --git a/webapp/src/lib/assets/favicon.svg b/webapp/src/lib/assets/favicon.svg index cc5dc66..eeeee17 100644 --- a/webapp/src/lib/assets/favicon.svg +++ b/webapp/src/lib/assets/favicon.svg @@ -1 +1,22 @@ -svelte-logo \ No newline at end of file + + + + + + + + + diff --git a/webapp/src/lib/css/main.css b/webapp/src/lib/css/main.css index a0a9792..5f02a45 100644 --- a/webapp/src/lib/css/main.css +++ b/webapp/src/lib/css/main.css @@ -30,6 +30,7 @@ a.styled:hover, button.styled:hover { display: flex; flex-direction: row; flex-wrap: wrap; + align-items: center; gap: 0.75rem; } diff --git a/webapp/src/routes/+page.svelte b/webapp/src/routes/+page.svelte index e629a2e..3dbd907 100644 --- a/webapp/src/routes/+page.svelte +++ b/webapp/src/routes/+page.svelte @@ -2,6 +2,7 @@ import { browser } from "$app/environment"; import { env } from "$env/dynamic/public"; import hotkeys from "hotkeys-js"; + import favicon from "$lib/assets/favicon.svg" let { data } = $props(); let prefix_name = $state(""); @@ -31,7 +32,16 @@