diff --git a/.gitignore b/.gitignore
index 1612285..0e72f66 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,6 @@ vite.config.ts.timestamp-*
# generated files
pdfgen/*
+!pdfgen/template*.tex
+
+documents/*
diff --git a/bun.lockb b/bun.lockb
index 9d988b5..800ba34 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index baa9555..2285bc0 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/pdfgen/template-est.tex b/pdfgen/template-est.tex
new file mode 100644
index 0000000..230d298
--- /dev/null
+++ b/pdfgen/template-est.tex
@@ -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}
diff --git a/pdfgen/template.tex b/pdfgen/template-rec.tex
similarity index 93%
rename from pdfgen/template.tex
rename to pdfgen/template-rec.tex
index d2d7b1b..8ce2c32 100644
--- a/pdfgen/template.tex
+++ b/pdfgen/template-rec.tex
@@ -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}%
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
index 5e4d66b..052ffd8 100644
--- a/src/hooks.server.ts
+++ b/src/hooks.server.ts
@@ -28,6 +28,7 @@ process.on('SIGINT', (reason) => {
process.exit(0);
})
+import { getAllFiles } from "$lib/server/docstore"
export async function handle({ event, resolve }) {
diff --git a/src/lib/db_types.ts b/src/lib/db_types.ts
index 821c0a4..53560bc 100644
--- a/src/lib/db_types.ts
+++ b/src/lib/db_types.ts
@@ -3,7 +3,9 @@ import { Database } from 'bun:sqlite';
export interface UserEntry {
id: number;
+ gender: string;
name: string;
+ address: string;
initials: string;
created: string;
}
diff --git a/src/lib/server/PDFGen.ts b/src/lib/server/PDFGen.ts
index 853e5a3..6360341 100644
--- a/src/lib/server/PDFGen.ts
+++ b/src/lib/server/PDFGen.ts
@@ -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());
}
diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts
index 172c8b2..5414120 100644
--- a/src/lib/server/database.ts
+++ b/src/lib/server/database.ts
@@ -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) {
diff --git a/src/lib/server/docstore.ts b/src/lib/server/docstore.ts
new file mode 100644
index 0000000..63a9754
--- /dev/null
+++ b/src/lib/server/docstore.ts
@@ -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`;
+}
diff --git a/src/lib/util.ts b/src/lib/util.ts
index 78194f6..c27d2c1 100644
--- a/src/lib/util.ts
+++ b/src/lib/util.ts
@@ -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 {
diff --git a/src/routes/dokumente/+page.server.ts b/src/routes/dokumente/+page.server.ts
index 6391ce4..311edcb 100644
--- a/src/routes/dokumente/+page.server.ts
+++ b/src/routes/dokumente/+page.server.ts
@@ -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;
diff --git a/src/routes/dokumente/+page.svelte b/src/routes/dokumente/+page.svelte
index 36df376..b03f370 100644
--- a/src/routes/dokumente/+page.svelte
+++ b/src/routes/dokumente/+page.svelte
@@ -1,92 +1,111 @@
+
+
Dokumente
+
+
+
+
+
{!(form?.success) ? form?.message : "Dokument erstellt."}
+
+
+
+
+
+ Erstellte Dokumente
- | Monat |
+ Dateiname |
+ Erstellt |
Aktion |
- {#each documents as doc}
- {#if doc.month != null}
-
- | Stundenliste {doc.month}-{doc.year} |
-
-
- |
- {:else}
-
- | Stundenschätzung {doc.quarter}. Quartal {doc.year} |
-
-
- |
-
- {/if}
+ {#each data.documents as doc}
+
+ | {doc.name} |
+ {`${isoToLocalDate(doc.timestamp.toISOString())} um ${padInt(doc.timestamp.getHours(), 2)}:${padInt(doc.timestamp.getMinutes(), 2)}:${padInt(doc.timestamp.getSeconds(), 2)}`} |
+
+
+ |
+
{/each}
-{#if data.availableMonths === undefined || data.availableMonths.length === 0}
-
-
- | No records |
-
-
-{/if}
+ {#if data.documents.length === 0}
+
+
+ | Noch keine Dokumente |
+
+
+ {/if}
+
+