It works
This commit is contained in:
parent
2cedcbcee9
commit
4650cb84c3
|
|
@ -30,3 +30,6 @@ vite.config.ts.timestamp-*
|
||||||
|
|
||||||
# generated files
|
# generated files
|
||||||
pdfgen/*
|
pdfgen/*
|
||||||
|
!pdfgen/template*.tex
|
||||||
|
|
||||||
|
documents/*
|
||||||
|
|
|
||||||
16
package.json
16
package.json
|
|
@ -2,16 +2,16 @@
|
||||||
"name": "stundenaufzeichnung",
|
"name": "stundenaufzeichnung",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.3.1",
|
||||||
"@sveltejs/adapter-node": "^5.2.11",
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
"@sveltejs/kit": "^2.9.0",
|
"@sveltejs/kit": "^2.16.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"@types/sqlite3": "^3.1.11",
|
"@types/sqlite3": "^3.1.11",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.19.6",
|
||||||
"svelte-adapter-bun": "^0.5.2",
|
"svelte-adapter-bun": "^0.5.2",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.1.4",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.0.11"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"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}
|
\documentclass[a4paper,oneside]{article}
|
||||||
\usepackage[left=2.5cm,top=3cm,right=2.5cm,bottom=3cm]{geometry}
|
\usepackage[left=2.5cm,top=3cm,right=2.5cm,bottom=3cm]{geometry}
|
||||||
|
\usepackage[utf8]{inputenc}
|
||||||
|
\usepackage[T1]{fontenc}
|
||||||
|
|
||||||
\usepackage{tabularx}
|
\usepackage{tabularx}
|
||||||
\usepackage{array}
|
\usepackage{array}
|
||||||
\usepackage{csvsimple}
|
\usepackage{csvsimple}
|
||||||
\usepackage{etoolbox}
|
\usepackage{etoolbox}
|
||||||
|
|
||||||
\newcommand{\datafile}{\jobname}
|
\newcommand{\csvext}{.csv}
|
||||||
|
\newcommand{\datafile}{\jobname\csvext}
|
||||||
|
|
||||||
\begin{document}
|
\begin{document}
|
||||||
\pagestyle{empty}
|
\pagestyle{empty}
|
||||||
|
|
@ -36,7 +39,7 @@
|
||||||
\noindent
|
\noindent
|
||||||
\begin{tabularx}{\textwidth}{
|
\begin{tabularx}{\textwidth}{
|
||||||
|>{\raggedleft\arraybackslash}p{2cm}%
|
|>{\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}%
|
|>{\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);
|
process.exit(0);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
import { getAllFiles } from "$lib/server/docstore"
|
||||||
|
|
||||||
export async function handle({ event, resolve }) {
|
export async function handle({ event, resolve }) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ import { Database } from 'bun:sqlite';
|
||||||
|
|
||||||
export interface UserEntry {
|
export interface UserEntry {
|
||||||
id: number;
|
id: number;
|
||||||
|
gender: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
address: string;
|
||||||
initials: string;
|
initials: string;
|
||||||
created: string;
|
created: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,26 @@
|
||||||
import b from "bun";
|
import b from "bun";
|
||||||
|
|
||||||
import type { RecordEntry } from "$lib/db_types";
|
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"
|
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) {
|
export async function generateRecordPDF(user: User, date: Date) {
|
||||||
|
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
|
|
@ -12,26 +28,37 @@ export async function generateRecordPDF(user: User, date: Date) {
|
||||||
|
|
||||||
const dir = "pdfgen/user-" + user.id + "/";
|
const dir = "pdfgen/user-" + user.id + "/";
|
||||||
const file_pref = "Stundenliste-" + year + "-" + padInt(month, 2, 0);
|
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 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 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) {
|
if (exitCode != 0) {
|
||||||
return;
|
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
|
// 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, ' ')}`);
|
console.log(`${user.name};${month_of(date)} ${year};${hr_sum.toFixed(2)};${estimate.toFixed(2)};${padInt(hr_sum - estimate, 2, 2, ' ')}`);
|
||||||
records.forEach((record) => {
|
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}`)
|
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 csvfile = Bun.file(dir + file_pref + ".csv")
|
||||||
const csvfilewriter = csvfile.writer()
|
const csvfilewriter = csvfile.writer()
|
||||||
|
|
@ -44,9 +71,43 @@ export async function generateRecordPDF(user: User, date: Date) {
|
||||||
|
|
||||||
csvfilewriter.end();
|
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 { mkdir } from "node:fs/promises";
|
||||||
import { Database, SQLiteError } from "bun:sqlite";
|
import { Database, SQLiteError } from "bun:sqlite";
|
||||||
|
|
||||||
import { UserEntry, RecordEntry } from "$lib/db_types";
|
import { UserEntry, RecordEntry, EstimatesEntry } from "$lib/db_types";
|
||||||
import { calculateDuration, parseDate, isTimeValidHHMM } from "$lib/util";
|
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 USER_DATABASE_PATH: string = DATABASES_PATH + "users.sqlite";
|
||||||
|
|
||||||
const CHECK_QUERY: string =
|
const CHECK_QUERY: string =
|
||||||
"SELECT * FROM sqlite_master;";
|
"SELECT * FROM sqlite_master;";
|
||||||
|
|
||||||
const USER_DATABASE_SETUP: string[] = [
|
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 =
|
const USER_DATABASE_ADD_USER: string =
|
||||||
"INSERT INTO users (name, initials) VALUES ($name, $initials);";
|
"INSERT INTO users (name, initials) VALUES ($name, $initials);";
|
||||||
|
|
@ -122,7 +129,7 @@ const ESTIMATES_DATABASE_GET_ALL: string =
|
||||||
const ESTIMATES_DATABASE_GET_QUARTERS: string =
|
const ESTIMATES_DATABASE_GET_QUARTERS: string =
|
||||||
"SELECT year, quarter FROM estimates;"
|
"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;"
|
"SELECT estimate_0, estimate_1, estimate_2 FROM estimates WHERE year = $year AND quarter = $quarter;"
|
||||||
|
|
||||||
const ESTIMATES_DATABASE_INSERT: string =
|
const ESTIMATES_DATABASE_INSERT: string =
|
||||||
|
|
@ -130,13 +137,20 @@ const ESTIMATES_DATABASE_INSERT: string =
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
id: number;
|
id: number;
|
||||||
|
gender: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
address: string;
|
||||||
|
initials: 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.name = user.name;
|
this.name = user.name;
|
||||||
|
this.address = user.address;
|
||||||
|
this.initials = user.initials;
|
||||||
this.created = user.created;
|
this.created = user.created;
|
||||||
this._database = db;
|
this._database = db;
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +159,9 @@ export class User {
|
||||||
const query = this._database.query(ENTRY_DATABASE_GET_MONTHS);
|
const query = this._database.query(ENTRY_DATABASE_GET_MONTHS);
|
||||||
const res = query.all();
|
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 }[] {
|
get_quarters(): { year: number, quarter: number }[] {
|
||||||
|
|
@ -219,8 +235,15 @@ export class User {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_estimate(year: number, month: number): number {
|
get_estimate(year: number, quarter: number): EstimatesEntry {
|
||||||
const query = this._database.query(ESTIMATES_DATABASE_GET_MONTH);
|
const query = this._database.query(ESTIMATES_DATABASE_GET_QUART);
|
||||||
|
const res = query.get({ year: year, quarter: quarter });
|
||||||
|
|
||||||
|
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) });
|
const res = query.all({ year: year, quarter: Math.floor(month / 4 + 1) });
|
||||||
|
|
||||||
return res[0]?.[`estimate_${month % 3}`] ?? NaN;
|
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[]) {
|
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() {
|
export function init_db() {
|
||||||
|
|
@ -306,11 +329,6 @@ function _get_user(): UserEntry | null {
|
||||||
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: UserEntry = statement.get();
|
||||||
|
|
||||||
if (result == null) {
|
|
||||||
create_user("Patrick Maschek", "PM");
|
|
||||||
return _get_user();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (e) {
|
} 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" ];
|
export const MONTHS: string[] = [ "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 WEEKDAYS: string[] = [ "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" ];
|
||||||
|
|
||||||
export function toInt(str: string): number {
|
export function toInt(str: string): number {
|
||||||
if (str.length === 0) {
|
if (str.length === 0) {
|
||||||
|
|
@ -54,8 +54,8 @@ export function toFloat(str: string): number {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function padInt(num: number, upper: number, float: number, pad_char: string = '0') {
|
export function padInt(num: number, upper: number, float: number = 0, pad_char: string = '0') {
|
||||||
if (num > 0) {
|
if (num >= 0) {
|
||||||
if (float != 0) {
|
if (float != 0) {
|
||||||
return num.toFixed(float).padStart(upper + 1 + float, pad_char)
|
return num.toFixed(float).padStart(upper + 1 + float, pad_char)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -129,10 +129,7 @@ export function isoToLocalDate(str: string): string | undefined {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(str);
|
return date.getDate() + "." + (date.getMonth() + 1) + "." + date.getFullYear();
|
||||||
console.log(date);
|
|
||||||
|
|
||||||
return date.getDate() + "." + date.getMonth() + "." + date.getFullYear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTimeValidHHMM(str: string): boolean {
|
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 { 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 {
|
return {
|
||||||
availableMonths: locals.user.get_months(),
|
documents: await getAllFiles(locals.user)
|
||||||
availableQuarters: locals.user.get_quarters()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
create_pdf: async ({ locals, request }) => {
|
create_estimate: async ({ locals, request }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
let month = toInt(data.get("month") ?? "");
|
const quarter = toInt(data.get("quarter") ?? "");
|
||||||
let year = toInt(data.get("year") ?? "");
|
const year = toInt(data.get("year") ?? "");
|
||||||
|
|
||||||
if (isNaN(year) || isNaN(month) || month > 12) {
|
if (isNaN(year) || isNaN(quarter) || quarter < 1 || quarter > 4) {
|
||||||
return fail(400, {});
|
return fail(400, { success: false, message: "Invalid parameter", year: year, quarter: quarter });
|
||||||
}
|
}
|
||||||
|
|
||||||
generateRecordPDF(locals.user, new Date(year, month - 1));
|
try {
|
||||||
|
await generateEstimatePDF(locals.user, year, quarter);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return fail(403, { success: false, message: e.toString(), year: year, quarter: quarter });
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true }
|
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") ?? "");
|
||||||
|
|
||||||
|
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">
|
<script lang="ts">
|
||||||
import { toInt } from "$lib/util";
|
import type { PageProps } from "./$types";
|
||||||
|
|
||||||
interface _Props {
|
import { enhance } from "$app/forms";
|
||||||
data: {
|
|
||||||
availableMonths: {
|
|
||||||
month: string,
|
|
||||||
year: string
|
|
||||||
}[],
|
|
||||||
availableQuarters: {
|
|
||||||
quarter: number
|
|
||||||
year: number
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let { data } : _Props = $props();
|
|
||||||
|
|
||||||
const documents : any = [...data.availableMonths, ...data.availableQuarters].sort((l, r) => {
|
import { isoToLocalDate, padInt } from "$lib/util";
|
||||||
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)
|
let { data, form } : PageProps = $props();
|
||||||
return l_m - r_m;
|
|
||||||
}).reverse();
|
|
||||||
|
|
||||||
</script>
|
</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>
|
<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>
|
<thead>
|
||||||
<tr>
|
<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>
|
<th style:width="12ch">Aktion</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each documents as doc}
|
{#each data.documents as doc}
|
||||||
{#if doc.month != null}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Stundenliste {doc.month}-{doc.year}</td>
|
<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>
|
<td>
|
||||||
<form id="create_pdf" method="POST" action="?/create_pdf">
|
<form method="GET" action={`dokumente/${doc.path}`}>
|
||||||
<input type="hidden" name="month" value={doc.month}>
|
<button type="submit">Download</button>
|
||||||
<input type="hidden" name="year" value={doc.year}/>
|
|
||||||
<button type="submit">PDF</button>
|
|
||||||
</form>
|
</form>
|
||||||
</tr>
|
|
||||||
{:else}
|
|
||||||
<tr>
|
|
||||||
<td>Stundenschätzung {doc.quarter}. Quartal {doc.year}</td>
|
|
||||||
<td>
|
|
||||||
<form></form>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/if}
|
|
||||||
{/each}
|
{/each}
|
||||||
<tr></tr>
|
<tr></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
{#if data.availableMonths === undefined || data.availableMonths.length === 0}
|
{#if data.documents.length === 0}
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-no-elements" colspan="999">No records</td>
|
<td class="td-no-elements" colspan="999">Noch keine Dokumente</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
{/if}
|
{/if}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 50%;
|
width: auto;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
table caption {
|
|
||||||
font-size: 25px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody > tr > td {
|
tbody > tr > td {
|
||||||
text-align: center;
|
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";
|
import { toInt, toFloat } from "$lib/util";
|
||||||
|
|
||||||
export async function load({ locals }) {
|
export async function load({ locals }) {
|
||||||
console.log(locals.user.get_estimates())
|
|
||||||
return {
|
return {
|
||||||
estimates: locals.user.get_estimates()
|
estimates: locals.user.get_estimates()
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue