This commit is contained in:
Patrick 2025-02-04 21:41:16 +01:00
parent 2cedcbcee9
commit 4650cb84c3
15 changed files with 590 additions and 108 deletions

3
.gitignore vendored
View File

@ -30,3 +30,6 @@ vite.config.ts.timestamp-*
# generated files
pdfgen/*
!pdfgen/template*.tex
documents/*

BIN
bun.lockb

Binary file not shown.

View File

@ -2,16 +2,16 @@
"name": "stundenaufzeichnung",
"version": "0.0.1",
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-node": "^5.2.11",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.16.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@types/sqlite3": "^3.1.11",
"svelte": "^5.0.0",
"svelte": "^5.19.6",
"svelte-adapter-bun": "^0.5.2",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.0.0"
"svelte-check": "^4.1.4",
"typescript": "^5.7.3",
"vite": "^6.0.11"
},
"private": true,
"scripts": {

101
pdfgen/template-est.tex Normal file
View File

@ -0,0 +1,101 @@
\documentclass[a4paper,11pt,oneside]{article}
\usepackage[left=2.5cm,top=3cm,right=2.5cm,bottom=3cm]{geometry}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[style=austrian]{csquotes}
\usepackage{csvsimple}
\usepackage{etoolbox}
%\newcommand{\Anrede}{Herrn}
%\newcommand{\Name}{Patrick Maschek}
%\newcommand{\Addresse}{2231 Strasshof, Ganghoferstra{\ss}e 16}
\newcommand{\csvext}{.csv}
%\newcommand{\datafile}{\jobname\csvext}
\newcommand{\datafile}{estimate.csv}
\newcommand{\listitemend}{,}
\newcommand{\listend}{.}
\setlength\parindent{0pt}
\begin{document}
\pagestyle{empty}
\begin{center}
\Large\bfseries
\underline{\underline{VEREINBARUNG ARBEITSBEDARF}}\\[\baselineskip]
\underline{\underline{FÜR KALENDERVIERTELJAHR}}\\[\baselineskip]
\end{center}
\bigskip
zwischen
\bigskip
Tanzschule Elmayer Vestenbrugg-GmbH\newline
1010 Wien, Bräunerstraße 13
\bigskip
im Folgenden \enquote{Arbeitgeberin}
\bigskip\bigskip
und
\bigskip
\csvreader[%
separator=semicolon,
no head,
filter test=\ifnumless{\thecsvinputline}{2}]
{\datafile}{1=\Name,2=\Addresse,3=\Arbeitnehmer,4=\Mitarbeiterrolle}{%
\Name
\bigskip
wohnhaft in \Addresse
\bigskip
im Folgenden \enquote{\Arbeitnehmer} (\Mitarbeiterrolle)
}
\bigskip
wird vereinbart:
\bigskip
\csvreader[%
separator=semicolon,
no head,
late after line=\listitemend\\,
late after last line=\listend\\,
filter test=\ifnumgreater{\thecsvinputline}{1}]
{\datafile}{1=\Monat,2=\Schaetzung}{%
Für den Monat {\Monat} werden \textbf{\Schaetzung} vereinbart%
}
\csvreader[%
separator=semicolon,
no head,
filter test=\ifnumless{\thecsvinputline}{2}]
{\datafile}{3=\Arbeitnehmer,5=\Datum}{%
Datum: \Datum
\vspace{2cm}
\noindent\begin{tabular}{p{0.5\linewidth} p{0.5\linewidth}}
(Arbeitgeberin) & (\Arbeitnehmer)
\end{tabular}
}
\end{document}

View File

@ -1,12 +1,15 @@
\documentclass[a4paper,oneside]{article}
\usepackage[left=2.5cm,top=3cm,right=2.5cm,bottom=3cm]{geometry}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{tabularx}
\usepackage{array}
\usepackage{csvsimple}
\usepackage{etoolbox}
\newcommand{\datafile}{\jobname}
\newcommand{\csvext}{.csv}
\newcommand{\datafile}{\jobname\csvext}
\begin{document}
\pagestyle{empty}
@ -36,7 +39,7 @@
\noindent
\begin{tabularx}{\textwidth}{
|>{\raggedleft\arraybackslash}p{2cm}%
|>{\raggedleft\arraybackslash}p{2cm}%
|>{\raggedright\arraybackslash}p{2cm}%
|>{\raggedleft\arraybackslash}p{1.25cm}%
|>{\raggedleft\arraybackslash}p{1.25cm}%
|>{\raggedleft\arraybackslash}p{1.25cm}%

View File

@ -28,6 +28,7 @@ process.on('SIGINT', (reason) => {
process.exit(0);
})
import { getAllFiles } from "$lib/server/docstore"
export async function handle({ event, resolve }) {

View File

@ -3,7 +3,9 @@ import { Database } from 'bun:sqlite';
export interface UserEntry {
id: number;
gender: string;
name: string;
address: string;
initials: string;
created: string;
}

View File

@ -1,10 +1,26 @@
import b from "bun";
import type { RecordEntry } from "$lib/db_types";
import { toInt, padInt, parseDate, calculateDuration, month_of, weekday_of } from "$lib/util"
import { MONTHS, toInt, padInt, parseDate, calculateDuration, isoToLocalDate, month_of, weekday_of } from "$lib/util"
import { User } from "$lib/server/database"
const SALUTATION = {
m: "Herrn",
w: "Frau"
}
const GENDER_END = {
m: "",
w: "in",
}
const TEMPLATE_LISTE = "template-liste.tex";
const TEMPLATE_LISTE_BASE = "pdfgen/" + TEMPLATE_LISTE;
const TEMPLATE_EST = "template-schaetzung.tex";
const TEMPLATE_EST_BASE = "pdfgen/" + TEMPLATE_EST;
export async function generateRecordPDF(user: User, date: Date) {
const year = date.getFullYear();
@ -12,26 +28,37 @@ export async function generateRecordPDF(user: User, date: Date) {
const dir = "pdfgen/user-" + user.id + "/";
const file_pref = "Stundenliste-" + year + "-" + padInt(month, 2, 0);
const template_path = dir + TEMPLATE_LISTE;
const records = user.get_entries_by_month(year, month);
const estimate = user.get_estimate(year, month);
const estimate = user.get_estimate_by_month(year, month);
const hr_sum = (() => { let s = 0; records.forEach((r) => { s += calculateDuration(r.start, r.end) }); return s; })()
const { exitCode } = await b.$`mkdir -p ${dir}`.nothrow().quiet();
if (records.length == 0 || estimate == null || isNaN(estimate)) {
return;
}
let exitCode = 0;
({ exitCode: exitCode } = await b.$`mkdir -p ${dir}`.nothrow().quiet());
if (exitCode != 0) {
return;
}
if (!await Bun.file(template_path).exists()) {
let { exitCode } = await b.$`cp ${TEMPLATE_LISTE_BASE} ${template_path}`.nothrow().quiet();
if (exitCode != 0) {
console.warn(`Failed to copy pdf template liste to ${dir} (exit code: ${exitCode})`)
}
}
// TODO: escape semicolon in comment
console.log(dir + file_pref + ".csv")
/*console.log(dir + file_pref + ".csv")
console.log(`${user.name};${month_of(date)} ${year};${hr_sum.toFixed(2)};${estimate.toFixed(2)};${padInt(hr_sum - estimate, 2, 2, ' ')}`);
records.forEach((record) => {
console.log(`${record.date};${weekday_of(parseDate(record.date))};${record.start};${record.end};${calculateDuration(record.start, record.end).toFixed(2)};${record.comment}`)
})
return;
})*/
const csvfile = Bun.file(dir + file_pref + ".csv")
const csvfilewriter = csvfile.writer()
@ -44,9 +71,43 @@ export async function generateRecordPDF(user: User, date: Date) {
csvfilewriter.end();
await b.$`pdflatex -jobname=${dir + file_pref} -output-format=pdf template.tex`;
({ exitCode: exitCode } = await b.$`pdflatex -interaction=nonstopmode -halt-on-error -jobname=${file_pref} -output-format=pdf template-liste.tex`.cwd(dir).quiet().nothrow());
}
function generateTable(path: string) {
export async function generateEstimatePDF(user: User, year: number, quarter: number) {
const dir = "pdfgen/user-" + user.id + "/";
const file_pref = "Stundenschätzung-" + year + "-Q" + number;
const template_path = dir + TEMPLATE_EST;
const estimates = user.get_estimate(year, quarter);
let exitCode = 0;
({ exitCode: exitCode } = await b.$`mkdir -p ${dir}`.nothrow().quiet());
if (exitCode != 0) {
return;
}
if (!await Bun.file(template_path).exists()) {
let { exitCode } = await b.$`cp ${TEMPLATE_EST_BASE} ${template_path}`.nothrow().quiet();
if (exitCode != 0) {
console.warn(`Failed to copy pdf template estimate to ${dir} (exit code: ${exitCode})`)
}
}
const csvfile = Bun.file(dir + file_pref + ".csv")
const csvfilewriter = csvfile.writer()
csvfilewriter.write(`${SALUTATION[user.gender]} ${user.name};${user.address};Arbeitnehmer${GENDER_END[user.gender]};Teilzeitmitarbeiter${GENDER_END[user.gender]};${isoToLocalDate(new Date().toISOString())}\n`)
for (let i = 0; i < 3; ++i) {
csvfilewriter.write(`${MONTHS[(quarter - 1) * 3 + i]} ${year};${estimates[`estimate_${i}`]}\n`);
}
csvfilewriter.end();
({ exitCode: exitCode } = await b.$`pdflatex -interaction=nonstopmode -halt-on-error -jobname=${file_pref} -output-format=pdf template-liste.tex`.cwd(dir).quiet().nothrow());
}

View File

@ -1,18 +1,25 @@
import { mkdir } from "node:fs/promises";
import { Database, SQLiteError } from "bun:sqlite";
import { UserEntry, RecordEntry } from "$lib/db_types";
import { calculateDuration, parseDate, isTimeValidHHMM } from "$lib/util";
import { UserEntry, RecordEntry, EstimatesEntry } from "$lib/db_types";
import { calculateDuration, parseDate, toInt, isTimeValidHHMM } from "$lib/util";
const DATABASES_PATH: string = "";
const DATABASES_PATH: string = "./databases/";
const USER_DATABASE_PATH: string = DATABASES_PATH + "users.sqlite";
const CHECK_QUERY: string =
"SELECT * FROM sqlite_master;";
const USER_DATABASE_SETUP: string[] = [
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT, initials TEXT, created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL);",
]
`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
gender TEXT,
name TEXT,
address TEXT,
initials TEXT,
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
);`,
];
const USER_DATABASE_ADD_USER: string =
"INSERT INTO users (name, initials) VALUES ($name, $initials);";
@ -122,7 +129,7 @@ const ESTIMATES_DATABASE_GET_ALL: string =
const ESTIMATES_DATABASE_GET_QUARTERS: string =
"SELECT year, quarter FROM estimates;"
const ESTIMATES_DATABASE_GET_MONTH: string =
const ESTIMATES_DATABASE_GET_QUART: string =
"SELECT estimate_0, estimate_1, estimate_2 FROM estimates WHERE year = $year AND quarter = $quarter;"
const ESTIMATES_DATABASE_INSERT: string =
@ -130,13 +137,20 @@ const ESTIMATES_DATABASE_INSERT: string =
export class User {
id: number;
gender: string;
name: string;
address: string;
initials: string;
created: string;
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.created = user.created;
this._database = db;
}
@ -145,7 +159,9 @@ export class User {
const query = this._database.query(ENTRY_DATABASE_GET_MONTHS);
const res = query.all();
return res;
const ret = res.map((v) => { return { year: toInt(v.year), month: toInt(v.month) }})
return ret;
}
get_quarters(): { year: number, quarter: number }[] {
@ -218,9 +234,16 @@ export class User {
return res;
}
get_estimate(year: number, quarter: number): EstimatesEntry {
const query = this._database.query(ESTIMATES_DATABASE_GET_QUART);
const res = query.get({ year: year, quarter: quarter });
get_estimate(year: number, month: number): number {
const query = this._database.query(ESTIMATES_DATABASE_GET_MONTH);
return res;
}
get_estimate_by_month(year: number, month: number): number {
const query = this._database.query(ESTIMATES_DATABASE_GET_QUART);
const res = query.all({ year: year, quarter: Math.floor(month / 4 + 1) });
return res[0]?.[`estimate_${month % 3}`] ?? NaN;
@ -264,7 +287,7 @@ function get_user_db_name(user: UserEntry) {
}
function setup_db(db: Database, setup_queries: string[]) {
setup_queries.forEach((q) => { console.log(q); db.query(q).run(); });
setup_queries.forEach((q) => { /*console.log(q);*/ db.query(q).run(); });
}
export function init_db() {
@ -306,11 +329,6 @@ function _get_user(): UserEntry | null {
const statement = user_database.prepare(USER_DATABASE_GET_USER);
const result: UserEntry = statement.get();
if (result == null) {
create_user("Patrick Maschek", "PM");
return _get_user();
}
return result;
} catch (e) {

238
src/lib/server/docstore.ts Normal file
View File

@ -0,0 +1,238 @@
import b from "bun";
import fs from "node:fs/promises"
import type { RecordEntry } from "$lib/db_types";
import { User } from "$lib/server/database";
import { MONTHS, toInt, padInt, parseDate, calculateDuration, isoToLocalDate, month_of, weekday_of } from "$lib/util"
export class UserBusyException extends Error {
constructor(message, userid) {
super(`user (id: ${userid}) is busy: ${message}`);
this.name = this.constructor.name;
this.userid = userid;
}
}
export class LatexException extends Error {
constructor(message, userid, errno, stdout) {
super(message + ` (userid: ${userid}, errno: ${errno})\n${stdout}`);
this.name = this.constructor.name;
this.errno = errno;
this.stdout = stdout;
}
}
export class FileExistsException extends Error {
constructor(path) {
super("File already exists: " + path);
this.name = this.constructor.name;
this.path = path;
}
}
const DOCUMENTS_PATH: string = "./documents"
const ESTIMATES_FOLD: string = "estimates"
const RECORDS_FOLD: string = "records"
const GENERATION_PATH: string = "./pdfgen";
const TEMPLATE_REC = "template-rec.tex";
const TEMPLATE_REC_PATH = `${GENERATION_PATH}/${TEMPLATE_REC}`;
const TEMPLATE_EST = "template-est.tex";
const TEMPLATE_EST_PATH = `${GENERATION_PATH}/${TEMPLATE_EST}`;
const SALUTATION = {
m: "Herrn",
w: "Frau"
}
const GENDER_END = {
m: "",
w: "in",
}
// this may be a race condition???
// is a mutex needed in JS?
let locked_users: number[] = new Set();
export async function getAllFiles(user: User) {
const path = `${DOCUMENTS_PATH}/user-${user.id}`;
let file_names = (await fs.readdir(path, { recursive: true }).catch((err) => {
return [];
})).filter((v) => {
return !(v == ESTIMATES_FOLD || v == RECORDS_FOLD);
});
let files = (await Promise.all(file_names.map(async (v) => {
return {
path: v,
name: v.split("/").pop(),
timestamp: (await fs.stat(`${path}/${v}`)).mtime
}
}))).sort((a, b) => {
return a.timestamp - b.timestamp
});
return files;
}
export async function getFile(user: User, filename: string) {
const path = `${DOCUMENTS_PATH}/user-${user.id}`;
let file = Bun.file(`${path}/${filename}`)
if (!await file.exists()) {
return null;
}
return await file.bytes();
}
export async function generateEstimatePDF(user: User, year: number, quarter: number) {
return await _runProtocted(_generateEstimatePDF, user, year, quarter);
}
export async function generateRecordPDF(user: User, year: number, quarter: number) {
return await _runProtocted(_generateRecordPDF, user, year, quarter);
}
async function _runProtocted(f, user: User, ...args) {
if (locked_users.has(user.id)) {
throw UserBusyException(`Cannot generate pdf (type: ${type})` ,user.id);
}
locked_users.add(user.id);
try {
await f(user, ...args);
} catch (e) {
throw e;
} finally {
locked_users.delete(user.id);
}
}
async function _generateRecordPDF(user: User, year: number, month: number, override: boolean = false) {
const path = `${DOCUMENTS_PATH}/user-${user.id}/${RECORDS_FOLD}`;
const file_pref = "Stundenliste-" + year + "-" + padInt(month, 2, 0);
if (!override && await Bun.file(`${path}/${file_pref}.pdf`).exists()) {
throw new FileExistsException(`${path}/${file_pref}.pdf`);
}
fs.mkdir(path, { recursive: true });
let gen_path = await _genLatexRec(user, file_pref, year, month);
let doc_path = `${path}/${gen_path.split('/').pop()}`;
await fs.rename(gen_path, doc_path);
return doc_path;
}
async function _generateEstimatePDF(user: User, year: number, quarter: number, override: boolean = false) {
const path = `${DOCUMENTS_PATH}/user-${user.id}/${ESTIMATES_FOLD}`;
const file_pref = `Vereinbarung_Arbeitsbedarf_${user.name.replace(" ", "_")}_${quarter}.Quartal_${year}`
if (!override && await Bun.file(`${path}/${file_pref}.pdf`).exists()) {
throw new FileExistsException(`${path}/${file_pref}.pdf`);
}
fs.mkdir(path, { recursive: true });
let gen_path = await _genLatexEst(user, file_pref, year, quarter);
let doc_path = `${path}/${gen_path.split('/').pop()}`;
await fs.rename(gen_path, doc_path);
return doc_path;
}
async function _genLatexRec(user: User, file_pref: string, year: number, month: number) {
const dir = `${GENERATION_PATH}/user-${user.id}`;
const template_path = `${dir}/${TEMPLATE_REC}`;
fs.mkdir(dir, { recursive: true });
if (!await Bun.file(template_path).exists()) {
// may fail and throw error
await b.$`cp ${TEMPLATE_REC_PATH} ${template_path}`.quiet();
}
const records = user.get_entries_by_month(year, month);
const estimate = user.get_estimate_by_month(year, month);
const hr_sum = (() => { let s = 0; records.forEach((r) => { s += calculateDuration(r.start, r.end) }); return s; })()
if (estimate == null || isNaN(estimate)) {
throw new Error("No estimate found")
}
// TODO: escape semicolon in comment
const csvfile = Bun.file(`${dir}/${file_pref}.csv`)
const csvfilewriter = csvfile.writer()
csvfilewriter.write(`${user.name};${MONTHS[month - 1]} ${year};${hr_sum.toFixed(2)};${estimate.toFixed(2)};${padInt(hr_sum - estimate, 2, 2, ' ')}\n`)
records.forEach((record) => {
csvfilewriter.write(`${record.date};${weekday_of(parseDate(record.date))};${record.start};${record.end};${calculateDuration(record.start, record.end).toFixed(2)};${record.comment}\n`)
})
csvfilewriter.end();
const { stdout, exitCode } = await b.$`pdflatex -interaction=nonstopmode -halt-on-error -jobname=${file_pref} -output-format=pdf ${TEMPLATE_REC}`.cwd(dir).quiet().nothrow();
if (exitCode != 0) {
throw new LatexException("Failed to create record PDF", user.id, exitCode, stdout);
}
return `${dir}/${file_pref}.pdf`;
}
async function _genLatexEst(user: User, file_pref: string, year: number, quarter: number) {
const dir = `${GENERATION_PATH}/user-${user.id}`;
const template_path = `${dir}/${TEMPLATE_EST}`;
fs.mkdir(dir, { recursive: true });
if (!await Bun.file(template_path).exists()) {
// may fail and throw error
await b.$`cp ${TEMPLATE_EST_PATH} ${template_path}`.quiet();
}
const estimates = user.get_estimate(year, quarter);
if (estimates == null) {
throw Error("No estimate for quarter");
}
const csvfile = Bun.file(`${dir}/estimate.csv`)
const csvfilewriter = csvfile.writer()
csvfilewriter.write(`${SALUTATION[user.gender]} ${user.name};${user.address};Arbeitnehmer${GENDER_END[user.gender]};Teilzeitmitarbeiter${GENDER_END[user.gender]};${isoToLocalDate(new Date().toISOString())}\n`)
for (let i = 0; i < 3; ++i) {
csvfilewriter.write(`${MONTHS[(quarter - 1) * 3 + i]} ${year};${estimates[`estimate_${i}`]}\n`);
}
csvfilewriter.end();
const { stdout, exitCode } = await b.$`pdflatex -interaction=nonstopmode -halt-on-error -jobname=${file_pref} -output-format=pdf ${TEMPLATE_EST}`.cwd(dir).quiet().nothrow();
if (exitCode != 0) {
throw new LatexException("Failed to create estimate PDF", user.id, exitCode, stdout);
}
return `${dir}/${file_pref}.pdf`;
}

View File

@ -1,6 +1,6 @@
const MONTHS = [ "Jänner", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember" ];
const WEEKDAYS: string[] = [ "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" ];
export const MONTHS: string[] = [ "Jänner", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember" ];
export const WEEKDAYS: string[] = [ "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" ];
export function toInt(str: string): number {
if (str.length === 0) {
@ -54,8 +54,8 @@ export function toFloat(str: string): number {
return value;
}
export function padInt(num: number, upper: number, float: number, pad_char: string = '0') {
if (num > 0) {
export function padInt(num: number, upper: number, float: number = 0, pad_char: string = '0') {
if (num >= 0) {
if (float != 0) {
return num.toFixed(float).padStart(upper + 1 + float, pad_char)
} else {
@ -128,11 +128,8 @@ export function isoToLocalDate(str: string): string | undefined {
if (!date) {
return undefined;
}
console.log(str);
console.log(date);
return date.getDate() + "." + date.getMonth() + "." + date.getFullYear();
return date.getDate() + "." + (date.getMonth() + 1) + "." + date.getFullYear();
}
export function isTimeValidHHMM(str: string): boolean {

View File

@ -1,27 +1,54 @@
import type { PageServerLoad, Actions } from "./$types";
import { fail } from "@sveltejs/kit"
import { toInt } from "$lib/util"
import { generateRecordPDF } from "$lib/server/PDFGen";
import { getAllFiles, generateEstimatePDF, generateRecordPDF } from "$lib/server/docstore";
export async function load({ locals }) {
export const load: PageServerLoad = async ({ locals }) => {
return {
availableMonths: locals.user.get_months(),
availableQuarters: locals.user.get_quarters()
documents: await getAllFiles(locals.user)
}
}
export const actions = {
create_pdf: async ({ locals, request }) => {
create_estimate: async ({ locals, request }) => {
const data = await request.formData();
const quarter = toInt(data.get("quarter") ?? "");
const year = toInt(data.get("year") ?? "");
let month = toInt(data.get("month") ?? "");
let year = toInt(data.get("year") ?? "");
if (isNaN(year) || isNaN(quarter) || quarter < 1 || quarter > 4) {
return fail(400, { success: false, message: "Invalid parameter", year: year, quarter: quarter });
}
if (isNaN(year) || isNaN(month) || month > 12) {
return fail(400, {});
try {
await generateEstimatePDF(locals.user, year, quarter);
} catch (e) {
console.log(e);
return fail(403, { success: false, message: e.toString(), year: year, quarter: quarter });
}
generateRecordPDF(locals.user, new Date(year, month - 1));
return { success: true };
},
create_record: async ({ locals, request }) => {
const data = await request.formData();
const month = toInt(data.get("month") ?? "");
const year = toInt(data.get("year") ?? "");
return { success: true }
if (isNaN(year) || isNaN(month) || month < 1 || month > 12) {
return fail(400, { success: false, message: "Invalid parameter", year: year, month: month });
}
try {
await generateRecordPDF(locals.user, year, month);
} catch (e) {
console.log(e);
return fail(403, { success: false, message: e.toString(), year: year, month: month });
}
return { success: true };
}
}
} satisfies Actions;

View File

@ -1,92 +1,111 @@
<script lang="ts">
import { toInt } from "$lib/util";
import type { PageProps } from "./$types";
interface _Props {
data: {
availableMonths: {
month: string,
year: string
}[],
availableQuarters: {
quarter: number
year: number
}[]
}
};
let { data } : _Props = $props();
import { enhance } from "$app/forms";
const documents : any = [...data.availableMonths, ...data.availableQuarters].sort((l, r) => {
const l_m = l.year * 4 + (l.month != null ? (l.month - 1) / 4 + 1 : l.quarter - 1)
const r_m = r.year * 4 + (r.month != null ? (r.month - 1) / 4 + 1 : r.quarter - 1)
return l_m - r_m;
}).reverse();
import { isoToLocalDate, padInt } from "$lib/util";
let { data, form } : PageProps = $props();
</script>
<div>
<h1>Dokumente</h1>
<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>Dokumente</caption>
<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">Monat</th>
<th style:width="30ch">Dateiname</th>
<th style:width="15ch">Erstellt</th>
<th style:width="12ch">Aktion</th>
</tr>
</thead>
<tbody>
{#each documents as doc}
{#if doc.month != null}
<tr>
<td>Stundenliste {doc.month}-{doc.year}</td>
<td>
<form id="create_pdf" method="POST" action="?/create_pdf">
<input type="hidden" name="month" value={doc.month}>
<input type="hidden" name="year" value={doc.year}/>
<button type="submit">PDF</button>
</form>
</tr>
{:else}
<tr>
<td>Stundenschätzung {doc.quarter}. Quartal {doc.year}</td>
<td>
<form></form>
</td>
</tr>
{/if}
{#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.availableMonths === undefined || data.availableMonths.length === 0}
<tfoot>
<tr>
<td class="td-no-elements" colspan="999">No records</td>
</tr>
</tfoot>
{/if}
{#if data.documents.length === 0}
<tfoot>
<tr>
<td class="td-no-elements" colspan="999">Noch keine Dokumente</td>
</tr>
</tfoot>
{/if}
</table>
</div>
<style>
h1 {
width: 100%;
text-align: center;
font-size: 25px;
font-weight: bold;
}
form {
width: fit-content;
border: none;
}
table {
width: 50%;
width: auto;
margin: auto;
border-collapse: collapse;
border: 1px solid;
}
table caption {
font-size: 25px;
font-weight: bold;
}
tbody > tr > td {
text-align: center;
}

View File

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

View File

@ -3,7 +3,6 @@ import { fail } from '@sveltejs/kit';
import { toInt, toFloat } from "$lib/util";
export async function load({ locals }) {
console.log(locals.user.get_estimates())
return {
estimates: locals.user.get_estimates()
};