From 3885082d2f11c776e07a0f740c573dac7f14170e Mon Sep 17 00:00:00 2001 From: Patrick Date: Fri, 3 Jan 2025 01:04:30 +0100 Subject: [PATCH] implemented update, moved input to component --- .gitignore | 3 + bun.lockb | Bin 39124 -> 47411 bytes docs/UserDatabase.uml | 10 + docs/UserEntryDatabase.svg | 1 + docs/UserEntryDatabase.uml | 28 ++ package.json | 25 +- src/hooks.server.ts | 38 ++ src/lib/db_types.ts | 20 + src/lib/server/database.ts | 233 ++++++++++ src/lib/util.ts | 89 ++++ src/routes/+page.server.ts | 172 ++++++++ src/routes/+page.svelte | 666 +++++++---------------------- src/routes/record_input_row.svelte | 623 +++++++++++++++++++++++++++ svelte.config.js | 4 +- 14 files changed, 1383 insertions(+), 529 deletions(-) create mode 100644 docs/UserDatabase.uml create mode 100644 docs/UserEntryDatabase.svg create mode 100644 docs/UserEntryDatabase.uml create mode 100644 src/hooks.server.ts create mode 100644 src/lib/db_types.ts create mode 100644 src/lib/server/database.ts create mode 100644 src/lib/util.ts create mode 100644 src/routes/+page.server.ts create mode 100644 src/routes/record_input_row.svelte diff --git a/.gitignore b/.gitignore index 3b462cb..eaa6c19 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Databases +*.sqlite diff --git a/bun.lockb b/bun.lockb index 215688af8d77309056fa77b7140ee4e205480751..9d988b5b0a78eec2a5043f46e3a6e4b4e8299a33 100755 GIT binary patch delta 10539 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 delta 5390 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 zm}eXF#recordsid: primarydate: varchar(10) [YYYY-MM-DD]start: varchar(5) [HH:MM]end: varchar(5) [HH:MM] created: timestampmodified: timestampmodified_to: records::idestimatesid: primarydate: varchar(7) [YYYY-MM]estimate: real created: timestampmodified: timestampmodified_to: estimates::id \ No newline at end of file diff --git a/docs/UserEntryDatabase.uml b/docs/UserEntryDatabase.uml new file mode 100644 index 0000000..3644cab --- /dev/null +++ b/docs/UserEntryDatabase.uml @@ -0,0 +1,28 @@ +@startuml + +skinparam linetype ortho + +entity "records" { + id: primary + -- + date: varchar(10) [YYYY-MM-DD] + start: varchar(5) [HH:MM] + end: varchar(5) [HH:MM] + + created: timestamp + modified: timestamp + modified_to: records::id +} + +entity "estimates" { + id: primary + -- + date: varchar(7) [YYYY-MM] + estimate: real + + created: timestamp + modified: timestamp + modified_to: estimates::id +} + +@enduml diff --git a/package.json b/package.json index d39b605..baa9555 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,19 @@ { "name": "stundenaufzeichnung", - "private": true, "version": "0.0.1", - "type": "module", + "devDependencies": { + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/adapter-node": "^5.2.11", + "@sveltejs/kit": "^2.9.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@types/sqlite3": "^3.1.11", + "svelte": "^5.0.0", + "svelte-adapter-bun": "^0.5.2", + "svelte-check": "^4.0.0", + "typescript": "^5.0.0", + "vite": "^6.0.0" + }, + "private": true, "scripts": { "dev": "vite dev", "build": "vite build", @@ -10,13 +21,5 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" }, - "devDependencies": { - "@sveltejs/adapter-auto": "^3.0.0", - "@sveltejs/kit": "^2.9.0", - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", - "typescript": "^5.0.0", - "vite": "^6.0.0" - } + "type": "module" } diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..5e4d66b --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,38 @@ + +import { User, UserDB, init_db, close_db, create_user, get_user, get_user_db } from "$lib/server/database"; + +import { Database } from "bun:sqlite"; + +function init() { + + init_db(); + + console.log("started"); + +} + +function deinit() { + close_db(); + console.log('exit'); +} + +init(); + +process.on('exit', (reason) => { + deinit(); + process.exit(0); +}); + +process.on('SIGINT', (reason) => { + console.log("SIGINT") + process.exit(0); +}) + + +export async function handle({ event, resolve }) { + + event.locals.user = get_user(); + console.log("handle"); + + return await resolve(event); +} diff --git a/src/lib/db_types.ts b/src/lib/db_types.ts new file mode 100644 index 0000000..60b5032 --- /dev/null +++ b/src/lib/db_types.ts @@ -0,0 +1,20 @@ + +import { Database } from 'bun:sqlite'; + +export interface UserEntry { + id: number; + name: string; + created: string; +} + +export interface RecordEntry { + id: number; + date: string; + start: string; + end: string; + comment: string; + created: string; + modified: string; + modified_to: number; +} + diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts new file mode 100644 index 0000000..c403183 --- /dev/null +++ b/src/lib/server/database.ts @@ -0,0 +1,233 @@ +import { mkdir } from "node:fs/promises"; +import { Database, SQLiteError } from "bun:sqlite"; + +import { UserEntry, RecordEntry } from "$lib/db_types"; +import { parseDate, isTimeValidHHMM } from "$lib/util"; + +const DATABASES_PATH: string = ""; +const USER_DATABASE_PATH: string = DATABASES_PATH + "users.sqlite"; + +const CHECK_QUERY: string = + "SELECT * FROM sqlite_master;"; + +const USER_DATABASE_SETUP: string[] = [ + "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT, created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL);", +] + +const USER_DATABASE_ADD_USER: string = + "INSERT INTO users (name) VALUES ($name);"; + +const USER_DATABASE_GET_USER: string = + "SELECT * FROM users;"; + +const ENTRY_DATABASE_SETUP: string[] = [ + "PRAGMA foreign_keys = ON;", + + "CREATE TABLE meta (key TEXT PRIMARY KEY NOT NULL, value NUMBER);", + "INSERT INTO meta(key, value) VALUES ('triggerActive', 1)", + + "CREATE TABLE records ( \ + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \ + date VARCHAR(10), \ + start VARCHAR(5), \ + end VARCHAR(5), \ + comment TEXT, \ + created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \ + modified DATETIME DEFAULT NULL, \ + modified_to INTEGER UNIQUE DEFAULT NULL, \ + FOREIGN KEY(modified_to) REFERENCES records(id) \ + );", + + `CREATE TRIGGER prevent_update_if_superseded + BEFORE UPDATE ON records + WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1 + AND (OLD.modified_to NOT NULL OR OLD.date ISNULL) + BEGIN + SELECT raise(ABORT, 'Modification on changed row is not allowed'); + END;`, + + "CREATE TRIGGER prevent_update \ + BEFORE UPDATE ON records \ + WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1 \ + BEGIN \ + INSERT INTO records(date, start, end, comment) VALUES (NEW.date, NEW.start, NEW.end, NEW.comment); \ + UPDATE records SET (modified, modified_to) = (CURRENT_TIMESTAMP, last_insert_rowid()) WHERE NEW.id == id; \ + SELECT raise(IGNORE); \ + END;", + + `CREATE TRIGGER prevent_delete + BEFORE DELETE ON records + BEGIN + UPDATE records SET (date, start, end, comment) = (null, null, null, null) WHERE OLD.id = id; + SELECT raise(IGNORE); + END;` +] + +const ENTRY_DATABASE_GET_ENTRY_BY_ID: string = + "SELECT * FROM records WHERE modified_to ISNULL AND id = $id;" + +const ENTRY_DATABASE_GET_ENTRIES: string = + "SELECT * FROM records WHERE modified_to ISNULL;" + +const ENTRY_DATABASE_ADD_ENTRY: string = + "INSERT INTO records(date, start, end, comment) VALUES ($date, $start, $end, $comment);" + +const Entry_DATABASE_EDIT_ENTRY: string = + "UPDATE records SET date = $date, start = $start, end = $end, comment = $comment WHERE id = $id;"; + + +export class User { + user: UserEntry; + private database: Database; + + constructor(user: UserEntry, db: Database) { + this.user = user; + this._database = db; + } + + get_entries(): Entry[] { + const query = this._database.query(ENTRY_DATABASE_GET_ENTRIES); + const res = query.all() + + return res; + } + + get_entry(id: number): Entry { + const query = this._database.query(ENTRY_DATABASE_GET_ENTRY_BY_ID); + const res = query.get({ id: id }); + + return res; + } + + insert_entry(date: string, start: string, end: string, comment: string | null) { + + if (parseDate(date) == null || !isTimeValidHHMM(start) || !isTimeValidHHMM(end)) { + return false; + } + + const query = this._database.query(ENTRY_DATABASE_ADD_ENTRY); + const res = query.run({ date: date, start: start, end: end, comment: comment }); + + return res.changes == 1; + } + + update_entry(id: number, ndate: string, nstart: string, nend: string, ncomment: string): RecordEntry | null { + + if (isNaN(id) || parseDate(ndate) == null || !isTimeValidHHMM(nstart) || !isTimeValidHHMM(nend)) { + return null; + } + + const query = this._database.query(Entry_DATABASE_EDIT_ENTRY); + const res = query.run({ id: id, date: ndate, start: nstart, end: nend, comment: ncomment }); + + return res.changes > 1; + } +} + + +let user_database: Database; + + +function is_db_initialized(db: Database): boolean { + try { + + let res = db.query(CHECK_QUERY).get(); + + return res != null; + } catch (exception) { + if (!(exception instanceof SQLiteError)) { + throw exception; + } + + console.log(e); + + return false; + } +} + +function get_user_db_name(user: UserEntry) { + return DATABASES_PATH + "user-" + user.id + ".sqlite" +} + +function setup_db(db: Database, setup_queries: string[]) { + setup_queries.forEach((q) => { db.query(q).run(); }); +} + +export function init_db() { + + user_database = new Database(USER_DATABASE_PATH, { strict: true, create: true }); + + if (!is_db_initialized(user_database)) { + setup_db(user_database, USER_DATABASE_SETUP); + } + +} + +export function close_db() { + if (user_database) { + user_database.close(); + } +} + +export function create_user(name: string): boolean { + + try { + const statement = user_database.query(USER_DATABASE_ADD_USER); + const result = statement.run({ name: name }); + + return true; + } catch (e) { + console.log(e); + if (e instanceof SQLiteError) { + return false; + } + + throw e; + } +} + +function _get_user(): UserEntry { + + try { + const statement = user_database.prepare(USER_DATABASE_GET_USER); + const result: UserEntry = statement.get(); + + if (result == null) { + create_user("PM"); + return get_user(); + } + + return result; + + } catch (e) { + if (e instanceof SQLiteError) { + return false; + } + + throw e; + } + +} + +export function get_user(): User | null { + + const user = _get_user(); + + const db_name = get_user_db_name(user); + + try { + + let userdb = new Database(db_name, { create: true, strict: true }); + + if (!is_db_initialized(userdb)) { + setup_db(userdb, ENTRY_DATABASE_SETUP); + } + + return new User(user, userdb); + } catch (e) { + console.log(e); + + return null; + } + +} diff --git a/src/lib/util.ts b/src/lib/util.ts new file mode 100644 index 0000000..9f461fe --- /dev/null +++ b/src/lib/util.ts @@ -0,0 +1,89 @@ + +export function toInt(str: string): number { + let value = 0; + for (let i = 0; i < str.length; ++i) { + let c = str.charAt(i); + let n = Number(c); + if (isNaN(n)) { + return NaN; + } + value = value*10 + n; + } + + return value; +} + +export function parseDate(str: string): Date | null { + if (str.length != 2+1+2+1+4) { + return null; + } + + let day = toInt(str.slice(0, 2)) + let month = toInt(str.slice(3, 5)); + let year = toInt(str.slice(6, 10)); + + if (isNaN(day) || isNaN(month) || isNaN(year) || str.charAt(2) !== '.' || str.charAt(5) !== '.') { + return null; + } + + let date = new Date(year, month-1, day); + + if (isNaN(date.valueOf())) { + return null; + } + + return date; +} + +export function calculateDuration(start: string, end: string): string { + + if (start.length !== 5 || end.length !== 5) { + return ""; + } + + let start_h = toInt(start.slice(0, 2)); + let start_m = toInt(start.slice(3, 5)); + + let end_h = toInt(end.slice(0, 2)); + let end_m = toInt(end.slice(3, 5)); + + if (isNaN(start_h) || isNaN(start_m) || start.charAt(2) !== ':' + || isNaN(end_h) || isNaN(end_m) || end.charAt(2) !== ':') { + return ""; + } + + let start_n = start_h * 60 + start_m; + let end_n = end_h * 60 + end_m; + + let duration = (end_n - start_n) / 60; + + return duration.toFixed(2); +} + +export function localToIsoDate(str: string): string | undefined { + return parseDate(str)?.toISOString() +} + +export function isoToLocalDate(str: string): string | undefined { + let date = new Date(str); + + if (!date) { + return undefined; + } + + console.log(str); + console.log(date); + + return date.getDate() + "." + date.getMonth() + "." + date.getFullYear(); +} + +export function isTimeValidHHMM(str: string): boolean { + if (str.length !== 5) { + return false; + } + + let h = toInt(str.slice(0, 2)); + let m = toInt(str.slice(3, 5)); + + return (!(isNaN(h) || isNaN(m))) && h < 24 && m < 60 && str.charAt(2) == ':'; +} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..8131ae6 --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,172 @@ +import type { SQLiteError } from "bun:sqlite" + +import { fail, redirect } from '@sveltejs/kit'; + +import { toInt, parseDate, isTimeValidHHMM } from "$lib/util" +import { get_user, User } from "$lib/server/database"; + +export async function load({ locals }) { + return { + records: locals.user.get_entries() + }; +} + +export const actions = { + new_entry: async ({locals, request}) => { + + const ok = "ok"; + const missing = "missing"; + const invalid = "invalid"; + + const data = await request.formData(); + + let date = data.get("date"); + let start = data.get("start"); + let end = data.get("end"); + let comment = data.get("comment"); + + let return_obj = { + date : { + status: ok, + value: date + }, + start : { + status: ok, + value: start + }, + end : { + status: ok, + value: end + }, + comment : { + status: ok, + value: comment + }, + } + + if (date == null) { + return_obj.date.status = missing; + } else if (parseDate(date) == null) { + return_obj.date.status = invalid; + } + + if (start == null) { + return_obj.start.status = missing; + } else if (!isTimeValidHHMM(start)) { + return_obj.start.status = invalid; + } + + if (end == null) { + return_obj.end.status = missing; + } else if (!isTimeValidHHMM(end)) { + return_obj.end.status = invalid; + } + + console.log(return_obj); + + if (Object.keys(return_obj).some((v) => { return return_obj[v].status != ok; })) { + return fail(400, { new_entry: return_obj }); + } + + let res = locals.user.insert_entry(date, start, end, comment); + + if (!res) { + return fail(500, { }) + } + + return { new_entry: { success: true } }; + }, + edit_entry: async ({locals, request}) => { + + const ok = "ok"; + const missing = "missing"; + const invalid = "invalid"; + + const data = await request.formData(); + + let id = data.get("id"); + let date = data.get("date"); + let start = data.get("start"); + let end = data.get("end"); + let comment = data.get("comment"); + + let return_obj = { + id: { + status: ok, + value: id, + }, + date : { + status: ok, + value: date + }, + start : { + status: ok, + value: start + }, + end : { + status: ok, + value: end + }, + comment : { + status: ok, + value: comment + }, + + } + + if (id == null) { + return_obj.id.status = missing; + } else if (isNaN(toInt(id))) { + return_obj.id.status = invalid; + } + + if (date == null) { + return_obj.date.status = missing; + } else if (parseDate(date) == null) { + return_obj.date.status = invalid; + } + + if (start == null) { + return_obj.start.status = missing; + } else if (!isTimeValidHHMM(start)) { + return_obj.start.status = invalid; + } + + if (end == null) { + return_obj.end.status = missing; + } else if (!isTimeValidHHMM(end)) { + return_obj.end.status = invalid; + } + + if (Object.keys(return_obj).some((v) => { return return_obj[v].status != ok; })) { + return fail(400, { edit_entry: return_obj }); + } + + let current = locals.user.get_entry(id); + + if (!current) { + return fail(404, { new_entry: return_obj }); + } + + if (current.id == id && current.date == date && current.start == start && current.end == end && current.comment == comment) { + return { success: false, edit_entry: { return_obj } } + } + + let res = false; + + try { + res = locals.user.update_entry(id, date, start, end, comment); + } catch (e) { + if (!(e instanceof SQLiteError)) { + throw e; + } + } + + if (!res) { + return fail(500, { }) + } + + redirect(303, '/'); + return { success: true }; + }, +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 20787c6..562bff2 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,65 +1,100 @@
-
+
validateForm(e, new_state)} use:enhance>
+
validateForm(e, edit_state)} use:enhance>
@@ -436,117 +120,73 @@ - + + + + + +{#each data.records as entry} + {#if editing?.id != entry.id } + {@const weekday = parseDate(entry.date)?.getDay()} + + {#if weekday != null } + + {:else} + + {/if} + + + + - - - - - - - - - - - - - - -{#each entries as entry} - - - {#each entry as i} - - {/each} + {:else} + + + +
{ + event.preventDefault(); + setEditing(null); + }} + > + + +
+ {/if} {/each} -{#if entries === undefined || entries.length === 0} +{#if data.records === undefined || data.records.length === 0} - + {/if} @@ -556,6 +196,11 @@ diff --git a/src/routes/record_input_row.svelte b/src/routes/record_input_row.svelte new file mode 100644 index 0000000..8788c0a --- /dev/null +++ b/src/routes/record_input_row.svelte @@ -0,0 +1,623 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svelte.config.js b/svelte.config.js index 1295460..6bcb485 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,4 +1,4 @@ -import adapter from '@sveltejs/adapter-auto'; +import adapter from '@sveltejs/adapter-node'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ @@ -15,4 +15,4 @@ const config = { } }; -export default config; +export default config
Stundenliste
Ende Dauer AnmerkungActionsActions
{entry.date}{WEEKDAYS[weekday]}{entry.start}{entry.end}{calculateDuration(entry.start, entry.end)}{entry.comment ?? ""} - { - dateInput.select(); - dateValid = true; - } - } - onfocusout={ - (_) => { - dateValid = validateDate(dateInput); - if (dateValid) { - inWeekDay = WEEKDAYS[parseDate(dateInput.value)!.getDay()]; - } - } - } - required> +
{ + event.preventDefault(); + setEditing(entry); + }} + > + +
- {inWeekDay} - - { - startInput.select(); - startValid = true; - } - } - onfocusout={ - (_) => { - startValid = validateTime(startInput); - inDuration = calculateDuration(startInput.value, endInput.value); - } - } - required> - - { - endInput.select(); - endValid = true; - } - } - onfocusout={ - (_) => { - endValid = validateTime(endInput); - inDuration = calculateDuration(startInput.value, endInput.value); - } - } - required> - - {inDuration} - - - - -
{i}
No elementsNo records
+ { + dateInput.select(); + states.date.valid = true; + } + } + onfocusout={ + (_) => { + states.date.valid = validateDate(dateInput); + states.date.value = dateInput.value; + } + } + disabled={!enabled} + required> + + {inWeekDay} + + { + startInput.select(); + states.start.valid = true; + } + } + onfocusout={ + (_) => { + states.start.valid = validateTime(startInput); + states.start.value = startInput.value; + updateDuration(); + } + } + disabled={!enabled} + required> + + { + endInput.select(); + states.end.valid = true; + } + } + onfocusout={ + (_) => { + states.end.valid = validateTime(endInput); + states.end.value = endInput.value; + updateDuration(); + } + } + disabled={!enabled} + required> + + {inDuration} + + + + {@render children?.()} +