implemented update, moved input to component
This commit is contained in:
parent
828895e773
commit
3885082d2f
|
|
@ -21,3 +21,6 @@ Thumbs.db
|
|||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# Databases
|
||||
*.sqlite
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
@startuml
|
||||
|
||||
entity "users" {
|
||||
id: primary
|
||||
--
|
||||
name: text
|
||||
database_location: text
|
||||
}
|
||||
|
||||
@enduml
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.9 KiB |
|
|
@ -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
|
||||
25
package.json
25
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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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) == ':';
|
||||
}
|
||||
|
|
@ -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 };
|
||||
},
|
||||
}
|
||||
|
|
@ -1,65 +1,100 @@
|
|||
<script lang="ts">
|
||||
let { data } = $props();
|
||||
import { untrack } from "svelte";
|
||||
import { enhance } from "$app/forms";
|
||||
import { page } from "$app/state"
|
||||
|
||||
import type { RecordEntry } from "$lib/db_types"
|
||||
import { toInt, parseDate, calculateDuration } from "$lib/util";
|
||||
|
||||
import type { RowState } from "./record_input_row.svelte"
|
||||
import RecordInputRow from "./record_input_row.svelte"
|
||||
|
||||
|
||||
let { data, form } : { data: { records: RecordEntry[] }, form: any } = $props();
|
||||
|
||||
const WEEKDAYS: string[] = [ "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" ]
|
||||
|
||||
const HEADERS: string[] = [ "Datum", "Wochentag", "Beginn", "Ende", "Dauer", "Anmerkung" ];
|
||||
|
||||
const TODAY: Date = new Date();
|
||||
const CENTURY_PREF: number = Math.floor(TODAY.getFullYear() / 100);
|
||||
const CENTURY_YEAR: number = CENTURY_PREF * 100;
|
||||
const status_ok = "ok";
|
||||
const status_missing = "missing";
|
||||
const status_invalid = "invalid";
|
||||
|
||||
let dateInput: HTMLInputElement;
|
||||
let startInput: HTMLInputElement;
|
||||
let endInput: HTMLInputElement;
|
||||
|
||||
let dateValid: boolean = $state(true);
|
||||
let startValid: boolean = $state(true);
|
||||
let endValid: boolean = $state(true);
|
||||
let new_state = $state() as RowState;
|
||||
|
||||
let inWeekDay: string = $state("");
|
||||
let inDuration: string = $state("");
|
||||
let editing: RecordEntry | null = $state(null);
|
||||
let edit_state = $state() as RowState;
|
||||
|
||||
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;
|
||||
setNewState();
|
||||
setEditing();
|
||||
|
||||
$effect(() => {
|
||||
if (form) {
|
||||
untrack(() => setNewState());
|
||||
}
|
||||
})
|
||||
$effect(() => {
|
||||
if (form) {
|
||||
untrack(() => setEditing(null));
|
||||
}
|
||||
})
|
||||
|
||||
function setNewState() {
|
||||
new_state = {
|
||||
date: {
|
||||
valid: form?.new_entry?.date?.value !== "" || form?.new_entry?.date?.valid,
|
||||
value: form?.new_entry?.date?.value ?? "",
|
||||
},
|
||||
start: {
|
||||
valid: form?.new_entry?.start?.value !== "" || form?.new_entry?.start?.valid,
|
||||
value: form?.new_entry?.start?.value ?? "",
|
||||
},
|
||||
end: {
|
||||
valid: form?.new_entry?.end?.value !== "" || form?.new_entry?.end?.valid,
|
||||
value: form?.new_entry?.end?.value ?? "",
|
||||
},
|
||||
comment: {
|
||||
value: form?.new_entry?.date?.comment ?? "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function setEditing(entry: RecordEntry | null = null) {
|
||||
if (entry) {
|
||||
editing = entry;
|
||||
} else if (form?.edit_entry) {
|
||||
editing = form.edit_entry
|
||||
} else if (page.url.searchParams.get("edit")) {
|
||||
let id = toInt(page.url.searchParams.get("edit")!)
|
||||
let entry = data.records.find((entry) => { return entry.id == id; })
|
||||
editing = entry ?? null;
|
||||
} else {
|
||||
editing = null;
|
||||
}
|
||||
|
||||
return value;
|
||||
edit_state = {
|
||||
date: {
|
||||
valid: editing?.id != form?.edit_entry?.id || !(form?.edit_entry?.date?.status && form.edit_entry.date.status != status_ok),
|
||||
value: editing?.date ?? ""
|
||||
},
|
||||
start: {
|
||||
valid: editing?.id != form?.edit_entry?.id || !(form?.edit_entry?.start?.status && form.edit_entry.start.status != status_ok),
|
||||
value: editing?.start ?? ""
|
||||
},
|
||||
end: {
|
||||
valid: editing?.id != form?.edit_entry?.id || !(form?.edit_entry?.end?.status && form.edit_entry.end.status != status_ok),
|
||||
value: editing?.end ?? ""
|
||||
},
|
||||
comment: {
|
||||
value: editing?.comment ?? ""
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function validateForm(event: Event): boolean {
|
||||
let valid = dateValid && dateInput.value.length !== 0
|
||||
&& startValid && startInput.value.length !== 0
|
||||
&& endValid && endInput.value.length !== 0;
|
||||
function validateForm(event: Event, state: RowState): boolean {
|
||||
let valid = state.date.valid && state.date.value.length !== 0
|
||||
&& state.start.valid && state.start.value.length !== 0
|
||||
&& state.end.valid && state.end.value.length !== 0;
|
||||
|
||||
if (!valid) {
|
||||
event.preventDefault();
|
||||
|
|
@ -67,363 +102,12 @@
|
|||
return valid;
|
||||
}
|
||||
|
||||
function validateDate(element: HTMLInputElement) {
|
||||
|
||||
/*
|
||||
supports:
|
||||
D.M
|
||||
|
||||
DDMM
|
||||
D.MM
|
||||
DD.M
|
||||
|
||||
DD.MM
|
||||
|
||||
DDMMYY
|
||||
D.M.YY
|
||||
|
||||
D.M.YYYY
|
||||
DD.MM.YY
|
||||
DDMMYYYY
|
||||
|
||||
DD.MM.YYYY
|
||||
*/
|
||||
|
||||
switch (element.value.length) {
|
||||
case 0: return true;
|
||||
case 3: {
|
||||
/*
|
||||
* D.M
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.charAt(0));
|
||||
let month = toInt(element.value.charAt(2));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || element.value.charAt(1) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (day > 0 && month > 0) {
|
||||
element.value = "0" + day + ".0" + month + "." + TODAY.getFullYear();
|
||||
return true;
|
||||
}
|
||||
} break;
|
||||
case 4: if (
|
||||
(() =>{
|
||||
/*
|
||||
* DDMM
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.slice(2, 4));
|
||||
|
||||
if (isNaN(day) || isNaN(month)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(TODAY.getFullYear(), month, 0).getDate())) {
|
||||
element.value = element.value.slice(0, 2) + "." + element.value.slice(2, 4) + "." + TODAY.getFullYear();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
/*
|
||||
* DD.M
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.charAt(3));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || element.value.charAt(2) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(TODAY.getFullYear(), month, 0).getDate())) {
|
||||
element.value = element.value.slice(0, 2) + ".0" + month + "." + TODAY.getFullYear();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
/*
|
||||
* D.MM
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.charAt(0));
|
||||
let month = toInt(element.value.slice(2, 4));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || element.value.charAt(1) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(TODAY.getFullYear(), month, 0).getDate())) {
|
||||
element.value = "0" + day + "." + element.value.slice(2, 4) + "." + TODAY.getFullYear();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true) {
|
||||
return true;
|
||||
} break;
|
||||
case 5: {
|
||||
/*
|
||||
* DD.MM
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.slice(3, 5));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || element.value.charAt(2) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!((month < 1 || month > 12)
|
||||
|| (day < 1 || day > new Date(TODAY.getFullYear(), month, 0).getDate()))) {
|
||||
element.value = element.value + "." + TODAY.getFullYear();
|
||||
return true;
|
||||
}
|
||||
} break;
|
||||
case 6: if ((() => {
|
||||
/*
|
||||
* DDMMYY
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.slice(2, 4));
|
||||
let year = toInt(element.value.slice(4, 6));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || isNaN(year)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
year += CENTURY_YEAR;
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = element.value.slice(0, 2) + "." + element.value.slice(2, 4) + "." + CENTURY_PREF + element.value.slice(4, 6);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
/*
|
||||
* D.M.YY
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.charAt(0));
|
||||
let month = toInt(element.value.charAt(2));
|
||||
let year = toInt(element.value.slice(4, 6));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || isNaN(year) || element.value.charAt(1) !== '.' || element.value.charAt(3) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
year += CENTURY_YEAR;
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = "0" + day + ".0" + month + "." + CENTURY_PREF + element.value.slice(4, 6);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true) {
|
||||
return true;
|
||||
} break;
|
||||
case 8: if (
|
||||
(() => {
|
||||
/*
|
||||
* D.M.YYYY
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.charAt(0));
|
||||
let month = toInt(element.value.charAt(2));
|
||||
let year = toInt(element.value.slice(4, 8));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || isNaN(year) || element.value.charAt(1) !== '.' || element.value.charAt(3) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
year += CENTURY_YEAR;
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = "0" + day + ".0" + month + "." + element.value.slice(4, 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
/*
|
||||
* DD.MM.YY
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.slice(3, 5));
|
||||
let year = toInt(element.value.slice(6, 8));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || isNaN(year) || element.value.charAt(2) !== '.' || element.value.charAt(5) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
year += CENTURY_YEAR;
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = element.value.slice(0, 2) + "." + element.value.slice(3, 5) + "." + CENTURY_PREF + element.value.slice(6, 8);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
/*
|
||||
* DDMMYYYY
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.slice(2, 4));
|
||||
let year = toInt(element.value.slice(4, 8));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || isNaN(year)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = element.value.slice(0, 2) + "." + element.value.slice(2, 4) + "." + element.value.slice(4, 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true) {
|
||||
return true;
|
||||
} break;
|
||||
case 10: {
|
||||
/*
|
||||
* DD.MM.YYYY
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.slice(3, 5));
|
||||
let year = toInt(element.value.slice(6, 10));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || isNaN(year) || element.value.charAt(2) !== '.' || element.value.charAt(5) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
return true;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function validateTime(element: HTMLInputElement): boolean {
|
||||
|
||||
/*
|
||||
supports:
|
||||
|
||||
H:MM
|
||||
HHMM
|
||||
HH:MM
|
||||
*/
|
||||
|
||||
switch(element.value.length) {
|
||||
case 0: return true;
|
||||
case 4: if (
|
||||
(() => {
|
||||
let h = toInt(element.value.slice(0, 2));
|
||||
let m = toInt(element.value.slice(2, 4));
|
||||
|
||||
if (isNaN(h) || isNaN(m)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (0 <= h && h <= 24 && 0 <= m && m <= 59) {
|
||||
element.value = element.value.slice(0, 2) + ":" + element.value.slice(2, 4);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
let h = toInt(element.value.charAt(0));
|
||||
let m = toInt(element.value.slice(2, 4));
|
||||
|
||||
if (isNaN(h) || isNaN(m) || element.value.charAt(1) !== ':') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (0 <= m && m <= 59) {
|
||||
element.value = "0" + element.value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})() === true) {
|
||||
return true;
|
||||
}
|
||||
case 5: {
|
||||
let h = toInt(element.value.slice(0, 2));
|
||||
let m = toInt(element.value.slice(3, 5));
|
||||
|
||||
if (isNaN(h) || isNaN(m) || element.value.charAt(2) !== ':') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (0 <= h && h <= 24 && 0 <= m && m <= 59) {
|
||||
return true;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
let entries: number[][] = [];
|
||||
|
||||
for (let i = 0; i < 50; ++i) {
|
||||
entries.push([i, i, i, i, i, i, i*i]);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="wrap">
|
||||
|
||||
<form id="form_new_entry" method="POST" action="?/new_entry" onsubmit={validateForm}></form>
|
||||
<form id="form_new_entry" method="POST" action="?/new_entry" onsubmit={(e) => validateForm(e, new_state)} use:enhance></form>
|
||||
<form id="form_edit_entry" method="POST" action="?/edit_entry" onsubmit={(e) => validateForm(e, edit_state)} use:enhance></form>
|
||||
|
||||
<table class="list">
|
||||
<caption>Stundenliste</caption>
|
||||
|
|
@ -436,117 +120,73 @@
|
|||
<th style:width="12ch">Ende</th>
|
||||
<th style:width="12ch">Dauer</th>
|
||||
<th>Anmerkung</th>
|
||||
<th style:width="fit-content">Actions</th>
|
||||
<th style:width="12ch">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<RecordInputRow targetForm="form_new_entry" bind:states={new_state} enabled={editing == null}>
|
||||
<button
|
||||
type="submit"
|
||||
form="form_new_entry"
|
||||
disabled={editing != null}>
|
||||
+
|
||||
</button>
|
||||
</RecordInputRow>
|
||||
|
||||
{#each data.records as entry}
|
||||
{#if editing?.id != entry.id }
|
||||
{@const weekday = parseDate(entry.date)?.getDay()}
|
||||
<tr>
|
||||
<td>{entry.date}</td>
|
||||
{#if weekday != null }
|
||||
<td>{WEEKDAYS[weekday]}</td>
|
||||
{:else}
|
||||
<td></td>
|
||||
{/if}
|
||||
<td>{entry.start}</td>
|
||||
<td>{entry.end}</td>
|
||||
<td>{calculateDuration(entry.start, entry.end)}</td>
|
||||
<td>{entry.comment ?? ""}</td>
|
||||
<td>
|
||||
<input
|
||||
bind:this={dateInput}
|
||||
class:form-invalid={!dateValid}
|
||||
name="date"
|
||||
type="text"
|
||||
form="form_new_entry"
|
||||
onfocusin={
|
||||
(_) => {
|
||||
dateInput.select();
|
||||
dateValid = true;
|
||||
}
|
||||
}
|
||||
onfocusout={
|
||||
(_) => {
|
||||
dateValid = validateDate(dateInput);
|
||||
if (dateValid) {
|
||||
inWeekDay = WEEKDAYS[parseDate(dateInput.value)!.getDay()];
|
||||
}
|
||||
}
|
||||
}
|
||||
required>
|
||||
<form
|
||||
id="form_edit"
|
||||
method="GET"
|
||||
onsubmit={(event) => {
|
||||
event.preventDefault();
|
||||
setEditing(entry);
|
||||
}}
|
||||
>
|
||||
<button type="submit" name="edit" value={entry.id}>Edit</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{inWeekDay}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input
|
||||
bind:this={startInput}
|
||||
class:form-invalid={!startValid}
|
||||
name="starttime"
|
||||
type="text"
|
||||
form="form_new_entry"
|
||||
onfocusin={
|
||||
(_) => {
|
||||
startInput.select();
|
||||
startValid = true;
|
||||
}
|
||||
}
|
||||
onfocusout={
|
||||
(_) => {
|
||||
startValid = validateTime(startInput);
|
||||
inDuration = calculateDuration(startInput.value, endInput.value);
|
||||
}
|
||||
}
|
||||
required>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input
|
||||
bind:this={endInput}
|
||||
class:form-invalid={!endValid}
|
||||
name="endtime"
|
||||
type="text"
|
||||
form="form_new_entry"
|
||||
onfocusin={
|
||||
(_) => {
|
||||
endInput.select();
|
||||
endValid = true;
|
||||
}
|
||||
}
|
||||
onfocusout={
|
||||
(_) => {
|
||||
endValid = validateTime(endInput);
|
||||
inDuration = calculateDuration(startInput.value, endInput.value);
|
||||
}
|
||||
}
|
||||
required>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{inDuration}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input
|
||||
name="comment"
|
||||
type="text"
|
||||
form="form_new_entry">
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<button
|
||||
type="submit"
|
||||
form="form_new_entry">
|
||||
+
|
||||
</button>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{#each entries as entry}
|
||||
<!--<tr><td>{entry}</td></tr>-->
|
||||
<tr>
|
||||
{#each entry as i}
|
||||
<td>{i}</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{:else}
|
||||
<RecordInputRow targetForm="form_edit_entry" bind:states={edit_state} enabled={true} >
|
||||
<input type="hidden" form="form_edit_entry" name="id" value={entry.id} />
|
||||
<button
|
||||
type="submit"
|
||||
form="form_edit_entry">
|
||||
Save
|
||||
</button>
|
||||
<form
|
||||
id="form_edit_cancel"
|
||||
method="GET"
|
||||
onsubmit={(event) => {
|
||||
event.preventDefault();
|
||||
setEditing(null);
|
||||
}}
|
||||
>
|
||||
<button type="submit">Cancel</button>
|
||||
</form>
|
||||
</RecordInputRow>
|
||||
{/if}
|
||||
{/each}
|
||||
<tr></tr>
|
||||
</tbody>
|
||||
{#if entries === undefined || entries.length === 0}
|
||||
{#if data.records === undefined || data.records.length === 0}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class="td-no-elements" colspan="999">No elements</td>
|
||||
<td class="td-no-elements" colspan="999">No records</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{/if}
|
||||
|
|
@ -556,6 +196,11 @@
|
|||
|
||||
<style>
|
||||
|
||||
form {
|
||||
width: fit-content;
|
||||
border: none;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
|
|
@ -569,31 +214,20 @@ table caption {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
tbody * {
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
tbody tr:nth-child(odd) {
|
||||
background: gray;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(odd) {
|
||||
tbody tr:nth-child(even) {
|
||||
background: lightgray;
|
||||
}
|
||||
|
||||
td input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
border: none;
|
||||
tr td:last-of-type {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.td-no-elements {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-invalid {
|
||||
background: #FF4444;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,623 @@
|
|||
<script module lang="ts">
|
||||
export interface RowState {
|
||||
date: { valid: boolean, value: string},
|
||||
start: { valid: boolean, value: string},
|
||||
end: { valid: boolean, value: string},
|
||||
comment: { value: string }
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
import { toInt, parseDate, calculateDuration } from "$lib/util";
|
||||
|
||||
interface Props {
|
||||
targetForm: string,
|
||||
enabled: boolean,
|
||||
states: RowState,
|
||||
children: Snippet
|
||||
}
|
||||
let { targetForm, enabled, states = $bindable(), children }: Props = $props();
|
||||
|
||||
const WEEKDAYS: string[] = [ "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" ]
|
||||
|
||||
const TODAY: Date = new Date();
|
||||
const CENTURY_PREF: number = Math.floor(TODAY.getFullYear() / 100);
|
||||
const CENTURY_YEAR: number = CENTURY_PREF * 100;
|
||||
|
||||
let dateInput: HTMLInputElement;
|
||||
let startInput: HTMLInputElement;
|
||||
let endInput: HTMLInputElement;
|
||||
|
||||
states = {
|
||||
...states,
|
||||
...(states?.date ? states.date : {}),
|
||||
...(states?.start ? states.start : {}),
|
||||
...(states?.end ? states.end : {}),
|
||||
...(states?.comment ? states.comment : {}),
|
||||
date: {
|
||||
...states.date,
|
||||
...(states.date?.valid == null && { valid: true }),
|
||||
...(states.date?.value == null && { value: "" }),
|
||||
},
|
||||
start: {
|
||||
...states.start,
|
||||
...(states.start?.valid == null && { valid: true }),
|
||||
...(states.start?.value == null && { value: "" }),
|
||||
},
|
||||
end: {
|
||||
...states.end,
|
||||
...(states.end?.valid == null && { valid: true }),
|
||||
...(states.end?.value == null && { value: "" }),
|
||||
},
|
||||
comment: {
|
||||
...states.comment,
|
||||
...(states.comment?.value == null && { value: "" }),
|
||||
},
|
||||
};
|
||||
|
||||
let inWeekDay: string = $derived.by(() => {
|
||||
let date = null;
|
||||
if (states?.date?.valid && states?.date?.value != null) {
|
||||
date = parseDate(states.date.value);
|
||||
}
|
||||
return date ? WEEKDAYS[date.getDay()] : "";
|
||||
})
|
||||
let inDuration: string = $state("");
|
||||
updateDuration();
|
||||
|
||||
|
||||
function updateDuration() {
|
||||
if ((states?.start?.valid && states?.end?.valid)
|
||||
&& (states?.start?.value != null && states?.end?.value != null)) {
|
||||
inDuration = calculateDuration(states.start.value, states.end.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function validateDate(element: HTMLInputElement) {
|
||||
|
||||
/*
|
||||
todo: trailing . after month
|
||||
supports:
|
||||
D.M
|
||||
|
||||
DDMM
|
||||
D.MM
|
||||
DD.M
|
||||
|
||||
DD.MM
|
||||
|
||||
DDMMYY
|
||||
D.M.YY
|
||||
|
||||
DD.M.YY
|
||||
D.MM.YY
|
||||
|
||||
D.M.YYYY
|
||||
DD.MM.YY
|
||||
DDMMYYYY
|
||||
|
||||
DD.M.YYYY
|
||||
D.MM.YYYY
|
||||
|
||||
DD.MM.YYYY
|
||||
*/
|
||||
|
||||
switch (element.value.length) {
|
||||
case 0: return true;
|
||||
case 3: {
|
||||
/*
|
||||
* D.M
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.charAt(0));
|
||||
let month = toInt(element.value.charAt(2));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || element.value.charAt(1) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (day > 0 && month > 0) {
|
||||
element.value = "0" + day + ".0" + month + "." + TODAY.getFullYear();
|
||||
return true;
|
||||
}
|
||||
} break;
|
||||
case 4: if (
|
||||
(() =>{
|
||||
/*
|
||||
* DDMM
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.slice(2, 4));
|
||||
|
||||
if (isNaN(day) || isNaN(month)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(TODAY.getFullYear(), month, 0).getDate())) {
|
||||
element.value = element.value.slice(0, 2) + "." + element.value.slice(2, 4) + "." + TODAY.getFullYear();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
/*
|
||||
* DD.M
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.charAt(3));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || element.value.charAt(2) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(TODAY.getFullYear(), month, 0).getDate())) {
|
||||
element.value = element.value.slice(0, 2) + ".0" + month + "." + TODAY.getFullYear();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
/*
|
||||
* D.MM
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.charAt(0));
|
||||
let month = toInt(element.value.slice(2, 4));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || element.value.charAt(1) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(TODAY.getFullYear(), month, 0).getDate())) {
|
||||
element.value = "0" + day + "." + element.value.slice(2, 4) + "." + TODAY.getFullYear();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true) {
|
||||
return true;
|
||||
} break;
|
||||
case 5: {
|
||||
/*
|
||||
* DD.MM
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.slice(3, 5));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || element.value.charAt(2) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!((month < 1 || month > 12)
|
||||
|| (day < 1 || day > new Date(TODAY.getFullYear(), month, 0).getDate()))) {
|
||||
element.value = element.value + "." + TODAY.getFullYear();
|
||||
return true;
|
||||
}
|
||||
} break;
|
||||
case 6: if ((() => {
|
||||
/*
|
||||
* DDMMYY
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.slice(2, 4));
|
||||
let year = toInt(element.value.slice(4, 6));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || isNaN(year)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
year += CENTURY_YEAR;
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = element.value.slice(0, 2) + "." + element.value.slice(2, 4) + "." + year;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
/*
|
||||
* D.M.YY
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.charAt(0));
|
||||
let month = toInt(element.value.charAt(2));
|
||||
let year = toInt(element.value.slice(4, 6));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || isNaN(year) || element.value.charAt(1) !== '.' || element.value.charAt(3) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
year += CENTURY_YEAR;
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = "0" + day + ".0" + month + "." + year;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true) {
|
||||
return true;
|
||||
} break;
|
||||
case 7: if ((() => {
|
||||
/*
|
||||
* D.MM.YY
|
||||
*/
|
||||
let day = toInt(element.value.charAt(0));
|
||||
let month = toInt(element.value.slice(2, 4));
|
||||
let year = toInt(element.value.slice(5, 7));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || element.value.charAt(1) !== '.' || element.value.charAt(4) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
year += CENTURY_YEAR;
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = "0" + element.value.slice(0, 5) + year;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
/*
|
||||
* D.MM.YY
|
||||
*/
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.charAt(3));
|
||||
let year = toInt(element.value.slice(5, 7));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || element.value.charAt(2) !== '.' || element.value.charAt(4) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
year += CENTURY_YEAR;
|
||||
|
||||
if (!(day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = element.value.slice(0, 3) + "0" + month + "." + year;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true) {
|
||||
return true;
|
||||
} break;
|
||||
case 8: if (
|
||||
(() => {
|
||||
/*
|
||||
* D.M.YYYY
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.charAt(0));
|
||||
let month = toInt(element.value.charAt(2));
|
||||
let year = toInt(element.value.slice(4, 8));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || isNaN(year) || element.value.charAt(1) !== '.' || element.value.charAt(3) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
year += CENTURY_YEAR;
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = "0" + day + ".0" + month + "." + element.value.slice(4, 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
/*
|
||||
* DD.MM.YY
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.slice(3, 5));
|
||||
let year = toInt(element.value.slice(6, 8));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || isNaN(year) || element.value.charAt(2) !== '.' || element.value.charAt(5) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
year += CENTURY_YEAR;
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = element.value.slice(0, 2) + "." + element.value.slice(3, 5) + "." + CENTURY_PREF + element.value.slice(6, 8);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
/*
|
||||
* DDMMYYYY
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.slice(2, 4));
|
||||
let year = toInt(element.value.slice(4, 8));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || isNaN(year)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = element.value.slice(0, 2) + "." + element.value.slice(2, 4) + "." + element.value.slice(4, 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true) {
|
||||
return true;
|
||||
} break;
|
||||
case 9: if ((() => {
|
||||
/*
|
||||
* D.MM.YYYY
|
||||
*/
|
||||
let day = toInt(element.value.charAt(0));
|
||||
let month = toInt(element.value.slice(2, 4));
|
||||
let year = toInt(element.value.slice(5, 9));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || element.value.charAt(1) !== '.' || element.value.charAt(4) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = "0" + element.value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
/*
|
||||
* D.MM.YYYY
|
||||
*/
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.charAt(3));
|
||||
let year = toInt(element.value.slice(5, 9));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || element.value.charAt(2) !== '.' || element.value.charAt(4) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
element.value = element.value.slice(0, 3) + "0" + month + element.value.slice(4, 9);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})() === true) {
|
||||
return true;
|
||||
} break;
|
||||
case 10: {
|
||||
/*
|
||||
* DD.MM.YYYY
|
||||
*/
|
||||
|
||||
let day = toInt(element.value.slice(0, 2));
|
||||
let month = toInt(element.value.slice(3, 5));
|
||||
let year = toInt(element.value.slice(6, 10));
|
||||
|
||||
if (isNaN(day) || isNaN(month) || isNaN(year) || element.value.charAt(2) !== '.' || element.value.charAt(5) !== '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(month < 1 || month > 12
|
||||
|| day < 1 || day > new Date(year, month, 0).getDate())) {
|
||||
return true;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function validateTime(element: HTMLInputElement): boolean {
|
||||
|
||||
/*
|
||||
supports:
|
||||
HH
|
||||
H:MM
|
||||
HHMM
|
||||
HH:MM
|
||||
*/
|
||||
|
||||
switch(element.value.length) {
|
||||
case 0: return true;
|
||||
case 2: {
|
||||
let h = toInt(element.value);
|
||||
|
||||
if (!isNaN(h) && 0 < h && h <= 24) {
|
||||
element.value = element.value + ":00";
|
||||
return true;
|
||||
}
|
||||
} break;
|
||||
case 4: if (
|
||||
(() => {
|
||||
let h = toInt(element.value.slice(0, 2));
|
||||
let m = toInt(element.value.slice(2, 4));
|
||||
|
||||
if (isNaN(h) || isNaN(m)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (0 <= h && h <= 24 && 0 <= m && m <= 59) {
|
||||
element.value = element.value.slice(0, 2) + ":" + element.value.slice(2, 4);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})() === true
|
||||
|| (() => {
|
||||
let h = toInt(element.value.charAt(0));
|
||||
let m = toInt(element.value.slice(2, 4));
|
||||
|
||||
if (isNaN(h) || isNaN(m) || element.value.charAt(1) !== ':') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (0 <= m && m <= 59) {
|
||||
element.value = "0" + element.value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})() === true) {
|
||||
return true;
|
||||
}
|
||||
case 5: {
|
||||
let h = toInt(element.value.slice(0, 2));
|
||||
let m = toInt(element.value.slice(3, 5));
|
||||
|
||||
if (isNaN(h) || isNaN(m) || element.value.charAt(2) !== ':') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (0 <= h && h <= 24 && 0 <= m && m <= 59) {
|
||||
return true;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
bind:this={dateInput}
|
||||
bind:value={states.date.value}
|
||||
class:form-invalid={!states.date.valid}
|
||||
name="date"
|
||||
type="text"
|
||||
form={targetForm}
|
||||
onfocusin={
|
||||
(_) => {
|
||||
dateInput.select();
|
||||
states.date.valid = true;
|
||||
}
|
||||
}
|
||||
onfocusout={
|
||||
(_) => {
|
||||
states.date.valid = validateDate(dateInput);
|
||||
states.date.value = dateInput.value;
|
||||
}
|
||||
}
|
||||
disabled={!enabled}
|
||||
required>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{inWeekDay}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input
|
||||
bind:this={startInput}
|
||||
bind:value={states.start.value}
|
||||
class:form-invalid={!states.start.valid}
|
||||
name="start"
|
||||
type="text"
|
||||
form={targetForm}
|
||||
onfocusin={
|
||||
(_) => {
|
||||
startInput.select();
|
||||
states.start.valid = true;
|
||||
}
|
||||
}
|
||||
onfocusout={
|
||||
(_) => {
|
||||
states.start.valid = validateTime(startInput);
|
||||
states.start.value = startInput.value;
|
||||
updateDuration();
|
||||
}
|
||||
}
|
||||
disabled={!enabled}
|
||||
required>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input
|
||||
bind:this={endInput}
|
||||
bind:value={states.end.value}
|
||||
class:form-invalid={!states.end.valid}
|
||||
name="end"
|
||||
type="text"
|
||||
form={targetForm}
|
||||
onfocusin={
|
||||
(_) => {
|
||||
endInput.select();
|
||||
states.end.valid = true;
|
||||
}
|
||||
}
|
||||
onfocusout={
|
||||
(_) => {
|
||||
states.end.valid = validateTime(endInput);
|
||||
states.end.value = endInput.value;
|
||||
updateDuration();
|
||||
}
|
||||
}
|
||||
disabled={!enabled}
|
||||
required>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{inDuration}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input
|
||||
name="comment"
|
||||
type="text"
|
||||
form={targetForm}
|
||||
value={states.comment.value}
|
||||
disabled={!enabled}>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{@render children?.()}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
<style>
|
||||
|
||||
/* * {
|
||||
border: 1px solid;
|
||||
}*/
|
||||
|
||||
td input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
vertical-align: middle;
|
||||
|
||||
border: none;
|
||||
}
|
||||
|
||||
tr td:last-of-type {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.form-invalid {
|
||||
background: #FF4444;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue