It works
This commit is contained in:
parent
2cedcbcee9
commit
4650cb84c3
|
|
@ -30,3 +30,6 @@ vite.config.ts.timestamp-*
|
|||
|
||||
# generated files
|
||||
pdfgen/*
|
||||
!pdfgen/template*.tex
|
||||
|
||||
documents/*
|
||||
|
|
|
|||
16
package.json
16
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": {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -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}%
|
||||
|
|
@ -28,6 +28,7 @@ process.on('SIGINT', (reason) => {
|
|||
process.exit(0);
|
||||
})
|
||||
|
||||
import { getAllFiles } from "$lib/server/docstore"
|
||||
|
||||
export async function handle({ event, resolve }) {
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import { Database } from 'bun:sqlite';
|
|||
|
||||
export interface UserEntry {
|
||||
id: number;
|
||||
gender: string;
|
||||
name: string;
|
||||
address: string;
|
||||
initials: string;
|
||||
created: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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()
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue