Compare commits
2 Commits
888e35f839
...
e2f011597d
| Author | SHA1 | Date |
|---|---|---|
|
|
e2f011597d | |
|
|
104cafbcec |
|
|
@ -1,6 +1,7 @@
|
|||
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";
|
||||
|
||||
async function init() {
|
||||
|
|
@ -40,10 +41,32 @@ process.on('SIGINT', (_) => {
|
|||
|
||||
export let handle: Handle = async function ({ event, resolve }) {
|
||||
|
||||
let user = get_user();
|
||||
console.log("incoming ", event.request.method, " request to: ", event.url.href, " (route id: ", event.route.id, ")");
|
||||
|
||||
if (!user) {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import fs from "node:fs/promises";
|
||||
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";
|
||||
|
||||
const DATABASES_PATH: string = (process.env.APP_USER_DATA_PATH ?? ".") + "/databases/";
|
||||
|
|
@ -11,22 +11,54 @@ const CHECK_QUERY: string =
|
|||
"SELECT * FROM sqlite_master;";
|
||||
|
||||
const USER_DATABASE_SETUP: string[] = [
|
||||
"PRAGMA foreign_keys = ON;",
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
gender TEXT,
|
||||
name TEXT,
|
||||
gender TEXT,
|
||||
address TEXT,
|
||||
initials TEXT,
|
||||
username TEXT,
|
||||
password TEXT,
|
||||
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 =
|
||||
"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 =
|
||||
"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[] = [
|
||||
"PRAGMA foreign_keys = ON;",
|
||||
|
||||
|
|
@ -143,17 +175,19 @@ export class User {
|
|||
gender: string;
|
||||
name: string;
|
||||
address: string;
|
||||
initials: string;
|
||||
username: string;
|
||||
password: string
|
||||
created: string;
|
||||
|
||||
private database: Database;
|
||||
private _database: Database;
|
||||
|
||||
constructor(user: UserEntry, db: Database) {
|
||||
this.id = user.id;
|
||||
this.gender = user.gender;
|
||||
this.name = user.name;
|
||||
this.address = user.address;
|
||||
this.initials = user.initials;
|
||||
this.username = user.username;
|
||||
this.password = user.password;
|
||||
this.created = user.created;
|
||||
this._database = db;
|
||||
}
|
||||
|
|
@ -308,11 +342,14 @@ function setup_db(db: Database, setup_queries: string[]) {
|
|||
export async function init_db() {
|
||||
|
||||
const stdout = await fs.mkdir(DATABASES_PATH, { recursive: true });
|
||||
console.log(stdout)
|
||||
|
||||
user_database = new Database(USER_DATABASE_PATH, { strict: true, create: true });
|
||||
|
||||
if (!is_db_initialized(user_database)) {
|
||||
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 {
|
||||
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) {
|
||||
console.log(e);
|
||||
if (e instanceof SQLiteError) {
|
||||
|
|
@ -344,7 +383,7 @@ function _get_user(): UserEntry | null {
|
|||
|
||||
try {
|
||||
const statement = user_database.prepare(USER_DATABASE_GET_USER);
|
||||
const result: UserEntry = statement.get();
|
||||
const result = statement.get() as UserEntry;
|
||||
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -9,6 +9,14 @@
|
|||
<li><a href="/">Stundenliste</a></li>
|
||||
<li><a href="/schaetzung">Stundenschätzung</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>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
|
@ -50,12 +58,23 @@
|
|||
list-style-type: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav ul li {
|
||||
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 {
|
||||
width: 100%;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,52 +1,55 @@
|
|||
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 { fail, redirect } from '@sveltejs/kit';
|
||||
|
||||
import { MONTHS, toInt, parseDate, isTimeValidHHMM, month_of } from "$lib/util"
|
||||
import { get_user, User } from "$lib/server/database";
|
||||
import { toInt, parseDate, isTimeValidHHMM } from "$lib/util"
|
||||
import { getRecordFiles } from "$lib/server/docstore";
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
let records: { entry: RecordEntry, parsedDate: Date } = locals.user.get_entries().map((v) => ({ entry: v, parsedDate: parseDate(v.date) }));
|
||||
let estimates: EstimatesEntry = locals.user.get_estimates()
|
||||
if (!locals.user) {
|
||||
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 records_grouped = Map.groupBy(records, (v) => { return v.parsedDate.getFullYear() } );
|
||||
|
||||
records_grouped.forEach((value, key, map) => {
|
||||
let m = Map.groupBy(value, (v) => v.parsedDate.getMonth());
|
||||
let records_grouped: Map<number, Map<number, RecordEntry[]>> = new Map()
|
||||
Map.groupBy(records, (v) => { return v.parsedDate.getFullYear() } ).forEach((value, key, _) => {
|
||||
let month_map: Map<number, RecordEntry[]> = new Map()
|
||||
Map.groupBy(value, (v) => v.parsedDate.getMonth()).forEach((value, key, _) => {
|
||||
// remove parsed date
|
||||
m.forEach((value, key, map) => {
|
||||
map.set(key, value.map((v) => v.entry));
|
||||
month_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 });
|
||||
estimates_grouped.forEach((value, key, map) => {
|
||||
let estimates_grouped: Map<number, Map<number, number>>= new Map();
|
||||
Map.groupBy(estimates, (v) => { return v.year }).forEach((value, key, _) => {
|
||||
let arr = value.map((v) => [
|
||||
{ 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 + 0), value: v["estimate_0"] },
|
||||
]).flat();
|
||||
|
||||
let m = Map.groupBy(arr, (e) => e.key);
|
||||
m.forEach((value, key, map) => {
|
||||
map.set(key, value[0].value);
|
||||
let m: Map<number, number> = new Map();
|
||||
Map.groupBy(arr, (e) => e.key).forEach((value, key, _) => {
|
||||
m.set(key, value[0].value);
|
||||
})
|
||||
map.set(key, m);
|
||||
estimates_grouped.set(key, m);
|
||||
})
|
||||
|
||||
let documents_grouped = Map.groupBy(documents, (v) => v.year );
|
||||
documents_grouped.forEach((value, key, map) => {
|
||||
let m = Map.groupBy(value, (v) => v.month);
|
||||
m.forEach((value, key, map) => {
|
||||
map.set(key, value.map((v) => v.file)[0]);
|
||||
let documents_grouped: Map<number, Map<number, FileProperties>> = new Map();
|
||||
Map.groupBy(documents, (v) => v.year ).forEach((value, key, _) => {
|
||||
let m: Map<number, FileProperties> = new Map()
|
||||
Map.groupBy(value, (v) => v.month).forEach((value, key, _) => {
|
||||
m.set(key, value.map((v) => v.file)[0]);
|
||||
})
|
||||
map.set(key, m);
|
||||
documents_grouped.set(key, m);
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
@ -58,6 +61,9 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||
|
||||
export const actions = {
|
||||
new_entry: async ({locals, request}) => {
|
||||
if (!locals.user) {
|
||||
return fail(403, { message: "Unauthorized user" })
|
||||
}
|
||||
|
||||
const ok = "ok";
|
||||
const missing = "missing";
|
||||
|
|
@ -65,12 +71,12 @@ export const actions = {
|
|||
|
||||
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 date = data.get("date") as string;
|
||||
let start = data.get("start") as string;
|
||||
let end = data.get("end") as string;
|
||||
let comment = data.get("comment") as string;
|
||||
|
||||
let return_obj = {
|
||||
let return_obj: Map<string, { status: string, value: string }> = new Map(Object.entries({
|
||||
date : {
|
||||
status: ok,
|
||||
value: date
|
||||
|
|
@ -87,27 +93,27 @@ export const actions = {
|
|||
status: ok,
|
||||
value: comment
|
||||
},
|
||||
}
|
||||
}))
|
||||
|
||||
if (date == null) {
|
||||
return_obj.date.status = missing;
|
||||
return_obj.get("date")!.status = missing;
|
||||
} else if (parseDate(date) == null) {
|
||||
return_obj.date.status = invalid;
|
||||
return_obj.get("date")!.status = invalid;
|
||||
}
|
||||
|
||||
if (start == null) {
|
||||
return_obj.start.status = missing;
|
||||
return_obj.get("start")!.status = missing;
|
||||
} else if (!isTimeValidHHMM(start)) {
|
||||
return_obj.start.status = invalid;
|
||||
return_obj.get("start")!.status = invalid;
|
||||
}
|
||||
|
||||
if (end == null) {
|
||||
return_obj.end.status = missing;
|
||||
return_obj.get("end")!.status = missing;
|
||||
} 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 });
|
||||
}
|
||||
|
||||
|
|
@ -117,9 +123,12 @@ export const actions = {
|
|||
return fail(500, { })
|
||||
}
|
||||
|
||||
return { new_entry: { success: true } };
|
||||
return { success: true, new_entry: return_obj };
|
||||
},
|
||||
edit_entry: async ({locals, request}) => {
|
||||
if (!locals.user) {
|
||||
return fail(403, { message: "Unauthorized user" })
|
||||
}
|
||||
|
||||
const ok = "ok";
|
||||
const missing = "missing";
|
||||
|
|
@ -127,13 +136,13 @@ export const actions = {
|
|||
|
||||
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 id = data.get("id") as string;
|
||||
let date = data.get("date") as string;
|
||||
let start = data.get("start") as string;
|
||||
let end = data.get("end") as string;
|
||||
let comment = data.get("comment") as string;
|
||||
|
||||
let return_obj = {
|
||||
let return_obj : Map<string, { status: string, value: string }> = new Map(Object.entries({
|
||||
id: {
|
||||
status: ok,
|
||||
value: id,
|
||||
|
|
@ -154,53 +163,52 @@ export const actions = {
|
|||
status: ok,
|
||||
value: comment
|
||||
},
|
||||
|
||||
}
|
||||
}))
|
||||
|
||||
if (id == null) {
|
||||
return_obj.id.status = missing;
|
||||
return_obj.get("id")!.status = missing;
|
||||
} else if (isNaN(toInt(id))) {
|
||||
return_obj.id.status = invalid;
|
||||
return_obj.get("id")!.status = invalid;
|
||||
}
|
||||
|
||||
if (date == null) {
|
||||
return_obj.date.status = missing;
|
||||
return_obj.get("date")!.status = missing;
|
||||
} else if (parseDate(date) == null) {
|
||||
return_obj.date.status = invalid;
|
||||
return_obj.get("date")!.status = invalid;
|
||||
}
|
||||
|
||||
if (start == null) {
|
||||
return_obj.start.status = missing;
|
||||
return_obj.get("start")!.status = missing;
|
||||
} else if (!isTimeValidHHMM(start)) {
|
||||
return_obj.start.status = invalid;
|
||||
return_obj.get("start")!.status = invalid;
|
||||
}
|
||||
|
||||
if (end == null) {
|
||||
return_obj.end.status = missing;
|
||||
return_obj.get("end")!.status = missing;
|
||||
} 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 });
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
return { success: false, edit_entry: { return_obj } }
|
||||
if (current.id == user_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);
|
||||
res = locals.user.update_entry(user_id, date, start, end, comment);
|
||||
} catch (e) {
|
||||
if (!(e instanceof SQLiteError)) {
|
||||
throw e;
|
||||
|
|
@ -215,23 +223,27 @@ export const actions = {
|
|||
return { success: true };
|
||||
},
|
||||
remove_entry: async ({ locals, request })=> {
|
||||
if (!locals.user) {
|
||||
return fail(403, { message: "Unauthorized user" })
|
||||
}
|
||||
|
||||
const data = await request.formData();
|
||||
|
||||
let id = data.get("id");
|
||||
let id = data.get("id") as string;
|
||||
|
||||
if (id == null) {
|
||||
return fail(400, { id: id });
|
||||
}
|
||||
|
||||
id = toInt(id);
|
||||
if (isNaN(id)) {
|
||||
let user_id = toInt(id);
|
||||
if (isNaN(user_id)) {
|
||||
return fail(400, { id: id });
|
||||
}
|
||||
|
||||
let res = false;
|
||||
|
||||
try {
|
||||
res = locals.user.remove_entry(id);
|
||||
res = locals.user.remove_entry(user_id);
|
||||
} catch (e) {
|
||||
if (!(e instanceof SQLiteError)) {
|
||||
throw e;
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@
|
|||
|
||||
let { data, form } : PageProps = $props();
|
||||
|
||||
//$inspect(data);
|
||||
|
||||
const HEADERS: string[] = [ "Datum", "Wochentag", "Beginn", "Ende", "Dauer", "Anmerkung" ];
|
||||
|
||||
const status_ok = "ok";
|
||||
const status_missing = "missing";
|
||||
|
|
@ -42,6 +39,26 @@
|
|||
})
|
||||
|
||||
function setNewState() {
|
||||
if (form?.success != null) {
|
||||
new_state = {
|
||||
date: {
|
||||
valid: true,
|
||||
value: ""
|
||||
},
|
||||
start: {
|
||||
valid: true,
|
||||
value: ""
|
||||
},
|
||||
end: {
|
||||
valid: true,
|
||||
value: ""
|
||||
},
|
||||
comment: {
|
||||
value: ""
|
||||
}
|
||||
}
|
||||
return
|
||||
} else {
|
||||
new_state = {
|
||||
date: {
|
||||
valid: form?.new_entry?.date?.value !== "" || form?.new_entry?.date?.valid,
|
||||
|
|
@ -60,6 +77,7 @@
|
|||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setEditing(entry: RecordEntry | null = null) {
|
||||
if (entry) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ import { toInt } from "$lib/util"
|
|||
import { getAllFiles, getRecordFiles, getEstimateFiles, generateEstimatePDF, generateRecordPDF } from "$lib/server/docstore";
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
|
||||
if (!locals.user) {
|
||||
return fail(403, { message: "Unauthorized user" })
|
||||
}
|
||||
|
||||
return {
|
||||
documents: await getAllFiles(locals.user),
|
||||
records: await getRecordFiles(locals.user),
|
||||
|
|
@ -15,10 +20,14 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||
|
||||
export const actions = {
|
||||
create_estimate: async ({ locals, request }) => {
|
||||
if (!locals.user) {
|
||||
return fail(403, { message: "Unauthorized user" })
|
||||
}
|
||||
|
||||
const data = await request.formData();
|
||||
|
||||
const quarter = toInt(data.get("quarter") ?? "");
|
||||
const year = toInt(data.get("year") ?? "");
|
||||
const quarter = toInt(data.get("quarter") as string ?? "");
|
||||
const year = toInt(data.get("year") as string ?? "");
|
||||
|
||||
if (isNaN(year) || isNaN(quarter) || quarter < 1 || quarter > 4) {
|
||||
return fail(400, { success: false, message: "Invalid parameter", year: year, quarter: quarter });
|
||||
|
|
@ -26,7 +35,7 @@ export const actions = {
|
|||
|
||||
try {
|
||||
await generateEstimatePDF(locals.user, year, quarter);
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
return fail(403, { success: false, message: e.toString(), year: year, quarter: quarter });
|
||||
}
|
||||
|
|
@ -34,10 +43,14 @@ export const actions = {
|
|||
return { success: true };
|
||||
},
|
||||
create_record: async ({ locals, request }) => {
|
||||
if (!locals.user) {
|
||||
return fail(403, { message: "Unauthorized user" })
|
||||
}
|
||||
|
||||
const data = await request.formData();
|
||||
|
||||
const month = toInt(data.get("month") ?? "");
|
||||
const year = toInt(data.get("year") ?? "");
|
||||
const month = toInt(data.get("month") as string|null ?? "");
|
||||
const year = toInt(data.get("year") as string|null ?? "");
|
||||
|
||||
if (isNaN(year) || isNaN(month) || month < 1 || month > 12) {
|
||||
return fail(400, { success: false, message: "Invalid parameter", year: year, month: month });
|
||||
|
|
@ -45,14 +58,13 @@ export const actions = {
|
|||
|
||||
try {
|
||||
await generateRecordPDF(locals.user, year, month);
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
return fail(403, { success: false, message: e.toString(), year: year, month: month });
|
||||
}
|
||||
|
||||
|
||||
redirect(303, "dokumente")
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
} satisfies Actions;
|
||||
|
|
|
|||
|
|
@ -1,24 +1,21 @@
|
|||
<script lang="ts">
|
||||
import type { PageProps } from "./$types";
|
||||
|
||||
import { enhance } from "$app/forms";
|
||||
import type { FileProperties } from "$lib/server/docstore";
|
||||
|
||||
import { isoToLocalDate, padInt } from "$lib/util";
|
||||
|
||||
import Expander from "./expander.svelte";
|
||||
|
||||
let { data, form } : PageProps = $props();
|
||||
|
||||
$inspect(data);
|
||||
let { data } : PageProps = $props();
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<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>
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -32,7 +29,7 @@
|
|||
{#each rows as file}
|
||||
<tr>
|
||||
<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>
|
||||
<form method="GET" action={`dokumente/${file.path}`} target="_blank">
|
||||
|
|
@ -86,78 +83,6 @@
|
|||
</Expander>
|
||||
</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>
|
||||
|
||||
<style>
|
||||
|
|
@ -187,10 +112,6 @@ tbody tr {
|
|||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
tbody tr th {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
tbody tr td {
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import type { RequestHandler } from "./$types";
|
||||
|
||||
import { redirect } from "@sveltejs/kit"
|
||||
import { fail } from "@sveltejs/kit"
|
||||
|
||||
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);
|
||||
|
||||
//redirect(307, "/dokumente")
|
||||
return new Response(file);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
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";
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
@ -497,11 +497,8 @@
|
|||
|
||||
</script>
|
||||
|
||||
<!--<tr>
|
||||
{#each { length: prepend_td }, n}
|
||||
<td></td>
|
||||
{/each}-->
|
||||
<td>
|
||||
<td>
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
bind:this={dateInput}
|
||||
bind:value={states.date.value}
|
||||
|
|
@ -532,13 +529,13 @@
|
|||
disabled={!enabled}
|
||||
{autofocus}
|
||||
required>
|
||||
</td>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td>
|
||||
{inWeekDay}
|
||||
</td>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td>
|
||||
<input
|
||||
bind:this={startInput}
|
||||
bind:value={states.start.value}
|
||||
|
|
@ -568,9 +565,9 @@
|
|||
}
|
||||
disabled={!enabled}
|
||||
required>
|
||||
</td>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td>
|
||||
<input
|
||||
bind:this={endInput}
|
||||
bind:value={states.end.value}
|
||||
|
|
@ -600,32 +597,27 @@
|
|||
}
|
||||
disabled={!enabled}
|
||||
required>
|
||||
</td>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td>
|
||||
{inDuration}
|
||||
</td>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td>
|
||||
<input
|
||||
name="comment"
|
||||
type="text"
|
||||
form={targetForm}
|
||||
value={states.comment.value}
|
||||
disabled={!enabled}>
|
||||
</td>
|
||||
</td>
|
||||
|
||||
<td class="action">
|
||||
<td class="action">
|
||||
{@render children?.()}
|
||||
</td>
|
||||
|
||||
<!--</tr>-->
|
||||
</td>
|
||||
|
||||
<style>
|
||||
|
||||
/* * {
|
||||
border: 1px solid;
|
||||
}*/
|
||||
|
||||
td input {
|
||||
box-sizing: border-box;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { PageServerLoad, Actions } from "./$types";
|
||||
import type { FileProperties } from "$lib/server/docstore"
|
||||
|
||||
import { fail } from '@sveltejs/kit';
|
||||
|
||||
|
|
@ -8,29 +9,33 @@ import { getEstimateFiles } from "$lib/server/docstore";
|
|||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
|
||||
if (!locals.user) {
|
||||
return fail(403, { message: "Unauthorized user" })
|
||||
}
|
||||
|
||||
let estimates = locals.user.get_estimates();
|
||||
let documents = await getEstimateFiles(locals.user);
|
||||
|
||||
let estimates_grouped = Map.groupBy(estimates, (v) => v.year);
|
||||
estimates_grouped.forEach((value, key, map) => {
|
||||
let estimates_grouped: Map<number, Map<number, {month: string, estimate: number}[]>> = new Map();
|
||||
Map.groupBy(estimates, (v) => v.year).forEach((value, key, _) => {
|
||||
|
||||
let quarters = Map.groupBy(value, (v) => v.quarter)
|
||||
quarters.forEach((_value, _key, _map) => {
|
||||
let quarters = new Map()
|
||||
Map.groupBy(value, (v) => v.quarter).forEach((_value, _key, _) => {
|
||||
let months = _value.map((v) => [
|
||||
{ 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 + 0], estimate: v.estimate_0 },
|
||||
] ).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)))
|
||||
documents_grouped.forEach((value, key, map) => {
|
||||
let documents_grouped: Map<number, Map<number, FileProperties[]>> = new 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)));
|
||||
map.set(key, quarters);
|
||||
documents_grouped.set(key, quarters);
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
@ -41,16 +46,17 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||
|
||||
export const actions = {
|
||||
add_quarter: async ({ locals, request }) => {
|
||||
if (!locals.user) {
|
||||
return fail(403, { message: "Unauthorized user" })
|
||||
}
|
||||
|
||||
const data = await request.formData();
|
||||
|
||||
const year = data.get("year");
|
||||
const quart = data.get("quarter");
|
||||
const estimate_0 = data.get("estimate_0");
|
||||
const estimate_1 = data.get("estimate_1");
|
||||
const estimate_2 = data.get("estimate_2");
|
||||
|
||||
console.log(data);
|
||||
const year = data.get("year") as string;
|
||||
const quart = data.get("quarter") as string;
|
||||
const estimate_0 = data.get("estimate_0") as string;
|
||||
const estimate_1 = data.get("estimate_1") as string;
|
||||
const estimate_2 = data.get("estimate_2") as string;
|
||||
|
||||
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 });
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@
|
|||
import { enhance } from "$app/forms";
|
||||
import { MONTHS, toInt, toFloat, padInt } from "$lib/util";
|
||||
|
||||
let { form, data }: PageProps = $props();
|
||||
|
||||
$inspect(data);
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
let next = $state((() => {
|
||||
if (data.estimates.size == 0) {
|
||||
|
|
@ -25,7 +23,6 @@
|
|||
estimate_2: { value: "" },
|
||||
editing: { value: "" }
|
||||
})
|
||||
$inspect(estimate_store);
|
||||
|
||||
function validate_year(event: InputEvent) {
|
||||
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>
|
||||
|
||||
<form id="form_add_quart" method="POST" action="?/add_quarter" use:enhance></form>
|
||||
|
|
@ -87,20 +70,20 @@
|
|||
|
||||
<tbody>
|
||||
<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><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>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
</tbody>
|
||||
|
||||
|
|
@ -142,7 +125,7 @@
|
|||
|
||||
<td class="action" rowspan="3">
|
||||
{#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}`}>
|
||||
<button type="submit">Download PDF</button>
|
||||
</form>
|
||||
|
|
@ -167,60 +150,15 @@
|
|||
{/each}
|
||||
</tbody>
|
||||
|
||||
{#if data.estimates === undefined || data.estimates.size === 0}
|
||||
{#if data.estimates.size === 0}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class="td-no-elements" colspan="999">No records</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{/if}
|
||||
{/if}
|
||||
</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>
|
||||
|
||||
form {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ const config = {
|
|||
// 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.
|
||||
adapter: adapter()
|
||||
}
|
||||
},
|
||||
|
||||
warningFilter: (warning) => !warning.code.startsWith('a11y')
|
||||
};
|
||||
|
||||
export default config
|
||||
|
|
|
|||
Loading…
Reference in New Issue