nightly - 2025-09-25
This commit is contained in:
@@ -5,6 +5,8 @@ from sys import argv
|
|||||||
|
|
||||||
from routers.prefixes import prefix_router
|
from routers.prefixes import prefix_router
|
||||||
from routers.tickets import ticket_router
|
from routers.tickets import ticket_router
|
||||||
|
from routers.baskets import basket_router
|
||||||
|
from routers.combined import combined_router
|
||||||
|
|
||||||
if argv[1] == "run":
|
if argv[1] == "run":
|
||||||
app = FastAPI(title="TAM3 API Server", docs_url=None, redoc_url=None)
|
app = FastAPI(title="TAM3 API Server", docs_url=None, redoc_url=None)
|
||||||
@@ -13,3 +15,5 @@ else:
|
|||||||
|
|
||||||
app.include_router(prefix_router)
|
app.include_router(prefix_router)
|
||||||
app.include_router(ticket_router)
|
app.include_router(ticket_router)
|
||||||
|
app.include_router(basket_router)
|
||||||
|
app.include_router(combined_router)
|
||||||
43
api/repos/baskets.py
Normal file
43
api/repos/baskets.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from .template import Repo
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Basket:
|
||||||
|
prefix: str
|
||||||
|
b_id: int
|
||||||
|
description: str = ""
|
||||||
|
donors: str = ""
|
||||||
|
winning_ticket: int = 0
|
||||||
|
changed: bool = False
|
||||||
|
|
||||||
|
class BasketRepo(Repo):
|
||||||
|
def get_prefix_one(self, prefix: str, b_id: int):
|
||||||
|
self.cur.execute("SELECT * FROM baskets WHERE prefix = %s AND b_id = %s", (prefix, b_id))
|
||||||
|
result = self.cur.fetchone()
|
||||||
|
if not result:
|
||||||
|
return Basket(prefix, b_id)
|
||||||
|
return Basket(*result)
|
||||||
|
def get_prefix_range(self, prefix: str, b_from: int, b_to: int):
|
||||||
|
r_dict = {i: Basket(prefix, i) for i in range(b_from, b_to+1)}
|
||||||
|
self.cur.execute("SELECT * FROM baskets WHERE prefix = %s AND b_id BETWEEN %s AND %s", (prefix, b_from, b_to))
|
||||||
|
results = self.cur.fetchall()
|
||||||
|
for r in results:
|
||||||
|
r_dict[r[1]] = Basket(*r)
|
||||||
|
return list(r_dict.values())
|
||||||
|
def get_prefix_all(self, prefix: str):
|
||||||
|
self.cur.execute("SELECT * FROM baskets WHERE prefix = %s", (prefix,))
|
||||||
|
results = self.cur.fetchall()
|
||||||
|
if not results:
|
||||||
|
return []
|
||||||
|
return [Basket(*r) for r in results]
|
||||||
|
def get_all(self):
|
||||||
|
self.cur.execute("SELECT * FROM baskets")
|
||||||
|
results = self.cur.fetchall()
|
||||||
|
if not results:
|
||||||
|
return []
|
||||||
|
return [Basket(*r) for r in results]
|
||||||
|
def post_list(self, baskets: list[Basket]):
|
||||||
|
for b in baskets:
|
||||||
|
self.cur.execute("REPLACE INTO baskets VALUES (%s, %s, %s, %s, %s)", (b.prefix, b.b_id, b.description, b.donors, b.winning_ticket))
|
||||||
|
self.conn.commit()
|
||||||
|
return {"detail": "Baskets posted successfully."}
|
||||||
43
api/repos/combined.py
Normal file
43
api/repos/combined.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from .template import Repo
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Combined:
|
||||||
|
prefix: str
|
||||||
|
b_id: int
|
||||||
|
winning_ticket: int = 0
|
||||||
|
winner: str = ", "
|
||||||
|
changed: bool = False
|
||||||
|
|
||||||
|
class CombinedRepo(Repo):
|
||||||
|
def get_prefix_one(self, prefix: str, b_id: int) -> Combined:
|
||||||
|
self.cur.execute("SELECT * FROM combined WHERE prefix = %s AND b_id = %s", (prefix, b_id))
|
||||||
|
result = self.cur.fetchone()
|
||||||
|
if not result:
|
||||||
|
return Combined(prefix, b_id)
|
||||||
|
return Combined(*result)
|
||||||
|
def get_prefix_range(self, prefix: str, b_from: int, b_to: int) -> list[Combined]:
|
||||||
|
r_dict = {i: Combined(prefix, i) for i in range(b_from, b_to+1)}
|
||||||
|
self.cur.execute("SELECT * FROM combined WHERE prefix = %s AND b_id BETWEEN %s AND %s", (prefix, b_from, b_to))
|
||||||
|
results = self.cur.fetchall()
|
||||||
|
for b in results:
|
||||||
|
r_dict[b[1]] = Combined(*b)
|
||||||
|
return list(r_dict.values())
|
||||||
|
def get_prefix_all(self, prefix:str) -> list[Combined]:
|
||||||
|
self.cur.execute("SELECT * FROM combined WHERE prefix = %s", (prefix,))
|
||||||
|
results = self.cur.fetchall()
|
||||||
|
if not results:
|
||||||
|
return []
|
||||||
|
return [Combined(*r) for r in results]
|
||||||
|
def get_all(self) -> list[Combined]:
|
||||||
|
self.cur.execute("SELECT * FROM combined")
|
||||||
|
results = self.cur.fetchall()
|
||||||
|
if not results:
|
||||||
|
return []
|
||||||
|
return [Combined(*r) for r in results]
|
||||||
|
def post_list(self, c_entries: list[Combined]):
|
||||||
|
for combined in c_entries:
|
||||||
|
self.cur.execute("INSERT INTO baskets (prefix, b_id, winning_ticket) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE winning_ticket=%s",
|
||||||
|
(combined.prefix, combined.b_id, combined.winning_ticket, combined.winning_ticket))
|
||||||
|
self.conn.commit()
|
||||||
|
return {"detail": "Winners posted successfully"}
|
||||||
36
api/routers/baskets.py
Normal file
36
api/routers/baskets.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from exceptions import bad_key
|
||||||
|
from repos.api_keys import ApiKeyRepo
|
||||||
|
from repos.baskets import Basket, BasketRepo
|
||||||
|
|
||||||
|
basket_router = APIRouter(prefix="/api/baskets")
|
||||||
|
|
||||||
|
@basket_router.get("/")
|
||||||
|
def get_all_baskets(api_key: str) -> list[Basket]:
|
||||||
|
if not ApiKeyRepo().check_api(api_key):
|
||||||
|
raise bad_key
|
||||||
|
return BasketRepo().get_all()
|
||||||
|
|
||||||
|
@basket_router.get("/{prefix}/")
|
||||||
|
def get_prefix_baskets(api_key: str, prefix: str) -> list[Basket]:
|
||||||
|
if not ApiKeyRepo().check_api(api_key):
|
||||||
|
raise bad_key
|
||||||
|
return BasketRepo().get_prefix_all(prefix)
|
||||||
|
|
||||||
|
@basket_router.get("/{prefix}/{b_id}/")
|
||||||
|
def get_prefix_basket_one(api_key: str, prefix: str, b_id: int) -> Basket:
|
||||||
|
if not ApiKeyRepo().check_api(api_key):
|
||||||
|
raise bad_key
|
||||||
|
return BasketRepo().get_prefix_one(prefix, b_id)
|
||||||
|
|
||||||
|
@basket_router.get("/{prefix}/{b_from}/{b_to}/")
|
||||||
|
def get_prefix_basket_range(api_key: str, prefix: str, b_from: int, b_to: int) -> list[Basket]:
|
||||||
|
if not ApiKeyRepo().check_api(api_key):
|
||||||
|
raise bad_key
|
||||||
|
return BasketRepo().get_prefix_range(prefix, b_from, b_to)
|
||||||
|
|
||||||
|
@basket_router.post("/")
|
||||||
|
def post_basket_list(api_key: str, baskets: list[Basket]):
|
||||||
|
if not ApiKeyRepo().check_api(api_key):
|
||||||
|
raise bad_key
|
||||||
|
return BasketRepo().post_list(baskets)
|
||||||
36
api/routers/combined.py
Normal file
36
api/routers/combined.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from repos.combined import Combined, CombinedRepo
|
||||||
|
from repos.api_keys import ApiKeyRepo
|
||||||
|
from exceptions import bad_key
|
||||||
|
|
||||||
|
combined_router = APIRouter(prefix="/api/combined")
|
||||||
|
|
||||||
|
@combined_router.get("/")
|
||||||
|
def get_all_combined(api_key: str) -> list[Combined]:
|
||||||
|
if not ApiKeyRepo().check_api(api_key):
|
||||||
|
raise bad_key
|
||||||
|
return CombinedRepo().get_all()
|
||||||
|
|
||||||
|
@combined_router.get("/{prefix}/")
|
||||||
|
def get_prefix_combined(api_key: str, prefix: str) -> list[Combined]:
|
||||||
|
if not ApiKeyRepo().check_api(api_key):
|
||||||
|
raise bad_key
|
||||||
|
return CombinedRepo().get_prefix_all(prefix)
|
||||||
|
|
||||||
|
@combined_router.get("/{prefix}/{b_id}/")
|
||||||
|
def get_prefix_combined_one(api_key: str, prefix: str, b_id: int) -> Combined:
|
||||||
|
if not ApiKeyRepo().check_api(api_key):
|
||||||
|
raise bad_key
|
||||||
|
return CombinedRepo().get_prefix_one(prefix, b_id)
|
||||||
|
|
||||||
|
@combined_router.get("/{prefix}/{b_from}/{b_to}/")
|
||||||
|
def get_prefix_combined_range(api_key: str, prefix: str, b_from: int, b_to: int) -> list[Combined]:
|
||||||
|
if not ApiKeyRepo().check_api(api_key):
|
||||||
|
raise bad_key
|
||||||
|
return CombinedRepo().get_prefix_range(prefix, b_from, b_to)
|
||||||
|
|
||||||
|
@combined_router.post("/")
|
||||||
|
def post_combined_range(api_key: str, winner_list: list[Combined]):
|
||||||
|
if not ApiKeyRepo().check_api(api_key):
|
||||||
|
raise bad_key
|
||||||
|
return CombinedRepo().post_list(winner_list)
|
||||||
@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS api_keys (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS prefixes (
|
CREATE TABLE IF NOT EXISTS prefixes (
|
||||||
`name` VARCHAR(255),
|
`name` VARCHAR(255) PRIMARY KEY,
|
||||||
`color` VARCHAR(255),
|
`color` VARCHAR(255),
|
||||||
`weight` INT DEFAULT 0
|
`weight` INT DEFAULT 0
|
||||||
);
|
);
|
||||||
@@ -24,7 +24,8 @@ CREATE TABLE IF NOT EXISTS baskets (
|
|||||||
`b_id` INT,
|
`b_id` INT,
|
||||||
`description` VARCHAR(255),
|
`description` VARCHAR(255),
|
||||||
`donors` VARCHAR(255),
|
`donors` VARCHAR(255),
|
||||||
`winning_ticket` INT
|
`winning_ticket` INT,
|
||||||
|
PRIMARY KEY (`prefix`, `b_id`)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE VIEW IF NOT EXISTS combined AS
|
CREATE VIEW IF NOT EXISTS combined AS
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import hotkeys from "hotkeys-js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
prefix,
|
prefix,
|
||||||
pagerForm = $bindable(),
|
pagerForm = $bindable(),
|
||||||
functions
|
functions
|
||||||
} = $props()
|
} = $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>
|
</script>
|
||||||
|
|
||||||
<div id="formheader" class="{prefix.color}">
|
<div id="formheader" class="{prefix.color}">
|
||||||
@@ -15,21 +31,21 @@
|
|||||||
<button class="styled" onclick={functions.refreshPage}>Refresh</button>
|
<button class="styled" onclick={functions.refreshPage}>Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<button class="styled" tabindex="-1" onclick={functions.prevPage}>Prev Page</button>
|
<button class="styled" title="Alt + B" tabindex="-1" onclick={functions.prevPage}>Prev Page</button>
|
||||||
<button class="styled" tabindex="-1" onclick={functions.nextPage}>Next Page</button>
|
<button class="styled" title="Alt + N" tabindex="-1" onclick={functions.nextPage}>Next Page</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-row-space tb-margin">
|
<div class="flex-row-space tb-margin">
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<button class="styled" tabindex="-1" onclick={functions.duplicateDown}>Duplicate Down</button>
|
<button class="styled" title="Alt + J" tabindex="-1" onclick={functions.duplicateDown}>Duplicate Down</button>
|
||||||
<button class="styled" tabindex="-1" onclick={functions.duplicateUp}>Duplicate Up</button>
|
<button class="styled" title="Alt + U" tabindex="-1" onclick={functions.duplicateUp}>Duplicate Up</button>
|
||||||
<button class="styled" tabindex="-1" onclick={functions.gotoNext}>Next</button>
|
<button class="styled" title="Alt + L" tabindex="-1" onclick={functions.gotoNext}>Next</button>
|
||||||
<button class="styled" tabindex="-1" onclick={functions.gotoPrev}>Previous</button>
|
<button class="styled" title="Alt + O" tabindex="-1" onclick={functions.gotoPrev}>Previous</button>
|
||||||
<button class="styled" tabindex="-1" onclick={functions.copy}>Copy</button>
|
<button class="styled" title="Alt + C" tabindex="-1" onclick={functions.copy}>Copy</button>
|
||||||
<button class="styled" tabindex="-1" onclick={functions.paste}>Paste</button>
|
<button class="styled" title="Alt + V" tabindex="-1" onclick={functions.paste}>Paste</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<button class="styled" tabindex="-1" onclick={functions.saveAll}>Save All</button>
|
<button class="styled" title="Alt + S" tabindex="-1" onclick={functions.saveAll}>Save All</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
43
webapp/src/routes/api/baskets/+server.js
Normal file
43
webapp/src/routes/api/baskets/+server.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { env } from "$env/dynamic/private";
|
||||||
|
import { db } from "$lib/server/db";
|
||||||
|
import { baskets } from "$lib/server/db/schema";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
if (env.TAM3_REMOTE) {
|
||||||
|
const res = await fetch(`${env.TAM3_REMOTE}/api/baskets/?api_key=${env.TAM3_REMOTE_KEY}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
return new Response(JSON.stringify({"detail": "Unable to fetch Baskets"}), {status: res.status, statusText: res.statusText})
|
||||||
|
};
|
||||||
|
const data = await res.json();
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
status: 200,
|
||||||
|
statusText: "Baskets fetched successfully."
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const data = await db.select().from(baskets);
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
status: 200,
|
||||||
|
statusText: "Baskets loaded successfully."
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST({ request }) {
|
||||||
|
const i_baskets = await request.json();
|
||||||
|
for (let basket of i_baskets) {
|
||||||
|
await db.insert(baskets).values({prefix: basket.prefix, b_id: basket.b_id, description: basket.description, donors: basket.donors, winning_ticket: basket.winning_ticket})
|
||||||
|
.onConflictDoUpdate({target: [baskets.prefix, baskets.b_id], set: {description: basket.description, donors: basket.donors, winning_ticket: basket.winning_ticket}})
|
||||||
|
};
|
||||||
|
if (env.TAM3_REMOTE) {
|
||||||
|
const res = await fetch(`${env.TAM3_REMOTE}/api/baskets/?api_key=${env.TAM3_REMOTE_KEY}`, {
|
||||||
|
body: JSON.stringify([...i_baskets]), method: 'POST', headers: {'Content-Type': 'application/json'}
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
return new Response(JSON.stringify({details: "Issue posting baskets to remote."}), {status: res.status, statusText: res.statusText})
|
||||||
|
};
|
||||||
|
const data = await res.json();
|
||||||
|
};
|
||||||
|
return new Response(JSON.stringify({details: "Posted baskets successfully."}), {status: 200, statusText: "Posted baskets successfully."})
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { env } from "$env/dynamic/private";
|
||||||
|
import { db } from "$lib/server/db";
|
||||||
|
import { baskets } from "$lib/server/db/schema";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
|
||||||
|
export async function GET({ params }) {
|
||||||
|
let n_b_from = parseInt(params.b_from), n_b_to = parseInt(params.b_to);
|
||||||
|
if (env.TAM3_REMOTE) {
|
||||||
|
const res = await fetch(`${env.TAM3_REMOTE}/api/baskets/${n_b_from}/${n_b_to}/?api_key=${env.TAM3_REMOTE_KEY}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
return new Response(JSON.stringify({detail: "Unable to fetch baskets"}), {status: res.status, statusText: res.statusText});
|
||||||
|
};
|
||||||
|
const data = await res.json();
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
status: 200,
|
||||||
|
statusText: "Baskets fetched successfully"
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let r_dict = {};
|
||||||
|
for (let i=n_b_from; i <= n_b_to; i++) {
|
||||||
|
let data = await db.select().from(baskets).where(and(eq(baskets.prefix, params.prefix), eq(baskets.b_id, i)));
|
||||||
|
if (data[0]) {
|
||||||
|
r_dict[i] = {...data[0]};
|
||||||
|
} else {
|
||||||
|
r_dict[i] = {prefix: params.prefix, b_id: i, description: "", donors: "", winning_ticket: 0, changed: false};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return new Response(JSON.stringify(Object.values(r_dict)), {
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
status: 200,
|
||||||
|
statusText: "Baskets loaded successfully."
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,6 @@ export async function GET() {
|
|||||||
|
|
||||||
export async function POST({ request }) {
|
export async function POST({ request }) {
|
||||||
const { name, color, weight } = await request.json();
|
const { name, color, weight } = await request.json();
|
||||||
console.log({name, color, weight})
|
|
||||||
await db.insert(prefixes).values({name: name, color: color, weight: weight}).onConflictDoUpdate({target: prefixes.name, set: {color: color, weight: weight}});
|
await db.insert(prefixes).values({name: name, color: color, weight: weight}).onConflictDoUpdate({target: prefixes.name, set: {color: color, weight: weight}});
|
||||||
if (env.TAM3_REMOTE) {
|
if (env.TAM3_REMOTE) {
|
||||||
const res = await fetch(`${env.TAM3_REMOTE}/api/prefixes/?api_key=${env.TAM3_REMOTE_KEY}`, {
|
const res = await fetch(`${env.TAM3_REMOTE}/api/prefixes/?api_key=${env.TAM3_REMOTE_KEY}`, {
|
||||||
|
|||||||
6
webapp/src/routes/baskets/[prefix]/+page.js
Normal file
6
webapp/src/routes/baskets/[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}
|
||||||
|
}
|
||||||
141
webapp/src/routes/baskets/[prefix]/+page.svelte
Normal file
141
webapp/src/routes/baskets/[prefix]/+page.svelte
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<script>
|
||||||
|
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});
|
||||||
|
let current_idx = $state(0);
|
||||||
|
let current_baskets = $state([]);
|
||||||
|
let copy_buffer = $state({prefix: prefix.name, b_id: 0, description: "", donors: "", winning_ticket: 0});
|
||||||
|
|
||||||
|
function changeFocus(idx) {
|
||||||
|
const focusDe = document.getElementById(`${idx}_de`);
|
||||||
|
if (focusDe) {
|
||||||
|
focusDe.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const functions = {
|
||||||
|
refreshPage: async () => {
|
||||||
|
if (current_baskets.length > 0) {
|
||||||
|
functions.saveAll()
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prevPage: () => {
|
||||||
|
const diff = current_baskets.length;
|
||||||
|
pagerForm.id_from = pagerForm.id_from - diff;
|
||||||
|
pagerForm.id_to = pagerForm.id_to - diff;
|
||||||
|
functions.refreshPage();
|
||||||
|
},
|
||||||
|
nextPage: () => {
|
||||||
|
const diff = current_baskets.length;
|
||||||
|
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_baskets[next_idx]) {
|
||||||
|
current_baskets[next_idx] = {...current_baskets[current_idx], b_id: current_baskets[next_idx].b_id, changed: true};
|
||||||
|
changeFocus(next_idx);
|
||||||
|
} else {
|
||||||
|
changeFocus(next_idx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
duplicateUp: () => {
|
||||||
|
const prev_idx = current_idx - 1;
|
||||||
|
if (prev_idx >= 0) {
|
||||||
|
current_baskets[prev_idx] = {...current_baskets[current_idx], b_id: current_baskets[prev_idx].b_id, changed: true};
|
||||||
|
changeFocus(prev_idx);
|
||||||
|
} else {
|
||||||
|
changeFocus(prev_idx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gotoNext: () => {
|
||||||
|
const next_idx = current_idx + 1;
|
||||||
|
if (current_baskets[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_baskets[current_idx]};
|
||||||
|
},
|
||||||
|
paste: () => {
|
||||||
|
current_baskets[current_idx] = {...copy_buffer, b_id: current_baskets[current_idx].b_id, changed: true}
|
||||||
|
},
|
||||||
|
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'}});
|
||||||
|
if (res.ok) {
|
||||||
|
for (let basket of current_baskets) {basket.changed = false};
|
||||||
|
changeFocus(0);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
document.title = `${prefix.name} Basket Entry`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>{prefix.name} Basket Entry</h1>
|
||||||
|
<FormHeader {prefix} {functions} bind:pagerForm />
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 12ch">Basket ID</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Donors</th>
|
||||||
|
<th>Changed</th>
|
||||||
|
</tr>
|
||||||
|
</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>
|
||||||
|
{/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>
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import FormHeader from '$lib/components/FormHeader.svelte';
|
import FormHeader from '$lib/components/FormHeader.svelte';
|
||||||
import hotkeys from 'hotkeys-js';
|
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
let prefix = {...data.prefix};
|
let prefix = {...data.prefix};
|
||||||
@@ -30,13 +29,13 @@
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
prevPage: () => {
|
prevPage: () => {
|
||||||
const diff = pagerForm.id_to - pagerForm.id_from + 1;
|
const diff = current_tickets.length;
|
||||||
pagerForm.id_from = pagerForm.id_from - diff;
|
pagerForm.id_from = pagerForm.id_from - diff;
|
||||||
pagerForm.id_to = pagerForm.id_to - diff;
|
pagerForm.id_to = pagerForm.id_to - diff;
|
||||||
functions.refreshPage()
|
functions.refreshPage()
|
||||||
},
|
},
|
||||||
nextPage: () => {
|
nextPage: () => {
|
||||||
const diff = pagerForm.id_to - pagerForm.id_from + 1;
|
const diff = current_tickets.length;
|
||||||
pagerForm.id_from = pagerForm.id_from + diff;
|
pagerForm.id_from = pagerForm.id_from + diff;
|
||||||
pagerForm.id_to = pagerForm.id_to + diff;
|
pagerForm.id_to = pagerForm.id_to + diff;
|
||||||
functions.refreshPage()
|
functions.refreshPage()
|
||||||
@@ -87,7 +86,7 @@
|
|||||||
const to_save = current_tickets.filter((ticket) => ticket.changed === true);
|
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 res = await fetch(`/api/tickets`, {body: JSON.stringify(to_save), method: 'POST', headers: {'Content-Type': 'application/json'}});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
current_tickets.map((ticket) => ticket.changed = false);
|
for (let ticket of current_tickets) {ticket.changed = false};
|
||||||
changeFocus(0);
|
changeFocus(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,16 +94,6 @@
|
|||||||
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
document.title = `${prefix.name} Ticket Entry`
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user