Compare commits

...

2 Commits

Author SHA1 Message Date
Patrick e2f011597d code improvements 2025-06-05 14:32:39 +02:00
Patrick 104cafbcec Added Login 2025-06-05 14:32:12 +02:00
17 changed files with 680 additions and 398 deletions

View File

@ -1,6 +1,7 @@
import type { Handle } from "@sveltejs/kit"; import type { Handle } from "@sveltejs/kit";
import { error } from "@sveltejs/kit"; import { error, redirect } from "@sveltejs/kit";
import SessionStore from "$lib/server/session_store"
import { init_db, close_db, get_user } from "$lib/server/database"; import { init_db, close_db, get_user } from "$lib/server/database";
async function init() { async function init() {
@ -39,11 +40,33 @@ process.on('SIGINT', (_) => {
}) })
export let handle: Handle = async function ({ event, resolve }) { export let handle: Handle = async function ({ event, resolve }) {
let user = get_user();
if (!user) { console.log("incoming ", event.request.method, " request to: ", event.url.href, " (route id: ", event.route.id, ")");
return error(404, "No user"); // redirect login
if (event.route.id == null) {
return error(404, "This page does not exist.");
}
const token = event.cookies.get("session_id")
const user = SessionStore.get_user_by_access_token(token ?? "")
console.log("tried access with token: ", token)
if (!token || !user) {
if (event.request.method == "POST" && event.route.id != "/login") {
return error(401, "Invalid Session");
}
if (token) {
event.cookies.delete("session_id", { path: "/" });
}
if (event.route.id == "/login") {
return await resolve(event);
} else {
event.url.searchParams.set("redirect", event.route.id);
return redirect(302, `/login?${event.url.searchParams}`);
}
} }
event.locals.user = user; event.locals.user = user;

21
src/lib/server/auth.ts Normal file
View File

@ -0,0 +1,21 @@
import Bun from 'bun';
import { User, get_user_by_name } from "$lib/server/database";
export async function authorize_password(username: string, password: string): Promise<User | null> {
const user = get_user_by_name(username);
const password_hash = user?.password ?? "";
console.log("Username: ", username);
console.log("Password: ", password);
console.log(user);
const res = await Bun.password.verify(password, password_hash, "bcrypt");
console.log("hash res:", res);
return res ? user : null;
}

View File

@ -1,7 +1,7 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import { Database, SQLiteError } from "bun:sqlite"; import { Database, SQLiteError } from "bun:sqlite";
import { UserEntry, RecordEntry, EstimatesEntry } from "$lib/db_types"; import type { UserEntry, RecordEntry, EstimatesEntry } from "$lib/db_types";
import { calculateDuration, parseDate, toInt, isTimeValidHHMM } from "$lib/util"; import { calculateDuration, parseDate, toInt, isTimeValidHHMM } from "$lib/util";
const DATABASES_PATH: string = (process.env.APP_USER_DATA_PATH ?? ".") + "/databases/"; const DATABASES_PATH: string = (process.env.APP_USER_DATA_PATH ?? ".") + "/databases/";
@ -11,22 +11,54 @@ const CHECK_QUERY: string =
"SELECT * FROM sqlite_master;"; "SELECT * FROM sqlite_master;";
const USER_DATABASE_SETUP: string[] = [ const USER_DATABASE_SETUP: string[] = [
"PRAGMA foreign_keys = ON;",
`CREATE TABLE IF NOT EXISTS users ( `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
gender TEXT,
name TEXT, name TEXT,
gender TEXT,
address TEXT, address TEXT,
initials TEXT, username TEXT,
password TEXT,
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
);`, );`,
`CREATE TABLE refresh_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
user_id INTEGER NOT NULL,
token TEXT UNIQUE,
expiry_date DATETIME,
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);`,
/*`CREATE TABLE session_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
user_id INTEGER NOT NULL,
create_token_id INTEGER NOT NULL,
token TEXT UNIQUE,
expiry_date DATETIME,
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id),
FOREIGN KEY(create_token_id) REFERENCES access_tokens(id)
);`,*/
]; ];
const USER_DATABASE_ADD_USER: string = const USER_DATABASE_ADD_USER: string =
"INSERT INTO users (name, initials) VALUES ($name, $initials);"; "INSERT INTO users (name, gender, address, username, password) VALUES ($name, $gender, $address, $username, $password);";
const USER_DATABASE_GET_USER: string = const USER_DATABASE_GET_USER: string =
"SELECT * FROM users;"; "SELECT * FROM users;";
const USER_DATABASE_GET_USER_BY_NAME: string =
"SELECT * FROM users WHERE username = $username;"
/*const USER_DATABASE_ADD_ACCESS_TOKEN: string =
"INSERT INTO access_tokens (user_id, token, expiry_date) VALUES ($user_id, $token, $expiry_date);"
const USER_DATABASE_REMOVE_ACCESS_TOKEN: string =
"UPDATE access_tokens SET expiry_date = NULL WHERE token = $token;"*/
const ENTRY_DATABASE_SETUP: string[] = [ const ENTRY_DATABASE_SETUP: string[] = [
"PRAGMA foreign_keys = ON;", "PRAGMA foreign_keys = ON;",
@ -143,17 +175,19 @@ export class User {
gender: string; gender: string;
name: string; name: string;
address: string; address: string;
initials: string; username: string;
password: string
created: string; created: string;
private database: Database; private _database: Database;
constructor(user: UserEntry, db: Database) { constructor(user: UserEntry, db: Database) {
this.id = user.id; this.id = user.id;
this.gender = user.gender; this.gender = user.gender;
this.name = user.name; this.name = user.name;
this.address = user.address; this.address = user.address;
this.initials = user.initials; this.username = user.username;
this.password = user.password;
this.created = user.created; this.created = user.created;
this._database = db; this._database = db;
} }
@ -308,11 +342,14 @@ function setup_db(db: Database, setup_queries: string[]) {
export async function init_db() { export async function init_db() {
const stdout = await fs.mkdir(DATABASES_PATH, { recursive: true }); const stdout = await fs.mkdir(DATABASES_PATH, { recursive: true });
console.log(stdout)
user_database = new Database(USER_DATABASE_PATH, { strict: true, create: true }); user_database = new Database(USER_DATABASE_PATH, { strict: true, create: true });
if (!is_db_initialized(user_database)) { if (!is_db_initialized(user_database)) {
setup_db(user_database, USER_DATABASE_SETUP); setup_db(user_database, USER_DATABASE_SETUP);
console.log("create user");
create_user({name: "PM", gender: "x", address: "home", username: "P", password: "M" });
} }
} }
@ -323,13 +360,15 @@ export function close_db() {
} }
} }
export function create_user(name: string, initials: string): boolean { export async function create_user(user: { name: string, gender: string, address: string, username: string, password: string }): Promise<boolean> {
user.password = await Bun.password.hash(user.password, { algorithm: "bcrypt", cost: 11});
try { try {
const statement = user_database.query(USER_DATABASE_ADD_USER); const statement = user_database.query(USER_DATABASE_ADD_USER);
const result = statement.run({ name: name, initials: initials }); const result = statement.run(user);
return true; return result.changes == 1;
} catch (e) { } catch (e) {
console.log(e); console.log(e);
if (e instanceof SQLiteError) { if (e instanceof SQLiteError) {
@ -344,7 +383,7 @@ function _get_user(): UserEntry | null {
try { try {
const statement = user_database.prepare(USER_DATABASE_GET_USER); const statement = user_database.prepare(USER_DATABASE_GET_USER);
const result: UserEntry = statement.get(); const result = statement.get() as UserEntry;
return result; return result;
@ -358,7 +397,7 @@ function _get_user(): UserEntry | null {
} }
export function get_user(): User | null { export function get_user(id: number): User | null {
const user = _get_user(); const user = _get_user();
if (user == null) { if (user == null) {
@ -385,3 +424,31 @@ export function get_user(): User | null {
} }
} }
export function get_user_by_name(username: string): User | null {
try {
const query = user_database.prepare(USER_DATABASE_GET_USER_BY_NAME);
const user = query.get({ username: username }) as UserEntry | null ;
if (!user) {
return null;
}
fs.mkdir(DATABASES_PATH, { recursive: true });
let userdb = new Database(get_user_db_name(user), { create: true, strict: true });
if (!is_db_initialized(userdb)) {
setup_db(userdb, ENTRY_DATABASE_SETUP);
}
return new User(user, userdb);
} catch (exception) {
if (!(exception instanceof SQLiteError)) {
throw exception;
}
}
return null;
}

View File

@ -0,0 +1,127 @@
import type { User } from "$lib/server/database";
import Crypto from "node:crypto";
interface UserSession {
user: User;
last_active: Date;
}
export interface TokenInfo {
user_id: number;
creation_time: Date;
expiry_time: Date;
}
class LoginError extends Error {
constructor(message: string) {
super(message);
}
}
class UserLoginError extends LoginError {
user_id: number;
constructor(user_id: number, message: string) {
super(message);
this.user_id = user_id;
}
}
const TOKEN_LENGTH = 256;
const active_users = new Map<number, UserSession>();
const active_session_tokens = new Map<string, TokenInfo>();
function issue_access_token_for_user(user: User, expiry_date: Date): string {
let user_session = active_users.get(user.id);
if (user_session === undefined) {
user_session = {
user: user,
last_active: new Date()
} as UserSession;
active_users.set(user.id, user_session);
}
/* | Token (256) | Expiry date as getTime | */
const token_info: TokenInfo = {
user_id: user.id,
creation_time: new Date(),
expiry_time: expiry_date
}
const session_token = Buffer.concat(
[ Crypto.randomBytes(TOKEN_LENGTH / 4 * 3),
Buffer.from(token_info.creation_time.getTime().toFixed(0)) ]
).toString("base64");
if (active_session_tokens.has(session_token)) {
throw new UserLoginError(user.id, "Tried issuing a duplicate session token.");
}
active_session_tokens.set(session_token, token_info);
return session_token;
}
function get_user_by_access_token(token: string): User | null {
const token_info = active_session_tokens.get(token);
if (token_info === undefined || token_info.expiry_time.getTime() < Date.now()) {
return null;
}
const user = active_users.get(token_info.user_id);
if (user === undefined) {
return null;
}
user.last_active = new Date();
return user.user;
}
function logout_user_session(token: string): boolean {
const token_info = active_session_tokens.get(token);
if (!token_info) {
console.log("this shouldn't happen.");
return false;
}
token_info.expiry_time = new Date(0);
return true;
}
async function __clean_session_store() {
console.log("cleaning sessions");
const active_users_session = new Set<number>();
active_session_tokens.forEach((token_info, token, token_map) => {
if (token_info.expiry_time.getTime() < Date.now()) {
token_map.delete(token);
} else {
active_users_session.add(token_info.user_id);
}
});
active_users.forEach((user_info, user_id, user_map) => {
if (user_info.last_active.getTime() + 15*60*1000 < Date.now() && !active_users.has(user_id)) {
user_map.delete(user_id)
}
});
}
export default class SessionStore {
static issue_access_token_for_user = issue_access_token_for_user;
static get_user_by_access_token = get_user_by_access_token;
static logout_user_session = logout_user_session;
}
setInterval(__clean_session_store, 15*60*1000);

View File

@ -9,6 +9,14 @@
<li><a href="/">Stundenliste</a></li> <li><a href="/">Stundenliste</a></li>
<li><a href="/schaetzung">Stundenschätzung</a></li> <li><a href="/schaetzung">Stundenschätzung</a></li>
<li><a href="/dokumente">Dokumente</a></li> <li><a href="/dokumente">Dokumente</a></li>
<li style="height: 20px"></li>
<li>Benutzerverwaltung</li>
<li>
<form method="POST" action="/login?/logout">
<button type="submit">Logout</button>
</form>
</li>
</ul> </ul>
</div> </div>
{/snippet} {/snippet}
@ -50,12 +58,23 @@
list-style-type: none; list-style-type: none;
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
height: 100%;
} }
.nav ul li { .nav ul li {
padding-bottom: 5px; padding-bottom: 5px;
} }
.nav button {
padding: 0;
border: none;
background: none;
font-family: arial, sans-serif;
text-decoration-line: underline;
color: blue;
cursor: pointer;
}
.nav h1 { .nav h1 {
width: 100%; width: 100%;

View File

@ -1,52 +1,55 @@
import type { PageServerLoad, Actions } from "./$types"; import type { PageServerLoad, Actions } from "./$types";
import type { SQLiteError } from "bun:sqlite"; import { SQLiteError } from "bun:sqlite";
import type { FileProperties } from "$lib/server/docstore"
import type { RecordEntry, EstimatesEntry } from "$lib/db_types"; import type { RecordEntry, EstimatesEntry } from "$lib/db_types";
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import { MONTHS, toInt, parseDate, isTimeValidHHMM, month_of } from "$lib/util" import { toInt, parseDate, isTimeValidHHMM } from "$lib/util"
import { get_user, User } from "$lib/server/database";
import { getRecordFiles } from "$lib/server/docstore"; import { getRecordFiles } from "$lib/server/docstore";
export const load: PageServerLoad = async ({ locals }) => { export const load: PageServerLoad = async ({ locals }) => {
let records: { entry: RecordEntry, parsedDate: Date } = locals.user.get_entries().map((v) => ({ entry: v, parsedDate: parseDate(v.date) })); if (!locals.user) {
let estimates: EstimatesEntry = locals.user.get_estimates() return fail(403, { message: "Unauthorized user" });
}
let records = locals.user.get_entries().map((v) => ({ entry: v, parsedDate: parseDate(v.date) ?? new Date(0) }));
let estimates = locals.user.get_estimates()
let documents = (await getRecordFiles(locals.user)).map((v) => { return { year: toInt(v.identifier.substring(0, 4)), month: toInt(v.identifier.substring(5, 7)), file: v }; }); let documents = (await getRecordFiles(locals.user)).map((v) => { return { year: toInt(v.identifier.substring(0, 4)), month: toInt(v.identifier.substring(5, 7)), file: v }; });
let records_grouped = Map.groupBy(records, (v) => { return v.parsedDate.getFullYear() } ); let records_grouped: Map<number, Map<number, RecordEntry[]>> = new Map()
Map.groupBy(records, (v) => { return v.parsedDate.getFullYear() } ).forEach((value, key, _) => {
records_grouped.forEach((value, key, map) => { let month_map: Map<number, RecordEntry[]> = new Map()
let m = Map.groupBy(value, (v) => v.parsedDate.getMonth()); Map.groupBy(value, (v) => v.parsedDate.getMonth()).forEach((value, key, _) => {
// remove parsed date // remove parsed date
m.forEach((value, key, map) => { month_map.set(key, value.map((v) => v.entry));
map.set(key, value.map((v) => v.entry));
}); });
map.set(key, m); records_grouped.set(key, month_map);
}); });
let estimates_grouped = Map.groupBy(estimates, (v) => { return v.year }); let estimates_grouped: Map<number, Map<number, number>>= new Map();
estimates_grouped.forEach((value, key, map) => { Map.groupBy(estimates, (v) => { return v.year }).forEach((value, key, _) => {
let arr = value.map((v) => [ let arr = value.map((v) => [
{ key: ((v.quarter - 1) * 3 + 2), value: v["estimate_2"] }, { key: ((v.quarter - 1) * 3 + 2), value: v["estimate_2"] },
{ key: ((v.quarter - 1) * 3 + 1), value: v["estimate_1"] }, { key: ((v.quarter - 1) * 3 + 1), value: v["estimate_1"] },
{ key: ((v.quarter - 1) * 3 + 0), value: v["estimate_0"] }, { key: ((v.quarter - 1) * 3 + 0), value: v["estimate_0"] },
]).flat(); ]).flat();
let m = Map.groupBy(arr, (e) => e.key); let m: Map<number, number> = new Map();
m.forEach((value, key, map) => { Map.groupBy(arr, (e) => e.key).forEach((value, key, _) => {
map.set(key, value[0].value); m.set(key, value[0].value);
}) })
map.set(key, m); estimates_grouped.set(key, m);
}) })
let documents_grouped = Map.groupBy(documents, (v) => v.year ); let documents_grouped: Map<number, Map<number, FileProperties>> = new Map();
documents_grouped.forEach((value, key, map) => { Map.groupBy(documents, (v) => v.year ).forEach((value, key, _) => {
let m = Map.groupBy(value, (v) => v.month); let m: Map<number, FileProperties> = new Map()
m.forEach((value, key, map) => { Map.groupBy(value, (v) => v.month).forEach((value, key, _) => {
map.set(key, value.map((v) => v.file)[0]); m.set(key, value.map((v) => v.file)[0]);
}) })
map.set(key, m); documents_grouped.set(key, m);
}) })
return { return {
@ -58,6 +61,9 @@ export const load: PageServerLoad = async ({ locals }) => {
export const actions = { export const actions = {
new_entry: async ({locals, request}) => { new_entry: async ({locals, request}) => {
if (!locals.user) {
return fail(403, { message: "Unauthorized user" })
}
const ok = "ok"; const ok = "ok";
const missing = "missing"; const missing = "missing";
@ -65,12 +71,12 @@ export const actions = {
const data = await request.formData(); const data = await request.formData();
let date = data.get("date"); let date = data.get("date") as string;
let start = data.get("start"); let start = data.get("start") as string;
let end = data.get("end"); let end = data.get("end") as string;
let comment = data.get("comment"); let comment = data.get("comment") as string;
let return_obj = { let return_obj: Map<string, { status: string, value: string }> = new Map(Object.entries({
date : { date : {
status: ok, status: ok,
value: date value: date
@ -87,27 +93,27 @@ export const actions = {
status: ok, status: ok,
value: comment value: comment
}, },
} }))
if (date == null) { if (date == null) {
return_obj.date.status = missing; return_obj.get("date")!.status = missing;
} else if (parseDate(date) == null) { } else if (parseDate(date) == null) {
return_obj.date.status = invalid; return_obj.get("date")!.status = invalid;
} }
if (start == null) { if (start == null) {
return_obj.start.status = missing; return_obj.get("start")!.status = missing;
} else if (!isTimeValidHHMM(start)) { } else if (!isTimeValidHHMM(start)) {
return_obj.start.status = invalid; return_obj.get("start")!.status = invalid;
} }
if (end == null) { if (end == null) {
return_obj.end.status = missing; return_obj.get("end")!.status = missing;
} else if (!isTimeValidHHMM(end)) { } else if (!isTimeValidHHMM(end)) {
return_obj.end.status = invalid; return_obj.get("end")!.status = invalid;
} }
if (Object.keys(return_obj).some((v) => { return return_obj[v].status != ok; })) { if ([...return_obj.values()].some((v) => { return v.status != ok; })) {
return fail(400, { new_entry: return_obj }); return fail(400, { new_entry: return_obj });
} }
@ -117,9 +123,12 @@ export const actions = {
return fail(500, { }) return fail(500, { })
} }
return { new_entry: { success: true } }; return { success: true, new_entry: return_obj };
}, },
edit_entry: async ({locals, request}) => { edit_entry: async ({locals, request}) => {
if (!locals.user) {
return fail(403, { message: "Unauthorized user" })
}
const ok = "ok"; const ok = "ok";
const missing = "missing"; const missing = "missing";
@ -127,13 +136,13 @@ export const actions = {
const data = await request.formData(); const data = await request.formData();
let id = data.get("id"); let id = data.get("id") as string;
let date = data.get("date"); let date = data.get("date") as string;
let start = data.get("start"); let start = data.get("start") as string;
let end = data.get("end"); let end = data.get("end") as string;
let comment = data.get("comment"); let comment = data.get("comment") as string;
let return_obj = { let return_obj : Map<string, { status: string, value: string }> = new Map(Object.entries({
id: { id: {
status: ok, status: ok,
value: id, value: id,
@ -154,53 +163,52 @@ export const actions = {
status: ok, status: ok,
value: comment value: comment
}, },
}))
}
if (id == null) { if (id == null) {
return_obj.id.status = missing; return_obj.get("id")!.status = missing;
} else if (isNaN(toInt(id))) { } else if (isNaN(toInt(id))) {
return_obj.id.status = invalid; return_obj.get("id")!.status = invalid;
} }
if (date == null) { if (date == null) {
return_obj.date.status = missing; return_obj.get("date")!.status = missing;
} else if (parseDate(date) == null) { } else if (parseDate(date) == null) {
return_obj.date.status = invalid; return_obj.get("date")!.status = invalid;
} }
if (start == null) { if (start == null) {
return_obj.start.status = missing; return_obj.get("start")!.status = missing;
} else if (!isTimeValidHHMM(start)) { } else if (!isTimeValidHHMM(start)) {
return_obj.start.status = invalid; return_obj.get("start")!.status = invalid;
} }
if (end == null) { if (end == null) {
return_obj.end.status = missing; return_obj.get("end")!.status = missing;
} else if (!isTimeValidHHMM(end)) { } else if (!isTimeValidHHMM(end)) {
return_obj.end.status = invalid; return_obj.get("end")!.status = invalid;
} }
if (Object.keys(return_obj).some((v) => { return return_obj[v].status != ok; })) { if ([...return_obj.values()].some((v) => { return v.status != ok; })) {
return fail(400, { edit_entry: return_obj }); return fail(400, { edit_entry: return_obj });
} }
id = toInt(id); let user_id = toInt(id);
let current = locals.user.get_entry(id); let current = locals.user.get_entry(user_id);
if (!current) { if (!current) {
return fail(404, { new_entry: return_obj }); return fail(404, { edit_entry: return_obj });
} }
if (current.id == id && current.date == date && current.start == start && current.end == end && current.comment == comment) { if (current.id == user_id && current.date == date && current.start == start && current.end == end && current.comment == comment) {
return { success: false, edit_entry: { return_obj } } return { success: false, edit_entry: return_obj }
} }
let res = false; let res = false;
try { try {
res = locals.user.update_entry(id, date, start, end, comment); res = locals.user.update_entry(user_id, date, start, end, comment);
} catch (e) { } catch (e) {
if (!(e instanceof SQLiteError)) { if (!(e instanceof SQLiteError)) {
throw e; throw e;
@ -215,23 +223,27 @@ export const actions = {
return { success: true }; return { success: true };
}, },
remove_entry: async ({ locals, request })=> { remove_entry: async ({ locals, request })=> {
if (!locals.user) {
return fail(403, { message: "Unauthorized user" })
}
const data = await request.formData(); const data = await request.formData();
let id = data.get("id"); let id = data.get("id") as string;
if (id == null) { if (id == null) {
return fail(400, { id: id }); return fail(400, { id: id });
} }
id = toInt(id); let user_id = toInt(id);
if (isNaN(id)) { if (isNaN(user_id)) {
return fail(400, { id: id }); return fail(400, { id: id });
} }
let res = false; let res = false;
try { try {
res = locals.user.remove_entry(id); res = locals.user.remove_entry(user_id);
} catch (e) { } catch (e) {
if (!(e instanceof SQLiteError)) { if (!(e instanceof SQLiteError)) {
throw e; throw e;

View File

@ -14,9 +14,6 @@
let { data, form } : PageProps = $props(); let { data, form } : PageProps = $props();
//$inspect(data);
const HEADERS: string[] = [ "Datum", "Wochentag", "Beginn", "Ende", "Dauer", "Anmerkung" ];
const status_ok = "ok"; const status_ok = "ok";
const status_missing = "missing"; const status_missing = "missing";
@ -42,22 +39,43 @@
}) })
function setNewState() { function setNewState() {
new_state = { if (form?.success != null) {
date: { new_state = {
valid: form?.new_entry?.date?.value !== "" || form?.new_entry?.date?.valid, date: {
value: form?.new_entry?.date?.value ?? "", valid: true,
}, value: ""
start: { },
valid: form?.new_entry?.start?.value !== "" || form?.new_entry?.start?.valid, start: {
value: form?.new_entry?.start?.value ?? "", valid: true,
}, value: ""
end: { },
valid: form?.new_entry?.end?.value !== "" || form?.new_entry?.end?.valid, end: {
value: form?.new_entry?.end?.value ?? "", valid: true,
}, value: ""
comment: { },
value: form?.new_entry?.date?.comment ?? "", comment: {
}, value: ""
}
}
return
} else {
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 ?? "",
},
}
} }
} }

View File

@ -6,6 +6,11 @@ import { toInt } from "$lib/util"
import { getAllFiles, getRecordFiles, getEstimateFiles, generateEstimatePDF, generateRecordPDF } from "$lib/server/docstore"; import { getAllFiles, getRecordFiles, getEstimateFiles, generateEstimatePDF, generateRecordPDF } from "$lib/server/docstore";
export const load: PageServerLoad = async ({ locals }) => { export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user) {
return fail(403, { message: "Unauthorized user" })
}
return { return {
documents: await getAllFiles(locals.user), documents: await getAllFiles(locals.user),
records: await getRecordFiles(locals.user), records: await getRecordFiles(locals.user),
@ -14,11 +19,15 @@ export const load: PageServerLoad = async ({ locals }) => {
} }
export const actions = { export const actions = {
create_estimate: async ({ locals, request }) => { create_estimate: async ({ locals, request }) => {
if (!locals.user) {
return fail(403, { message: "Unauthorized user" })
}
const data = await request.formData(); const data = await request.formData();
const quarter = toInt(data.get("quarter") ?? ""); const quarter = toInt(data.get("quarter") as string ?? "");
const year = toInt(data.get("year") ?? ""); const year = toInt(data.get("year") as string ?? "");
if (isNaN(year) || isNaN(quarter) || quarter < 1 || quarter > 4) { if (isNaN(year) || isNaN(quarter) || quarter < 1 || quarter > 4) {
return fail(400, { success: false, message: "Invalid parameter", year: year, quarter: quarter }); return fail(400, { success: false, message: "Invalid parameter", year: year, quarter: quarter });
@ -26,7 +35,7 @@ export const actions = {
try { try {
await generateEstimatePDF(locals.user, year, quarter); await generateEstimatePDF(locals.user, year, quarter);
} catch (e) { } catch (e: any) {
console.log(e); console.log(e);
return fail(403, { success: false, message: e.toString(), year: year, quarter: quarter }); return fail(403, { success: false, message: e.toString(), year: year, quarter: quarter });
} }
@ -34,10 +43,14 @@ export const actions = {
return { success: true }; return { success: true };
}, },
create_record: async ({ locals, request }) => { create_record: async ({ locals, request }) => {
if (!locals.user) {
return fail(403, { message: "Unauthorized user" })
}
const data = await request.formData(); const data = await request.formData();
const month = toInt(data.get("month") ?? ""); const month = toInt(data.get("month") as string|null ?? "");
const year = toInt(data.get("year") ?? ""); const year = toInt(data.get("year") as string|null ?? "");
if (isNaN(year) || isNaN(month) || month < 1 || month > 12) { if (isNaN(year) || isNaN(month) || month < 1 || month > 12) {
return fail(400, { success: false, message: "Invalid parameter", year: year, month: month }); return fail(400, { success: false, message: "Invalid parameter", year: year, month: month });
@ -45,14 +58,13 @@ export const actions = {
try { try {
await generateRecordPDF(locals.user, year, month); await generateRecordPDF(locals.user, year, month);
} catch (e) { } catch (e: any) {
console.log(e); console.log(e);
return fail(403, { success: false, message: e.toString(), year: year, month: month }); return fail(403, { success: false, message: e.toString(), year: year, month: month });
} }
redirect(303, "dokumente") redirect(303, "dokumente")
return { success: true };
} }
} satisfies Actions; } satisfies Actions;

View File

@ -1,24 +1,21 @@
<script lang="ts"> <script lang="ts">
import type { PageProps } from "./$types"; import type { PageProps } from "./$types";
import type { FileProperties } from "$lib/server/docstore";
import { enhance } from "$app/forms";
import { isoToLocalDate, padInt } from "$lib/util"; import { isoToLocalDate, padInt } from "$lib/util";
import Expander from "./expander.svelte"; import Expander from "./expander.svelte";
let { data, form } : PageProps = $props(); let { data } : PageProps = $props();
$inspect(data);
</script> </script>
<div> <div>
<h1>Dokumente</h1> <h1>Dokumente</h1>
<form id="form_download" method="GET" > <form id="form_download" method="GET" ></form>
{#snippet table(first_cell_name: string, rows)} {#snippet table(first_cell_name: string, rows: FileProperties[])}
<table> <table>
<thead> <thead>
<tr> <tr>
@ -32,7 +29,7 @@
{#each rows as file} {#each rows as file}
<tr> <tr>
<td>{file.identifier}</td> <td>{file.identifier}</td>
<td>{file.filename}</td> <td>{file.name}</td>
<td>{isoToLocalDate(file.cdate.toISOString())}<br/>um {padInt(file.cdate.getHours(), 2)}:{padInt(file.cdate.getMinutes(), 2)}:{padInt(file.cdate.getSeconds(), 2)}</td> <td>{isoToLocalDate(file.cdate.toISOString())}<br/>um {padInt(file.cdate.getHours(), 2)}:{padInt(file.cdate.getMinutes(), 2)}:{padInt(file.cdate.getSeconds(), 2)}</td>
<td> <td>
<form method="GET" action={`dokumente/${file.path}`} target="_blank"> <form method="GET" action={`dokumente/${file.path}`} target="_blank">
@ -86,78 +83,6 @@
</Expander> </Expander>
</div> </div>
<div style:height="100px"></div>
<!--<form method="POST" id="create_est" action="?/create_estimate" use:enhance></form>
<form method="POST" id="create_rec" action="?/create_record" use:enhance></form>
<table>
<caption>Dokument erstellen</caption>
<tbody>
<tr>
<td>Stundenliste</td>
<td>
<input form="create_rec" style:width="2ch" name="month" /> -
<input form="create_rec" style:width="4ch" name="year" placeholder="Jahr" value={new Date().getFullYear()} />
</td>
<td>
<button form="create_rec" type="submit">Erstellen</button>
</td>
</tr>
<tr>
<td>Stundenschätzung</td>
<td>
<input form="create_est" style:width="1ch" name="quarter" />. Quartal
<input form="create_est" style:width="4ch" name="year" placeholder="Jahr" value={new Date().getFullYear()} />
</td>
<td>
<button form="create_est" type="submit">Erstellen</button>
</td>
</tr>
</tbody>
</table>
<div style:width="100%" style="text-align: center;">{!(form?.success) ? form?.message : "Dokument erstellt."}</div>
<div style:height="20px"></div>
<table>
<caption>Erstellte Dokumente</caption>
<thead>
<tr>
<th style:width="30ch">Dateiname</th>
<th style:width="15ch">Erstellt</th>
<th style:width="12ch">Aktion</th>
</tr>
</thead>
<tbody>
{#each data.documents as doc}
<tr>
<td>{doc.name}</td>
<td>{`${isoToLocalDate(doc.timestamp.toISOString())} um ${padInt(doc.timestamp.getHours(), 2)}:${padInt(doc.timestamp.getMinutes(), 2)}:${padInt(doc.timestamp.getSeconds(), 2)}`}</td>
<td>
<form method="GET" action={`dokumente/${doc.path}`}>
<button type="submit">Download</button>
</form>
</td>
</tr>
{/each}
<tr></tr>
</tbody>
{#if data.documents.length === 0}
<tfoot>
<tr>
<td class="td-no-elements" colspan="999">Noch keine Dokumente</td>
</tr>
</tfoot>
{/if}
</table> -->
</div> </div>
<style> <style>
@ -187,10 +112,6 @@ tbody tr {
border-bottom: 1px solid black; border-bottom: 1px solid black;
} }
tbody tr th {
text-align: center;
}
tbody tr td { tbody tr td {
vertical-align: middle; vertical-align: middle;
text-align: center; text-align: center;

View File

@ -1,13 +1,16 @@
import type { RequestHandler } from "./$types"; import type { RequestHandler } from "./$types";
import { redirect } from "@sveltejs/kit" import { fail } from "@sveltejs/kit"
import { getFile } from "$lib/server/docstore" import { getFile } from "$lib/server/docstore"
export const GET: RequestHandler = async ({ locals, url, params }) => { export const GET: RequestHandler = async ({ locals, params }) => {
if (!locals.user) {
return fail(403, { message: "Unauthorized user" })
}
const file = await getFile(locals.user, params.file); const file = await getFile(locals.user, params.file);
//redirect(307, "/dokumente")
return new Response(file); return new Response(file);
} }

View File

@ -2,7 +2,7 @@
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
let { id, label, children } : { label: string, children: Snippet } = $props(); let { id, label, children } : { id: string, label: string, children: Snippet } = $props();
let _id = id + "_check"; let _id = id + "_check";
</script> </script>

View File

@ -0,0 +1,73 @@
import type { Actions } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
import { fail, redirect } from "@sveltejs/kit";
import { authorize_password } from "$lib/server/auth";
import SessionStore from "$lib/server/session_store";
export const load: PageServerLoad = ({ locals, url }) => {
let redirect_url = url.searchParams.get("redirect") ?? "/";
if (locals.user != null) {
redirect(302, redirect_url);
}
}
export const actions = {
login: async ({ locals, request, cookies, url }) => {
let redirect_url = url.searchParams.get("redirect") ?? "/";
if (locals.user != null) {
redirect(302, redirect_url);
}
console.log("logging in");
const params = await request.formData();
const username = params.get("username") as string | null;
const password = params.get("password") as string | null;
if (username == null || password == null) {
return fail(400, { message: "Invalid request" });
}
const user = await authorize_password(username, password);
if (user == null) {
return fail(403, { message: "Benutzername oder Passwort falsch.", username: username })
}
const expiry_date = new Date(Date.now() + 15*60*1000)
const token = SessionStore.issue_access_token_for_user(user, expiry_date)
cookies.set("session_id", token, {
expires: expiry_date,
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/'
})
redirect(302, redirect_url);
},
logout: async ({ locals, cookies }) => {
if (locals.user == null) {
return fail(403, { message: "Not logged in." });
}
const token = cookies.get("session_id");
if (!token) {
console.log("how is this user logged in right now?");
return fail(500);
}
SessionStore.logout_user_session(token);
cookies.delete("session_id", { path: "/" });
return redirect(302, "/login");
}
} satisfies Actions;

View File

@ -0,0 +1,48 @@
<script lang="ts">
import type { PageProps } from "./$types";
let { form }: PageProps = $props();
</script>
<div>
<h1>Login</h1>
<form id="login" method="POST" action="?/login">
<table>
<tbody>
<tr>
<td><label for="username">Benutzername:</label></td>
<td><input type="text" id="username" name="username" value={form?.username ?? ""} required /></td>
</tr>
<tr>
<td><label for="password">Passwort:</label></td>
<td><input type="password" id="password" name="password" required /></td>
</tr>
<tr>
<td colspan="2"><button type="submit">Login</button></td>
</tr>
</tbody>
</table>
</form>
</div>
<style>
div {
position: absolute;
top: 33%;
left: 50%;
transform: translate(-50%, -50%);
}
table {
margin: auto;
}
tr td:first-of-type {
padding-right: 10px;
}
button {
margin: 0 auto;
padding: 0 auto;
}
</style>

View File

@ -497,135 +497,127 @@
</script> </script>
<!--<tr> <td>
{#each { length: prepend_td }, n} <!-- svelte-ignore a11y_autofocus -->
<td></td> <input
{/each}--> bind:this={dateInput}
<td> bind:value={states.date.value}
<input class:form-invalid={!states.date.valid}
bind:this={dateInput} name="date"
bind:value={states.date.value} type="text"
class:form-invalid={!states.date.valid} form={targetForm}
name="date" onfocusin={
type="text" (_) => {
form={targetForm} dateInput.select();
onfocusin={ states.date.valid = true;
(_) => {
dateInput.select();
states.date.valid = true;
}
} }
onfocusout={ }
(_) => { onfocusout={
(_) => {
states.date.valid = validateDate(dateInput);
states.date.value = dateInput.value;
}
}
onkeydown={
(event) => {
if (event.key == "Enter") {
states.date.valid = validateDate(dateInput); states.date.valid = validateDate(dateInput);
states.date.value = dateInput.value; states.date.value = dateInput.value;
} }
} }
onkeydown={ }
(event) => { disabled={!enabled}
if (event.key == "Enter") { {autofocus}
states.date.valid = validateDate(dateInput); required>
states.date.value = dateInput.value; </td>
}
}
}
disabled={!enabled}
{autofocus}
required>
</td>
<td> <td>
{inWeekDay} {inWeekDay}
</td> </td>
<td> <td>
<input <input
bind:this={startInput} bind:this={startInput}
bind:value={states.start.value} bind:value={states.start.value}
class:form-invalid={!states.start.valid} class:form-invalid={!states.start.valid}
name="start" name="start"
type="text" type="text"
form={targetForm} form={targetForm}
onfocusin={ onfocusin={
(_) => { (_) => {
startInput.select(); startInput.select();
states.start.valid = true; states.start.valid = true;
}
} }
onfocusout={ }
(_) => { onfocusout={
(_) => {
states.start.valid = validateTime(startInput);
states.start.value = startInput.value;
}
}
onkeydown={
(event) => {
if (event.key == "Enter") {
states.start.valid = validateTime(startInput); states.start.valid = validateTime(startInput);
states.start.value = startInput.value; states.start.value = startInput.value;
} }
} }
onkeydown={ }
(event) => { disabled={!enabled}
if (event.key == "Enter") { required>
states.start.valid = validateTime(startInput); </td>
states.start.value = startInput.value;
}
}
}
disabled={!enabled}
required>
</td>
<td> <td>
<input <input
bind:this={endInput} bind:this={endInput}
bind:value={states.end.value} bind:value={states.end.value}
class:form-invalid={!states.end.valid} class:form-invalid={!states.end.valid}
name="end" name="end"
type="text" type="text"
form={targetForm} form={targetForm}
onfocusin={ onfocusin={
(_) => { (_) => {
endInput.select(); endInput.select();
states.end.valid = true; states.end.valid = true;
}
} }
onfocusout={ }
(_) => { onfocusout={
(_) => {
states.end.valid = validateTime(endInput);
states.end.value = endInput.value;
}
}
onkeydown={
(event) => {
if (event.key == "Enter") {
states.end.valid = validateTime(endInput); states.end.valid = validateTime(endInput);
states.end.value = endInput.value; states.end.value = endInput.value;
} }
} }
onkeydown={ }
(event) => { disabled={!enabled}
if (event.key == "Enter") { required>
states.end.valid = validateTime(endInput); </td>
states.end.value = endInput.value;
}
}
}
disabled={!enabled}
required>
</td>
<td> <td>
{inDuration} {inDuration}
</td> </td>
<td> <td>
<input <input
name="comment" name="comment"
type="text" type="text"
form={targetForm} form={targetForm}
value={states.comment.value} value={states.comment.value}
disabled={!enabled}> disabled={!enabled}>
</td> </td>
<td class="action"> <td class="action">
{@render children?.()} {@render children?.()}
</td> </td>
<!--</tr>-->
<style> <style>
/* * {
border: 1px solid;
}*/
td input { td input {
box-sizing: border-box; box-sizing: border-box;

View File

@ -1,4 +1,5 @@
import type { PageServerLoad, Actions } from "./$types"; import type { PageServerLoad, Actions } from "./$types";
import type { FileProperties } from "$lib/server/docstore"
import { fail } from '@sveltejs/kit'; import { fail } from '@sveltejs/kit';
@ -7,30 +8,34 @@ import { MONTHS, toInt, toFloat } from "$lib/util";
import { getEstimateFiles } from "$lib/server/docstore"; import { getEstimateFiles } from "$lib/server/docstore";
export const load: PageServerLoad = async ({ locals }) => { export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user) {
return fail(403, { message: "Unauthorized user" })
}
let estimates = locals.user.get_estimates(); let estimates = locals.user.get_estimates();
let documents = await getEstimateFiles(locals.user); let documents = await getEstimateFiles(locals.user);
let estimates_grouped = Map.groupBy(estimates, (v) => v.year); let estimates_grouped: Map<number, Map<number, {month: string, estimate: number}[]>> = new Map();
estimates_grouped.forEach((value, key, map) => { Map.groupBy(estimates, (v) => v.year).forEach((value, key, _) => {
let quarters = Map.groupBy(value, (v) => v.quarter) let quarters = new Map()
quarters.forEach((_value, _key, _map) => { Map.groupBy(value, (v) => v.quarter).forEach((_value, _key, _) => {
let months = _value.map((v) => [ let months = _value.map((v) => [
{ month: MONTHS[(v.quarter - 1) * 3 + 2], estimate: v.estimate_2 }, { month: MONTHS[(v.quarter - 1) * 3 + 2], estimate: v.estimate_2 },
{ month: MONTHS[(v.quarter - 1) * 3 + 1], estimate: v.estimate_1 }, { month: MONTHS[(v.quarter - 1) * 3 + 1], estimate: v.estimate_1 },
{ month: MONTHS[(v.quarter - 1) * 3 + 0], estimate: v.estimate_0 }, { month: MONTHS[(v.quarter - 1) * 3 + 0], estimate: v.estimate_0 },
] ).flat(); ] ).flat();
_map.set(_key, months); quarters.set(_key, months);
}) })
map.set(key, quarters); estimates_grouped.set(key, quarters);
}) })
let documents_grouped = Map.groupBy(documents, (v) => toInt(v.identifier.slice(0, 4))) let documents_grouped: Map<number, Map<number, FileProperties[]>> = new Map()
documents_grouped.forEach((value, key, map) => { Map.groupBy(documents, (v) => toInt(v.identifier.slice(0, 4))).forEach((value, key, _) => {
let quarters = Map.groupBy(value, (v) => toInt(v.identifier.slice(5, 7))); let quarters = Map.groupBy(value, (v) => toInt(v.identifier.slice(5, 7)));
map.set(key, quarters); documents_grouped.set(key, quarters);
}) })
return { return {
@ -40,17 +45,18 @@ export const load: PageServerLoad = async ({ locals }) => {
} }
export const actions = { export const actions = {
add_quarter: async ({ locals, request }) => { add_quarter: async ({ locals, request }) => {
if (!locals.user) {
return fail(403, { message: "Unauthorized user" })
}
const data = await request.formData(); const data = await request.formData();
const year = data.get("year"); const year = data.get("year") as string;
const quart = data.get("quarter"); const quart = data.get("quarter") as string;
const estimate_0 = data.get("estimate_0"); const estimate_0 = data.get("estimate_0") as string;
const estimate_1 = data.get("estimate_1"); const estimate_1 = data.get("estimate_1") as string;
const estimate_2 = data.get("estimate_2"); const estimate_2 = data.get("estimate_2") as string;
console.log(data);
if (year == null || quart == null || estimate_0 == null || estimate_1 == null || estimate_2 == null) { if (year == null || quart == null || estimate_0 == null || estimate_1 == null || estimate_2 == null) {
return fail(400, { year: year, quarter: quart, estimate_0: estimate_0, estimate_1: estimate_1, estimate_2: estimate_2 }); return fail(400, { year: year, quarter: quart, estimate_0: estimate_0, estimate_1: estimate_1, estimate_2: estimate_2 });

View File

@ -4,9 +4,7 @@
import { enhance } from "$app/forms"; import { enhance } from "$app/forms";
import { MONTHS, toInt, toFloat, padInt } from "$lib/util"; import { MONTHS, toInt, toFloat, padInt } from "$lib/util";
let { form, data }: PageProps = $props(); let { data }: PageProps = $props();
$inspect(data);
let next = $state((() => { let next = $state((() => {
if (data.estimates.size == 0) { if (data.estimates.size == 0) {
@ -25,7 +23,6 @@
estimate_2: { value: "" }, estimate_2: { value: "" },
editing: { value: "" } editing: { value: "" }
}) })
$inspect(estimate_store);
function validate_year(event: InputEvent) { function validate_year(event: InputEvent) {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
@ -55,20 +52,6 @@
} }
} }
/*const TODAY = new Date();
const NEXT_QUART = (data?.estimates?.length > 0)
? new Date(data.estimates[0].year + (data.estimates[0].quarter === 4 ? 1 : 0), 4 * (data.estimates[0].quarter - (data.estimates[0].quarter === 4 ? -4 : 0)))
: TODAY;
const add_quart = quart_from_date(NEXT_QUART);
function quart_from_date(date: Date) {
return Math.floor((date.getMonth()+1) / 4) + 1;
}
function month_from_quart(quart: number, num: number) {
return new Date(TODAY.getFullYear(), (quart - 1) * 4 + num);
}*/
</script> </script>
<form id="form_add_quart" method="POST" action="?/add_quarter" use:enhance></form> <form id="form_add_quart" method="POST" action="?/add_quarter" use:enhance></form>
@ -87,20 +70,20 @@
<tbody> <tbody>
<tr> <tr>
<td rowspan="3">Neue Schätzung: <input form="form_add_quart" style:width="7ch" type="number" name="year" oninput={validate_year} value={next.year} tabindex="8"/> - <input form="form_add_quart" style:width="4ch" type="number" name="quarter" oninput={validate_quarter} value={new_quart} tabindex="9"/>. Quartal</td> <td rowspan="3">Neue Schätzung: <input form="form_add_quart" style:width="7ch" type="number" name="year" oninput={validate_year as any} value={next.year} tabindex="8"/> - <input form="form_add_quart" style:width="4ch" type="number" name="quarter" oninput={validate_quarter as any} value={new_quart} tabindex="9"/>. Quartal</td>
<td>{MONTHS[(new_quart - 1) * 3 + 0]}</td> <td>{MONTHS[(new_quart - 1) * 3 + 0]}</td>
<td><input form="form_add_quart" type="text" name="estimate_0" oninput={(e) => validate_estimate(e, estimate_store.estimate_0)} tabindex="10" /></td> <td><input form="form_add_quart" type="text" name="estimate_0" oninput={(e: any) => validate_estimate(e, estimate_store.estimate_0)} tabindex="10" /></td>
<td rowspan="3"><button form="form_add_quart" type="submit" tabindex="13">Erstellen</button></td> <td rowspan="3"><button form="form_add_quart" type="submit" tabindex="13">Erstellen</button></td>
</tr> </tr>
<tr> <tr>
<td>{MONTHS[(new_quart - 1) * 3 + 1]}</td> <td>{MONTHS[(new_quart - 1) * 3 + 1]}</td>
<td><input form="form_add_quart" type="text" name="estimate_1" oninput={(e) => validate_estimate(e, estimate_store.estimate_1)} tabindex="11" /></td> <td><input form="form_add_quart" type="text" name="estimate_1" oninput={(e: any) => validate_estimate(e, estimate_store.estimate_1)} tabindex="11" /></td>
</tr> </tr>
<tr> <tr>
<td>{MONTHS[(new_quart - 1) * 3 + 2]}</td> <td>{MONTHS[(new_quart - 1) * 3 + 2]}</td>
<td><input form="form_add_quart" type="text" name="estimate_2" oninput={(e) => validate_estimate(e, estimate_store.estimate_2)} tabindex="12" /></td> <td><input form="form_add_quart" type="text" name="estimate_2" oninput={(e: any) => validate_estimate(e, estimate_store.estimate_2)} tabindex="12" /></td>
</tr> </tr>
</tbody> </tbody>
@ -142,7 +125,7 @@
<td class="action" rowspan="3"> <td class="action" rowspan="3">
{#if data.documents?.get(year)?.get(quarter)?.[0] != null} {#if data.documents?.get(year)?.get(quarter)?.[0] != null}
{@const document = data.documents.get(year).get(quarter)[0]} {@const document = data.documents!.get(year)!.get(quarter)![0]}
<form method="GET" action={`/dokumente/${document.path}`}> <form method="GET" action={`/dokumente/${document.path}`}>
<button type="submit">Download PDF</button> <button type="submit">Download PDF</button>
</form> </form>
@ -167,60 +150,15 @@
{/each} {/each}
</tbody> </tbody>
{#if data.estimates === undefined || data.estimates.size === 0} {#if data.estimates.size === 0}
<tfoot> <tfoot>
<tr> <tr>
<td class="td-no-elements" colspan="999">No records</td> <td class="td-no-elements" colspan="999">No records</td>
</tr> </tr>
</tfoot> </tfoot>
{/if} {/if}
</table> </table>
{"" /*<tr>
<td rowspan="3">{add_quart}. Quartal {NEXT_QUART.getFullYear()}</td>
<td>{month_of(month_from_quart(add_quart, 0))}</td>
<!-- svelte-ignore a11y_positive_tabindex -->
<td><input form="form_add_quart" type="text" tabindex="1" name="estimate_0" value={form?.data?.estimate_0}/></td>
<td rowspan="3">
<input form="form_add_quart" type="hidden" name="year" value={NEXT_QUART.getFullYear()}/>
<input form="form_add_quart" type="hidden" name="quarter" value={add_quart}/>
<button form="form_add_quart" type="submit">+</button>
</td>
</tr>
<tr>
<td>{month_of(month_from_quart(add_quart, 1))}</td>
<!-- svelte-ignore a11y_positive_tabindex -->
<td><input form="form_add_quart" type="text" tabindex="2" name="estimate_1" value={form?.data?.estimate_1}/></td>
</tr>
<tr>
<td>{month_of(month_from_quart(add_quart, 2))}</td>
<!-- svelte-ignore a11y_positive_tabindex -->
<td><input form="form_add_quart" type="text" tabindex="3" name="estimate_2" value={form?.data?.estimate_2}/></td>
</tr>*/}
{"" /*
{#if months.length > 0}
<tr>
<td rowspan={rowspan}>
{quarter.quarter}. Quartal {quarter.year}
</td>
<td>{month_of(month_from_quart(quarter.quarter, months[0].i))}</td>
<td>{months[0].estimate.toFixed(2)}</td>
<td rowspan={rowspan}>Edit</td>
</tr>
{#each months.slice(1) as month}
<tr>
<td>{month_of(month_from_quart(quarter.quarter, month.i))}</td>
<td>{month.estimate.toFixed(2)}</td>
</tr>
{/each}
{/if}*/}
<style> <style>
form { form {

View File

@ -12,7 +12,9 @@ const config = {
// If your environment is not supported, or you settled on a specific environment, switch out the adapter. // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters. // See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter() adapter: adapter()
} },
warningFilter: (warning) => !warning.code.startsWith('a11y')
}; };
export default config export default config