added pdf generation
This commit is contained in:
parent
3885082d2f
commit
d273e659e7
|
|
@ -24,3 +24,9 @@ vite.config.ts.timestamp-*
|
||||||
|
|
||||||
# Databases
|
# Databases
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
|
||||||
|
# old files
|
||||||
|
*.old
|
||||||
|
|
||||||
|
# generated files
|
||||||
|
pdfgen/*
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -10,19 +10,40 @@ entity "records" {
|
||||||
end: varchar(5) [HH:MM]
|
end: varchar(5) [HH:MM]
|
||||||
|
|
||||||
created: timestamp
|
created: timestamp
|
||||||
modified: timestamp
|
}
|
||||||
modified_to: records::id
|
|
||||||
|
entity "records_history" {
|
||||||
|
id: primary
|
||||||
|
--
|
||||||
|
record: records::id
|
||||||
|
|
||||||
|
date: varchar(10)
|
||||||
|
start: varchar(5)
|
||||||
|
end: varchar(5)
|
||||||
|
|
||||||
|
modified_at: timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
entity "estimates" {
|
entity "estimates" {
|
||||||
id: primary
|
id: primary
|
||||||
--
|
--
|
||||||
date: varchar(7) [YYYY-MM]
|
year: integer
|
||||||
|
month: integer
|
||||||
estimate: real
|
estimate: real
|
||||||
|
|
||||||
created: timestamp
|
created: timestamp
|
||||||
modified: timestamp
|
}
|
||||||
modified_to: estimates::id
|
|
||||||
|
entity "estimates_history" {
|
||||||
|
id: primary
|
||||||
|
--
|
||||||
|
estimate: estimates::id
|
||||||
|
|
||||||
|
year: integer
|
||||||
|
month: integer
|
||||||
|
estimate: real
|
||||||
|
|
||||||
|
modified_at: timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
@enduml
|
@enduml
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
\documentclass[a4paper,oneside]{article}
|
||||||
|
\usepackage[left=2.5cm,top=3cm,right=2.5cm,bottom=3cm]{geometry}
|
||||||
|
|
||||||
|
\usepackage{tabularx}
|
||||||
|
\usepackage{array}
|
||||||
|
\usepackage{csvsimple}
|
||||||
|
\usepackage{etoolbox}
|
||||||
|
|
||||||
|
\newcommand{\datafile}{\jobname}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
\pagestyle{empty}
|
||||||
|
|
||||||
|
\begin{center}
|
||||||
|
\section*{Stundenliste}
|
||||||
|
\end{center}
|
||||||
|
|
||||||
|
\vspace{1cm}
|
||||||
|
|
||||||
|
%\Name ;\MonatJahr ;\IstStunden ;\SollStunden ;\DiffStunden
|
||||||
|
\csvreader[%
|
||||||
|
separator=semicolon,
|
||||||
|
no head,
|
||||||
|
filter test = \ifnumless{\thecsvinputline}{2}]{\datafile}{1=\Name,2=\MonatJahr}{%
|
||||||
|
|
||||||
|
\begin{tabular}{l l}
|
||||||
|
Name: & \Name \\
|
||||||
|
Monat/Jahr: & \MonatJahr
|
||||||
|
\end{tabular}
|
||||||
|
}
|
||||||
|
|
||||||
|
\vspace{1cm}
|
||||||
|
|
||||||
|
{
|
||||||
|
%\adjustboxset{margin=3pt}
|
||||||
|
\noindent
|
||||||
|
\begin{tabularx}{\textwidth}{
|
||||||
|
|>{\raggedleft\arraybackslash}p{2cm}%
|
||||||
|
|>{\raggedleft\arraybackslash}p{2cm}%
|
||||||
|
|>{\raggedleft\arraybackslash}p{1.25cm}%
|
||||||
|
|>{\raggedleft\arraybackslash}p{1.25cm}%
|
||||||
|
|>{\raggedleft\arraybackslash}p{1.25cm}%
|
||||||
|
|X|}
|
||||||
|
\hline
|
||||||
|
\centering\arraybackslash\bfseries Datum & %
|
||||||
|
\centering\arraybackslash\bfseries Wochentag & %
|
||||||
|
\centering\arraybackslash\bfseries Beginn & %
|
||||||
|
\centering\arraybackslash\bfseries Ende & %
|
||||||
|
\centering\arraybackslash\bfseries Dauer & %
|
||||||
|
\centering\arraybackslash\bfseries Anmerkung %\\ \noalign{\hrule height 1.5pt}
|
||||||
|
\csvreader[%
|
||||||
|
separator=semicolon,
|
||||||
|
no head,
|
||||||
|
filter test = \ifnumgreater{\thecsvinputline}{1}]{\datafile}{1=\csvdate,2=\csvday,3=\csvstart,4=\csvend,5=\csvduration,6=\csvcomment}{%
|
||||||
|
\\\noalign{\csviffirstrow{\hrule height 1.5pt}{\hrule height 0.5pt}}
|
||||||
|
\csvdate & \csvday & \csvstart & \csvend & \csvduration & \csvcomment
|
||||||
|
}
|
||||||
|
\csvreader[%
|
||||||
|
separator=semicolon,
|
||||||
|
no head,
|
||||||
|
filter test = \ifnumless{\thecsvinputline}{2}]{\datafile}{3=\Ist,4=\Soll,5=\Diff}{%
|
||||||
|
\\\hline
|
||||||
|
\multicolumn{5}{>{\tiny}p{7.5cm}}{} \\
|
||||||
|
\multicolumn{2}{p{4cm}}{} & \multicolumn{2}{>{\raggedleft\arraybackslash}p{2.5cm}}{Ist-Stunden:} & \multicolumn{1}{>{\raggedleft\arraybackslash}p{1.25cm}}{\Ist} \\
|
||||||
|
\multicolumn{2}{p{4cm}}{} & \multicolumn{2}{>{\raggedleft\arraybackslash}p{2.5cm}}{Soll-Stunden:} & \multicolumn{1}{>{\raggedleft\arraybackslash}p{1.25cm}}{\Soll} \\ \cline{3-5}
|
||||||
|
\multicolumn{2}{p{4cm}}{} & \multicolumn{2}{>{\raggedleft\arraybackslash}p{2.5cm}}{Differenz:} & \multicolumn{1}{>{\raggedleft\arraybackslash}p{1.25cm}}{\Diff}
|
||||||
|
}
|
||||||
|
\end{tabularx}
|
||||||
|
}
|
||||||
|
|
||||||
|
\end{document}
|
||||||
|
|
@ -4,6 +4,7 @@ import { Database } from 'bun:sqlite';
|
||||||
export interface UserEntry {
|
export interface UserEntry {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
initials: string;
|
||||||
created: string;
|
created: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import b from "bun";
|
||||||
|
|
||||||
|
import type { RecordEntry } from "$lib/db_types";
|
||||||
|
import { toInt, parseDate, calculateDuration, weekday_of } from "$lib/util"
|
||||||
|
|
||||||
|
import { User } from "$lib/server/database"
|
||||||
|
|
||||||
|
const MONTHS = [ "Jänner", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember" ];
|
||||||
|
|
||||||
|
export async function generateRecordPDF(user: User, date: Date, soll: str) {
|
||||||
|
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth() + 1;
|
||||||
|
|
||||||
|
const dir = "pdfgen/user-" + user.id + "/";
|
||||||
|
const file_pref = "Stundenliste-" + year + "-" + month;
|
||||||
|
|
||||||
|
const records = user.get_entries_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 (exitCode != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: escape semicolon in comment
|
||||||
|
|
||||||
|
console.log(`${user.name};${MONTHS[date.getMonth()]} ${year};${hr_sum.toFixed(2)};${soll};${(hr_sum - toInt(soll)).toFixed(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}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const csvfile = Bun.file(dir + file_pref + ".csv")
|
||||||
|
const csvfilewriter = csvfile.writer()
|
||||||
|
|
||||||
|
csvfilewriter.write(`${user.name};${MONTHS[date.getMonth()]} ${year};${hr_sum.toFixed(2)};${soll};${(hr_sum - toInt(soll)).toFixed(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTable(path: string) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ 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 } from "$lib/db_types";
|
||||||
import { parseDate, isTimeValidHHMM } from "$lib/util";
|
import { calculateDuration, parseDate, isTimeValidHHMM } from "$lib/util";
|
||||||
|
|
||||||
const DATABASES_PATH: string = "";
|
const DATABASES_PATH: string = "";
|
||||||
const USER_DATABASE_PATH: string = DATABASES_PATH + "users.sqlite";
|
const USER_DATABASE_PATH: string = DATABASES_PATH + "users.sqlite";
|
||||||
|
|
@ -11,11 +11,11 @@ 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, created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL);",
|
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name 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) VALUES ($name);";
|
"INSERT INTO users (name, initials) VALUES ($name, $initials);";
|
||||||
|
|
||||||
const USER_DATABASE_GET_USER: string =
|
const USER_DATABASE_GET_USER: string =
|
||||||
"SELECT * FROM users;";
|
"SELECT * FROM users;";
|
||||||
|
|
@ -60,35 +60,113 @@ const ENTRY_DATABASE_SETUP: string[] = [
|
||||||
BEGIN
|
BEGIN
|
||||||
UPDATE records SET (date, start, end, comment) = (null, null, null, null) WHERE OLD.id = id;
|
UPDATE records SET (date, start, end, comment) = (null, null, null, null) WHERE OLD.id = id;
|
||||||
SELECT raise(IGNORE);
|
SELECT raise(IGNORE);
|
||||||
END;`
|
END;`,
|
||||||
|
|
||||||
|
`CREATE TABLE estimates (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
year INTEGER NOT NULL,
|
||||||
|
month INTEGER NOT NULL,
|
||||||
|
estimate REAL NOT NULL,
|
||||||
|
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
modified DATETIME DEFAULT NULL,
|
||||||
|
modified_to INTEGER UNIQUE DEFAULT NULL,
|
||||||
|
FOREIGN KEY(modified_to) REFERENCES estimates(id)
|
||||||
|
);`,
|
||||||
|
|
||||||
|
`CREATE TRIGGER estimates_prevent_duplicates
|
||||||
|
BEFORE INSERT ON estimates
|
||||||
|
WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1
|
||||||
|
AND EXISTS(SELECT 1 FROM estimates WHERE year = NEW.year AND month = NEW.month)
|
||||||
|
BEGIN
|
||||||
|
SELECT raise (ABORT, 'Prevented INSERT of duplicate row');
|
||||||
|
END;`,
|
||||||
|
|
||||||
|
`CREATE TRIGGER estimates_prevent_update_if_superseded
|
||||||
|
BEFORE UPDATE ON estimates
|
||||||
|
WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1
|
||||||
|
AND (OLD.modified_to NOT NULL)
|
||||||
|
BEGIN
|
||||||
|
SELECT raise(ABORT, 'Modification on changed row is not allowed');
|
||||||
|
END;`,
|
||||||
|
|
||||||
|
`CREATE TRIGGER estimates_prevent_update
|
||||||
|
BEFORE UPDATE ON estimates
|
||||||
|
WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO estimates(year, month, estimate) VALUES (NEW.year, NEW.month, NEW.estimate);
|
||||||
|
UPDATE estimates SET (modified, modified_to) = (CURRENT_TIMESTAMP, last_insert_rowid()) WHERE NEW.id == id;
|
||||||
|
SELECT raise(IGNORE);
|
||||||
|
END;`,
|
||||||
|
|
||||||
|
`CREATE TRIGGER estimates_prevent_delete
|
||||||
|
BEFORE DELETE ON estimates
|
||||||
|
BEGIN
|
||||||
|
SELECT raise(ABORT, 'DELETE is not allowed on this table');
|
||||||
|
END;`,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const ENTRY_DATABASE_GET_MONTHS: string =
|
||||||
|
"SELECT DISTINCT SUBSTR(date, 7, 4) as year, SUBSTR(date, 4, 2) as month FROM records ORDER BY year DESC, month DESC;"
|
||||||
|
|
||||||
const ENTRY_DATABASE_GET_ENTRY_BY_ID: string =
|
const ENTRY_DATABASE_GET_ENTRY_BY_ID: string =
|
||||||
"SELECT * FROM records WHERE modified_to ISNULL AND id = $id;"
|
"SELECT * FROM records WHERE modified_to ISNULL AND id = $id;"
|
||||||
|
|
||||||
|
const ENTRY_DATABASE_GET_ENTRIES_IN_MONTH: string =
|
||||||
|
"SELECT * FROM records WHERE modified_to ISNULL AND SUBSTR(date, 7, 4) = $year AND SUBSTR(date, 4, 2) = $month ORDER BY SUBSTR(date, 1, 2);"
|
||||||
|
|
||||||
const ENTRY_DATABASE_GET_ENTRIES: string =
|
const ENTRY_DATABASE_GET_ENTRIES: string =
|
||||||
"SELECT * FROM records WHERE modified_to ISNULL;"
|
"SELECT * FROM records WHERE modified_to ISNULL ORDER BY SUBSTR(date, 7, 4) DESC, SUBSTR(date, 4, 2) DESC, SUBSTR(date, 1, 2) DESC;"
|
||||||
|
|
||||||
const ENTRY_DATABASE_ADD_ENTRY: string =
|
const ENTRY_DATABASE_ADD_ENTRY: string =
|
||||||
"INSERT INTO records(date, start, end, comment) VALUES ($date, $start, $end, $comment);"
|
"INSERT INTO records(date, start, end, comment) VALUES ($date, $start, $end, $comment);"
|
||||||
|
|
||||||
const Entry_DATABASE_EDIT_ENTRY: string =
|
const ENTRY_DATABASE_EDIT_ENTRY: string =
|
||||||
"UPDATE records SET date = $date, start = $start, end = $end, comment = $comment WHERE id = $id;";
|
"UPDATE records SET date = $date, start = $start, end = $end, comment = $comment WHERE id = $id;";
|
||||||
|
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
user: UserEntry;
|
id: number;
|
||||||
|
name: string;
|
||||||
|
created: string;
|
||||||
private database: Database;
|
private database: Database;
|
||||||
|
|
||||||
constructor(user: UserEntry, db: Database) {
|
constructor(user: UserEntry, db: Database) {
|
||||||
this.user = user;
|
this.id = user.id;
|
||||||
|
this.name = user.name;
|
||||||
|
this.created = user.created;
|
||||||
this._database = db;
|
this._database = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_months(): { year: number, month: number }[] {
|
||||||
|
const query = this._database.query(ENTRY_DATABASE_GET_MONTHS);
|
||||||
|
const res = query.all();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_hr_sum(year: number, month: number): number {
|
||||||
|
const months = this.get_entries_by_month(year, month);
|
||||||
|
|
||||||
|
let sum = 0;
|
||||||
|
months.forEach((record) => { sum += calculateDuration(record.start, record.end) })
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
get_entries(): Entry[] {
|
get_entries(): Entry[] {
|
||||||
const query = this._database.query(ENTRY_DATABASE_GET_ENTRIES);
|
const query = this._database.query(ENTRY_DATABASE_GET_ENTRIES);
|
||||||
const res = query.all()
|
const res = query.all()
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_entries_by_month(year: number, month: number): Entry[] {
|
||||||
|
if (!(month > 0 && month < 13)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this._database.query(ENTRY_DATABASE_GET_ENTRIES_IN_MONTH);
|
||||||
|
const res = query.all({ year: year.toString(), month: month.toString().padStart(2, '0') });
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,7 +195,7 @@ export class User {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = this._database.query(Entry_DATABASE_EDIT_ENTRY);
|
const query = this._database.query(ENTRY_DATABASE_EDIT_ENTRY);
|
||||||
const res = query.run({ id: id, date: ndate, start: nstart, end: nend, comment: ncomment });
|
const res = query.run({ id: id, date: ndate, start: nstart, end: nend, comment: ncomment });
|
||||||
|
|
||||||
return res.changes > 1;
|
return res.changes > 1;
|
||||||
|
|
@ -169,11 +247,11 @@ export function close_db() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function create_user(name: string): boolean {
|
export function create_user(name: string, initials: string): boolean {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const statement = user_database.query(USER_DATABASE_ADD_USER);
|
const statement = user_database.query(USER_DATABASE_ADD_USER);
|
||||||
const result = statement.run({ name: name });
|
const result = statement.run({ name: name, initials: initials });
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -186,22 +264,22 @@ export function create_user(name: string): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _get_user(): UserEntry {
|
function _get_user(): UserEntry | null {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const statement = user_database.prepare(USER_DATABASE_GET_USER);
|
const statement = user_database.prepare(USER_DATABASE_GET_USER);
|
||||||
const result: UserEntry = statement.get();
|
const result: UserEntry = statement.get();
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
create_user("PM");
|
create_user("Patrick Maschek", "PM");
|
||||||
return get_user();
|
return _get_user();
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof SQLiteError) {
|
if (e instanceof SQLiteError) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
|
|
@ -212,6 +290,9 @@ function _get_user(): UserEntry {
|
||||||
export function get_user(): User | null {
|
export function get_user(): User | null {
|
||||||
|
|
||||||
const user = _get_user();
|
const user = _get_user();
|
||||||
|
if (user == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const db_name = get_user_db_name(user);
|
const db_name = get_user_db_name(user);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
|
||||||
|
const WEEKDAYS: string[] = [ "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" ]
|
||||||
|
|
||||||
export function toInt(str: string): number {
|
export function toInt(str: string): number {
|
||||||
let value = 0;
|
let value = 0;
|
||||||
for (let i = 0; i < str.length; ++i) {
|
for (let i = 0; i < str.length; ++i) {
|
||||||
|
|
@ -13,6 +15,36 @@ export function toInt(str: string): number {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toFloat(str: string): number {
|
||||||
|
let value = 0;
|
||||||
|
let value_after_dot = 0;
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
for (i = 0; i < str.length; ++i) {
|
||||||
|
let c = str.charAt(i);
|
||||||
|
if (c === ',' || c === '.') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let n = Number(c);
|
||||||
|
if (isNaN(n)) {
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
value = value * 10 + n;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dec = 1;
|
||||||
|
for (++i; i < str.length; ++i, ++dec) {
|
||||||
|
let c = str.charAt(i);
|
||||||
|
let n = Number(c);
|
||||||
|
if (isNaN(n)) {
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
value += n / Math.pow(10, dec);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseDate(str: string): Date | null {
|
export function parseDate(str: string): Date | null {
|
||||||
if (str.length != 2+1+2+1+4) {
|
if (str.length != 2+1+2+1+4) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -35,10 +67,10 @@ export function parseDate(str: string): Date | null {
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateDuration(start: string, end: string): string {
|
export function calculateDuration(start: string, end: string): number {
|
||||||
|
|
||||||
if (start.length !== 5 || end.length !== 5) {
|
if (start.length !== 5 || end.length !== 5) {
|
||||||
return "";
|
return NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_h = toInt(start.slice(0, 2));
|
let start_h = toInt(start.slice(0, 2));
|
||||||
|
|
@ -49,7 +81,7 @@ export function calculateDuration(start: string, end: string): string {
|
||||||
|
|
||||||
if (isNaN(start_h) || isNaN(start_m) || start.charAt(2) !== ':'
|
if (isNaN(start_h) || isNaN(start_m) || start.charAt(2) !== ':'
|
||||||
|| isNaN(end_h) || isNaN(end_m) || end.charAt(2) !== ':') {
|
|| isNaN(end_h) || isNaN(end_m) || end.charAt(2) !== ':') {
|
||||||
return "";
|
return NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_n = start_h * 60 + start_m;
|
let start_n = start_h * 60 + start_m;
|
||||||
|
|
@ -57,7 +89,7 @@ export function calculateDuration(start: string, end: string): string {
|
||||||
|
|
||||||
let duration = (end_n - start_n) / 60;
|
let duration = (end_n - start_n) / 60;
|
||||||
|
|
||||||
return duration.toFixed(2);
|
return duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function localToIsoDate(str: string): string | undefined {
|
export function localToIsoDate(str: string): string | undefined {
|
||||||
|
|
@ -87,3 +119,12 @@ export function isTimeValidHHMM(str: string): boolean {
|
||||||
|
|
||||||
return (!(isNaN(h) || isNaN(m))) && h < 24 && m < 60 && str.charAt(2) == ':';
|
return (!(isNaN(h) || isNaN(m))) && h < 24 && m < 60 && str.charAt(2) == ':';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function weekday_of(date: Date | null): string {
|
||||||
|
if (date == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const weekday = date.getDay();
|
||||||
|
return WEEKDAYS[weekday];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,41 @@
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="nav">
|
||||||
|
<ul>
|
||||||
|
<li><a href="/dokumente">Ausdrucken</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul {
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
|
||||||
|
width: 80%;
|
||||||
|
|
||||||
|
list-style-type: none;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul li {
|
||||||
|
border: black;
|
||||||
|
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import { page } from "$app/state"
|
import { page } from "$app/state"
|
||||||
|
|
||||||
import type { RecordEntry } from "$lib/db_types"
|
import type { RecordEntry } from "$lib/db_types"
|
||||||
import { toInt, parseDate, calculateDuration } from "$lib/util";
|
import { toInt, parseDate, calculateDuration, weekday_of } from "$lib/util";
|
||||||
|
|
||||||
import type { RowState } from "./record_input_row.svelte"
|
import type { RowState } from "./record_input_row.svelte"
|
||||||
import RecordInputRow from "./record_input_row.svelte"
|
import RecordInputRow from "./record_input_row.svelte"
|
||||||
|
|
@ -12,8 +12,6 @@
|
||||||
|
|
||||||
let { data, form } : { data: { records: RecordEntry[] }, form: any } = $props();
|
let { data, form } : { data: { records: RecordEntry[] }, form: any } = $props();
|
||||||
|
|
||||||
const WEEKDAYS: string[] = [ "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" ]
|
|
||||||
|
|
||||||
const HEADERS: string[] = [ "Datum", "Wochentag", "Beginn", "Ende", "Dauer", "Anmerkung" ];
|
const HEADERS: string[] = [ "Datum", "Wochentag", "Beginn", "Ende", "Dauer", "Anmerkung" ];
|
||||||
|
|
||||||
const status_ok = "ok";
|
const status_ok = "ok";
|
||||||
|
|
@ -104,12 +102,11 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrap">
|
|
||||||
|
|
||||||
<form id="form_new_entry" method="POST" action="?/new_entry" onsubmit={(e) => validateForm(e, new_state)} use:enhance></form>
|
<form id="form_new_entry" method="POST" action="?/new_entry" onsubmit={(e) => validateForm(e, new_state)} use:enhance></form>
|
||||||
<form id="form_edit_entry" method="POST" action="?/edit_entry" onsubmit={(e) => validateForm(e, edit_state)} use:enhance></form>
|
<form id="form_edit_entry" method="POST" action="?/edit_entry" onsubmit={(e) => validateForm(e, edit_state)} use:enhance></form>
|
||||||
|
|
||||||
<table class="list">
|
<table>
|
||||||
<caption>Stundenliste</caption>
|
<caption>Stundenliste</caption>
|
||||||
|
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -135,17 +132,12 @@
|
||||||
|
|
||||||
{#each data.records as entry}
|
{#each data.records as entry}
|
||||||
{#if editing?.id != entry.id }
|
{#if editing?.id != entry.id }
|
||||||
{@const weekday = parseDate(entry.date)?.getDay()}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{entry.date}</td>
|
<td>{entry.date}</td>
|
||||||
{#if weekday != null }
|
<td>{weekday_of(parseDate(entry.date))}</td>
|
||||||
<td>{WEEKDAYS[weekday]}</td>
|
|
||||||
{:else}
|
|
||||||
<td></td>
|
|
||||||
{/if}
|
|
||||||
<td>{entry.start}</td>
|
<td>{entry.start}</td>
|
||||||
<td>{entry.end}</td>
|
<td>{entry.end}</td>
|
||||||
<td>{calculateDuration(entry.start, entry.end)}</td>
|
<td>{calculateDuration(entry.start, entry.end).toFixed(2)}</td>
|
||||||
<td>{entry.comment ?? ""}</td>
|
<td>{entry.comment ?? ""}</td>
|
||||||
<td>
|
<td>
|
||||||
<form
|
<form
|
||||||
|
|
@ -192,7 +184,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
|
|
@ -214,18 +206,22 @@ table caption {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr:nth-child(odd) {
|
tbody > tr:nth-child(odd) {
|
||||||
background: gray;
|
background: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr:nth-child(even) {
|
tbody > tr:nth-child(even) {
|
||||||
background: lightgray;
|
background: lightgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr td:last-of-type {
|
tbody > tr > td:last-of-type {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tfoot > tr {
|
||||||
|
border-top: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
.td-no-elements {
|
.td-no-elements {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { toInt } from "$lib/util"
|
||||||
|
|
||||||
|
import { generateRecordPDF } from "$lib/server/PDFGen";
|
||||||
|
|
||||||
|
export async function load({ locals }) {
|
||||||
|
return {
|
||||||
|
availableMonths: locals.user.get_months()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
create_pdf: async ({ locals, request }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
|
||||||
|
let month = toInt(data.get("month") ?? "");
|
||||||
|
let year = toInt(data.get("year") ?? "");
|
||||||
|
|
||||||
|
if (isNaN(year) || isNaN(month) || month > 12) {
|
||||||
|
return fail(400, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
generateRecordPDF(locals.user, new Date(year, month - 1), data.get("soll"));
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
<script lang="ts">
|
||||||
|
interface _Props {
|
||||||
|
data: {
|
||||||
|
availableMonths: {
|
||||||
|
month: number,
|
||||||
|
year: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let { data } : _Props = $props();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<table class="list">
|
||||||
|
<caption>Stundenliste</caption>
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style:width="30ch">Monat</th>
|
||||||
|
<th style:width="12ch">Aktion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{#each data.availableMonths as month}
|
||||||
|
<tr>
|
||||||
|
<td>Stundenliste {month.month + "-" + month.year}</td>
|
||||||
|
<td>
|
||||||
|
<form id="create_pdf" method="POST" action="?/create_pdf">
|
||||||
|
<input type="hidden" name="month" value={month.month}>
|
||||||
|
<input type="hidden" name="year" value={month.year}/>
|
||||||
|
<input type="text" name="soll"/>
|
||||||
|
<button type="submit">PDF</button>
|
||||||
|
</form>
|
||||||
|
</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}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
form {
|
||||||
|
width: fit-content;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 50%;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
table caption {
|
||||||
|
font-size: 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody > tr > td {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody > tr:nth-child(odd) {
|
||||||
|
background: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody > tr:nth-child(even) {
|
||||||
|
background: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody > tr > td:last-of-type {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
tfoot {
|
||||||
|
border-top: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.td-no-elements {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
import { toInt, parseDate, calculateDuration } from "$lib/util";
|
import { toInt, parseDate, calculateDuration, weekday_of } from "$lib/util";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
targetForm: string,
|
targetForm: string,
|
||||||
|
|
@ -19,8 +19,6 @@
|
||||||
}
|
}
|
||||||
let { targetForm, enabled, states = $bindable(), children }: Props = $props();
|
let { targetForm, enabled, states = $bindable(), children }: Props = $props();
|
||||||
|
|
||||||
const WEEKDAYS: string[] = [ "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" ]
|
|
||||||
|
|
||||||
const TODAY: Date = new Date();
|
const TODAY: Date = new Date();
|
||||||
const CENTURY_PREF: number = Math.floor(TODAY.getFullYear() / 100);
|
const CENTURY_PREF: number = Math.floor(TODAY.getFullYear() / 100);
|
||||||
const CENTURY_YEAR: number = CENTURY_PREF * 100;
|
const CENTURY_YEAR: number = CENTURY_PREF * 100;
|
||||||
|
|
@ -61,7 +59,7 @@
|
||||||
if (states?.date?.valid && states?.date?.value != null) {
|
if (states?.date?.valid && states?.date?.value != null) {
|
||||||
date = parseDate(states.date.value);
|
date = parseDate(states.date.value);
|
||||||
}
|
}
|
||||||
return date ? WEEKDAYS[date.getDay()] : "";
|
return weekday_of(date);
|
||||||
})
|
})
|
||||||
let inDuration: string = $state("");
|
let inDuration: string = $state("");
|
||||||
updateDuration();
|
updateDuration();
|
||||||
|
|
@ -70,7 +68,8 @@
|
||||||
function updateDuration() {
|
function updateDuration() {
|
||||||
if ((states?.start?.valid && states?.end?.valid)
|
if ((states?.start?.valid && states?.end?.valid)
|
||||||
&& (states?.start?.value != null && states?.end?.value != null)) {
|
&& (states?.start?.value != null && states?.end?.value != null)) {
|
||||||
inDuration = calculateDuration(states.start.value, states.end.value)
|
let duration = calculateDuration(states.start.value, states.end.value);
|
||||||
|
inDuration = isNaN(duration) ? "" : duration.toFixed(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue