diff --git a/api/app/data/baskets.py b/api/app/data/baskets.py
index 6b82614..f7afa0a 100644
--- a/api/app/data/baskets.py
+++ b/api/app/data/baskets.py
@@ -7,8 +7,8 @@ from fastapi import APIRouter, Header
class Basket:
prefix: str
b_id: int
- description: str = ""
- donors: str = ""
+ description: str | None = ""
+ donors: str | None = ""
winning_ticket: int = 0
class BasketRepo(RepoTemplate):
diff --git a/api/app/routers.py b/api/app/routers.py
index 52176af..7104c98 100644
--- a/api/app/routers.py
+++ b/api/app/routers.py
@@ -1,5 +1,6 @@
from fastapi import FastAPI
from system.auth import auth_router
+from system.backuprestore import backuprestore_router
from system.prefixes import prefix_router
from data.tickets import tickets_router
from data.baskets import basket_router
@@ -8,10 +9,11 @@ from data.reports import reports_router
from data.search import search_router
def imp_routers(app: FastAPI):
- app.include_router(auth_router)
- app.include_router(prefix_router)
- app.include_router(tickets_router)
- app.include_router(basket_router)
- app.include_router(drawing_router)
- app.include_router(reports_router)
- app.include_router(search_router)
+ app.include_router(auth_router)
+ app.include_router(prefix_router)
+ app.include_router(tickets_router)
+ app.include_router(basket_router)
+ app.include_router(drawing_router)
+ app.include_router(reports_router)
+ app.include_router(search_router)
+ app.include_router(backuprestore_router)
diff --git a/api/app/system/backuprestore.py b/api/app/system/backuprestore.py
new file mode 100644
index 0000000..973a037
--- /dev/null
+++ b/api/app/system/backuprestore.py
@@ -0,0 +1,37 @@
+from data.baskets import Basket, BasketRepo
+from data.tickets import Ticket, TicketRepo
+from .prefixes import Prefix, PrefixRepo
+from dataclasses import dataclass, field
+from typing import List
+from fastapi import APIRouter, Header
+from .auth import AuthRepo
+
+chunk_size = 300
+
+@dataclass
+class BackupFile:
+ prefixes: List[Prefix] = field(default_factory=list)
+ baskets: List[Basket] = field(default_factory=list)
+ tickets: List[Ticket] = field(default_factory=list)
+
+@dataclass
+class ReturnMessage:
+ message: str
+
+backuprestore_router = APIRouter(prefix="/api/backuprestore")
+
+@backuprestore_router.get("")
+def get_backup_file(tam_key: str = Header("")):
+ AuthRepo().verify_key(tam_key)
+ return BackupFile(prefixes=PrefixRepo().get_all_prefixes(), baskets=BasketRepo().get_all_baskets(), tickets=TicketRepo().get_all_tickets())
+
+@backuprestore_router.post("")
+def post_backup_file(bf: BackupFile, tam_key: str = Header("")):
+ AuthRepo().verify_key(tam_key)
+ for i in range(0, len(bf.prefixes), chunk_size):
+ PrefixRepo().post_prefixes(bf.prefixes[i:i+chunk_size])
+ for i in range(0, len(bf.baskets), chunk_size):
+ BasketRepo().post_baskets(bf.baskets[i:i+chunk_size])
+ for i in range(0, len(bf.tickets), chunk_size):
+ TicketRepo().post_tickets(bf.tickets[i:i+chunk_size])
+ return ReturnMessage(message="Backup file imported successfully.")
diff --git a/client/src/lib/client/components/PagerBar.svelte b/client/src/lib/client/components/PagerBar.svelte
index e6c200d..f033748 100644
--- a/client/src/lib/client/components/PagerBar.svelte
+++ b/client/src/lib/client/components/PagerBar.svelte
@@ -36,9 +36,13 @@
type="number"
id="id_from"
class={iS.normal}
+ title="Alt + Q"
onclick={(e) => {
e.target.select();
}}
+ onchange={() => {
+ if (functions.pagerFromUpdate) functions.pagerFromUpdate();
+ }}
bind:value={pager.idFrom}
/>
-
@@ -46,6 +50,7 @@
type="number"
id="id_to"
class={iS.normal}
+ title="Alt + W"
onclick={(e) => {
e.target.select();
}}
@@ -62,6 +67,7 @@
{#if functions.prevPage}
{
functions.nextPage();
}}>Next Page {
+ const data = {};
+ data.prefixes = await db.select().from(prefixes);
+ data.baskets = await db.select().from(baskets);
+ data.tickets = await db.select().from(tickets);
+ return json(data);
+};
+
+export const POST = async ({ request }) => {
+ const reqData = await request.json();
+ for (let i = 0; i < reqData.prefixes.length; i += chunk_size) {
+ const chunk = reqData.prefixes.slice(i, i + chunk_size);
+ await db
+ .insert(prefixes)
+ .values(chunk)
+ .onConflictDoUpdate({
+ target: prefixes.prefix,
+ set: { color: sql`EXCLUDED.color`, weight: sql`EXCLUDED.weight` }
+ });
+ }
+ for (let i = 0; i < reqData.baskets.length; i += chunk_size) {
+ const chunk = reqData.baskets.slice(i, i + chunk_size);
+ await db
+ .insert(baskets)
+ .values(chunk)
+ .onConflictDoUpdate({
+ target: [baskets.prefix, baskets.b_id],
+ set: {
+ description: sql`EXCLUDED.description`,
+ donors: sql`EXCLUDED.donors`,
+ winning_ticket: sql`EXCLUDED.winning_ticket`
+ }
+ });
+ }
+ for (let i = 0; i < reqData.tickets.length; i += chunk_size) {
+ const chunk = reqData.tickets.slice(i, i + chunk_size);
+ await db
+ .insert(tickets)
+ .values(chunk)
+ .onConflictDoNothing({
+ target: [tickets.prefix, tickets.t_id],
+ set: {
+ first_name: sql`EXCLUDED.first_name`,
+ last_name: sql`EXCLUDED.last_name`,
+ phone_number: sql`EXCLUDED.phone_number`,
+ pref: sql`EXCLUDED.pref`
+ }
+ });
+ }
+ return json({ details: 'Data loaded successfully.' });
+};
diff --git a/client/src/routes/api/backuprestore/push/[target]/+server.js b/client/src/routes/api/backuprestore/push/[target]/+server.js
new file mode 100644
index 0000000..44f0c7b
--- /dev/null
+++ b/client/src/routes/api/backuprestore/push/[target]/+server.js
@@ -0,0 +1,32 @@
+import { db } from '$lib/server/db/index.js';
+import { baskets, prefixes, tickets } from '$lib/server/db/schema';
+import { getPath, getSettings } from '$lib/server/settings/index.js';
+import { error } from '@sveltejs/kit';
+
+export const HEAD = async ({ params }) => {
+ const target = params.target;
+ const pushData = {};
+ if (target === 'prefixes') {
+ pushData.prefixes = await db.select().from(prefixes);
+ } else if (target === 'tickets') {
+ pushData.tickets = await db.select().from(tickets);
+ } else if (target === 'baskets') {
+ pushData.baskets = await db.select().from(baskets);
+ } else {
+ throw error(400, 'Can only push prefixes, tickets, or baskets.');
+ }
+
+ const s = getSettings();
+ if (s.remote_server) {
+ const connStr = getPath(s);
+ const res = await fetch(`${connStr}/api/backuprestore`, {
+ method: 'POST',
+ headers: { 'TAM-KEY': s.remote_key, 'Content-Type': 'application/json' },
+ body: JSON.stringify(pushData)
+ });
+ if (!res.ok) throw error(res.status, res.statusText);
+ return new Response(null, { status: 200, statusText: 'Data pushed successfully.' });
+ } else {
+ throw error(500, 'Server not set.');
+ };
+};
diff --git a/client/src/routes/api/backuprestore/remote/+server.js b/client/src/routes/api/backuprestore/remote/+server.js
new file mode 100644
index 0000000..734b51b
--- /dev/null
+++ b/client/src/routes/api/backuprestore/remote/+server.js
@@ -0,0 +1,36 @@
+import { getPath, getSettings } from '$lib/server/settings';
+import { error, json } from '@sveltejs/kit';
+
+export const GET = async () => {
+ const s = getSettings();
+ if (s.remote_server) {
+ const connStr = getPath(s);
+ try {
+ const res = await fetch(`${connStr}/api/backuprestore`, {
+ headers: { TAM_KEY: s.remote_key }
+ });
+ if (!res.ok) throw error(res.status);
+ const data = await res.json();
+ return json(data);
+ } catch {
+ return json({});
+ }
+ } else {
+ return json({});
+ }
+};
+
+export const POST = async ({ request }) => {
+ const resData = await request.json();
+ const s = getSettings();
+ if (s.remote_server) {
+ const connStr = getPath(s);
+ const res = await fetch(`${connStr}/api/backuprestore`, {
+ method: 'POST',
+ headers: { 'TAM-KEY': s.remote_key, 'Content-Type': 'application/json' },
+ body: JSON.stringify(resData)
+ });
+ if (!res.ok) throw error(res.status);
+ return json([]);
+ }
+};
diff --git a/client/src/routes/baskets/[prefix]/+page.svelte b/client/src/routes/baskets/[prefix]/+page.svelte
index b3bce78..b429507 100644
--- a/client/src/routes/baskets/[prefix]/+page.svelte
+++ b/client/src/routes/baskets/[prefix]/+page.svelte
@@ -21,7 +21,7 @@
curIdx = idx;
const elemIdx = document.getElementById(`${idx}_first`);
if (elemIdx) {
- elemIdx.focus();
+ elemIdx.select();
}
};
@@ -44,6 +44,13 @@
items = [...resData];
setTimeout(() => focusIdx(0));
},
+ pagerFromUpdate(){
+ if (itemsLength === 0) {
+ pager.idTo = pager.idFrom;
+ } else {
+ pager.idTo = pager.idFrom + (itemsLength - 1);
+ }
+ },
save: async () => {
if (itemsBuffer.length > 0) {
const res = await fetch('/api/baskets', {
diff --git a/client/src/routes/drawing/[prefix]/+page.svelte b/client/src/routes/drawing/[prefix]/+page.svelte
index c3056ef..81d9276 100644
--- a/client/src/routes/drawing/[prefix]/+page.svelte
+++ b/client/src/routes/drawing/[prefix]/+page.svelte
@@ -21,7 +21,7 @@
curIdx = idx;
const elemIdx = document.getElementById(`${idx}_first`);
if (elemIdx) {
- elemIdx.focus();
+ elemIdx.select();
}
};
@@ -44,6 +44,13 @@
items = [...resData];
setTimeout(() => focusIdx(0));
},
+ pagerFromUpdate(){
+ if (itemsLength === 0) {
+ pager.idTo = pager.idFrom;
+ } else {
+ pager.idTo = pager.idFrom + (itemsLength - 1);
+ }
+ },
save: async () => {
if (itemsBuffer.length > 0) {
const res = await fetch('/api/drawing', {
diff --git a/client/src/routes/settings/+page.svelte b/client/src/routes/settings/+page.svelte
index f637778..5ec6e14 100644
--- a/client/src/routes/settings/+page.svelte
+++ b/client/src/routes/settings/+page.svelte
@@ -29,6 +29,7 @@
Auth Keys
{/if}
Prefixes
+ Backup/Restore
{pageTitle}
diff --git a/client/src/routes/settings/backuprestore/+page.server.js b/client/src/routes/settings/backuprestore/+page.server.js
new file mode 100644
index 0000000..7fb9c8f
--- /dev/null
+++ b/client/src/routes/settings/backuprestore/+page.server.js
@@ -0,0 +1,6 @@
+import { getSettings } from "$lib/server/settings/index.js"
+
+export const load = async () => {
+ const s = getSettings();
+ return { remoteServer: s.remote_server };
+}
diff --git a/client/src/routes/settings/backuprestore/+page.svelte b/client/src/routes/settings/backuprestore/+page.svelte
new file mode 100644
index 0000000..cd79117
--- /dev/null
+++ b/client/src/routes/settings/backuprestore/+page.svelte
@@ -0,0 +1,131 @@
+
+
+
+ {pageTitle}
+
+
+
+
+ Back to Settings
+ Other Settings
+ Prefixes
+ {#if remoteServer}
+ Auth Keys
+ {/if}
+
+
{pageTitle}
+
+
Backup File Downloads
+
+
+ {#if remoteServer}
+
+ {/if}
+
+
+
+
Backup File Upload
+
+
+
+ {#if remoteServer}
+
+ {/if}
+
+
+ {#if remoteServer}
+
+
Push Data to Server
+
+
+
+
+
+
+ {/if}
+
+ Status: {results}
+
+
diff --git a/client/src/routes/tickets/[prefix]/+page.svelte b/client/src/routes/tickets/[prefix]/+page.svelte
index 7685621..410afbf 100644
--- a/client/src/routes/tickets/[prefix]/+page.svelte
+++ b/client/src/routes/tickets/[prefix]/+page.svelte
@@ -44,6 +44,13 @@
items = [...resData];
setTimeout(() => focusIdx(0));
},
+ pagerFromUpdate(){
+ if (itemsLength === 0) {
+ pager.idTo = pager.idFrom;
+ } else {
+ pager.idTo = pager.idFrom + (itemsLength - 1);
+ }
+ },
save: async () => {
if (itemsBuffer.length > 0) {
const res = await fetch('/api/tickets', {