Compare commits
2 Commits
888e35f839
...
e2f011597d
| Author | SHA1 | Date |
|---|---|---|
|
|
e2f011597d | |
|
|
104cafbcec |
|
|
@ -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() {
|
||||||
|
|
@ -40,10 +41,32 @@ process.on('SIGINT', (_) => {
|
||||||
|
|
||||||
export let handle: Handle = async function ({ event, resolve }) {
|
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) {
|
if (event.route.id == null) {
|
||||||
return error(404, "No user"); // redirect login
|
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;
|
||||||
|
|
|
||||||
|
|
@ -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 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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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="/">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%;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 ?? "",
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
@ -15,10 +20,14 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,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;
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
@ -8,29 +9,33 @@ 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 {
|
||||||
|
|
@ -41,16 +46,17 @@ 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 });
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue