Added estimates, populating database with csv and started pdf generation

This commit is contained in:
Patrick 2025-01-30 12:31:25 +01:00
parent d273e659e7
commit 2cedcbcee9
11 changed files with 493 additions and 93 deletions

88
populate.py Normal file
View File

@ -0,0 +1,88 @@
import os.path
import sys
from argparse import ArgumentError
import sqlite3
def get_records(records):
extracted = []
with open(records, 'r', encoding='utf-8') as file:
for line in file:
line = line.strip('\n')
values = line.split(',', 3)
values[0] = values[0][8:] + "." + values[0][5:7] + "." + values[0][0:4]
values[3] = values[3].strip('"')
extracted.append(tuple(values))
return extracted
def get_estimates(estimates):
extracted = []
with open(estimates, 'r', encoding='utf-8') as file:
for line in file:
line = line.strip('\n')
values = line.split(',', 1)
extracted.append(tuple(values))
expanded = [extracted[0]]
for i, row in enumerate(extracted[1:]):
next_month = f'{int(expanded[-1][0][5:7]) + 1:02d}' if int(expanded[-1][0][5:7]) < 12 else "01"
next_date = f'{expanded[-1][0][:4] if int(expanded[-1][0][5:7]) < 12 else f'{int(expanded[-1][0][:4]) + 1:04d}'}-{next_month}'
while(next_date != row[0]):
expanded.append((next_date, None))
next_month = f'{int(expanded[-1][0][5:7]) + 1:02d}' if int(expanded[-1][0][5:7]) < 12 else "01"
next_date = f'{expanded[-1][0][:4] if int(expanded[-1][0][5:7]) < 12 else f'{int(expanded[-1][0][:4]) + 1:04d}'}-{next_month}'
expanded.append(row)
estimates = list(zip([int(r[0][:4]) for r in expanded][::3], [int(r[0][5:])//3 + 1 for r in expanded][::3], [r[1] for r in expanded][::3], [r[1] for r in expanded][1::3], [r[1] for r in expanded][2::3]))
estimates = list(filter(lambda e: not (e[2] == '' and e[3] == '' and e[4] == ''), estimates))
return estimates
def main():
if len(sys.argv) != 4:
raise ArgumentError(argument=None, message='Usage: python3 populate.py <userdb> <records> <estimates>')
userdb = sys.argv[1]
records_file = sys.argv[2]
estimates_file = sys.argv[3]
if not os.path.exists(userdb):
raise ArgumentError(argument=None, message='userdb must exist')
if not os.path.exists(records_file):
raise ArgumentError(argument=None, message=f'records file does not exist: {records_file}')
if not os.path.exists(estimates_file):
raise ArgumentError(argument=None, message=f'estimates file does not exist: {estimates_file}')
print("reading data...")
records = get_records(records_file)
estimates = get_estimates(estimates_file)
db = sqlite3.connect(userdb)
db_cursor = db.cursor()
print("inserting records...")
db_cursor.executemany("INSERT INTO records(date, start, end, comment) VALUES (?,?,?,?)", records)
print("inserting estimates...")
db_cursor.executemany("INSERT INTO estimates(year, quarter, estimate_0, estimate_1, estimate_2) VALUES (?,?,?,?,?)", estimates)
print("committing...")
db.commit()
db_cursor.close()
db.close()
print("Records and estimates populated")
if __name__ == '__main__':
main()

View File

@ -15,7 +15,14 @@ export interface RecordEntry {
end: string; end: string;
comment: string; comment: string;
created: string; created: string;
modified: string;
modified_to: number;
} }
export interface EstimatesEntry {
id: number;
year: number;
quarter: number;
estimate_0: number;
estimate_1: number;
estimate_2: number;
created: string;
}

View File

@ -1,21 +1,20 @@
import b from "bun"; import b from "bun";
import type { RecordEntry } from "$lib/db_types"; import type { RecordEntry } from "$lib/db_types";
import { toInt, parseDate, calculateDuration, weekday_of } from "$lib/util" import { toInt, padInt, parseDate, calculateDuration, month_of, weekday_of } from "$lib/util"
import { User } from "$lib/server/database" 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) {
export async function generateRecordPDF(user: User, date: Date, soll: str) {
const year = date.getFullYear(); const year = date.getFullYear();
const month = date.getMonth() + 1; const month = date.getMonth() + 1;
const dir = "pdfgen/user-" + user.id + "/"; const dir = "pdfgen/user-" + user.id + "/";
const file_pref = "Stundenliste-" + year + "-" + month; const file_pref = "Stundenliste-" + year + "-" + padInt(month, 2, 0);
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 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(); const { exitCode } = await b.$`mkdir -p ${dir}`.nothrow().quiet();
@ -26,21 +25,26 @@ export async function generateRecordPDF(user: User, date: Date, soll: str) {
// TODO: escape semicolon in comment // TODO: escape semicolon in comment
console.log(`${user.name};${MONTHS[date.getMonth()]} ${year};${hr_sum.toFixed(2)};${soll};${(hr_sum - toInt(soll)).toFixed(2)}`); 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) => { 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()
csvfilewriter.write(`${user.name};${MONTHS[date.getMonth()]} ${year};${hr_sum.toFixed(2)};${soll};${(hr_sum - toInt(soll)).toFixed(2)}\n`) csvfilewriter.write(`${user.name};${month_of(date)} ${year};${hr_sum.toFixed(2)};${estimate.toFixed(2)};${padInt(hr_sum - estimate, 2, 2, ' ')}\n`)
records.forEach((record) => { 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.write(`${record.date};${weekday_of(parseDate(record.date))};${record.start};${record.end};${calculateDuration(record.start, record.end).toFixed(2)};${record.comment}\n`)
}) })
csvfilewriter.end(); csvfilewriter.end();
await b.$`pdflatex -jobname=${dir + file_pref} -output-format=pdf template.tex`;
} }
function generateTable(path: string) { function generateTable(path: string) {

View File

@ -26,82 +26,75 @@ const ENTRY_DATABASE_SETUP: string[] = [
"CREATE TABLE meta (key TEXT PRIMARY KEY NOT NULL, value NUMBER);", "CREATE TABLE meta (key TEXT PRIMARY KEY NOT NULL, value NUMBER);",
"INSERT INTO meta(key, value) VALUES ('triggerActive', 1)", "INSERT INTO meta(key, value) VALUES ('triggerActive', 1)",
"CREATE TABLE records ( \ `CREATE TABLE records (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
date VARCHAR(10), \ date VARCHAR(10),
start VARCHAR(5), \ start VARCHAR(5),
end VARCHAR(5), \ end VARCHAR(5),
comment TEXT, \ comment TEXT,
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \ created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
modified DATETIME DEFAULT NULL, \ );`,
modified_to INTEGER UNIQUE DEFAULT NULL, \
FOREIGN KEY(modified_to) REFERENCES records(id) \
);",
`CREATE TRIGGER prevent_update_if_superseded `CREATE TABLE records_history (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
record_id INTEGER NOT NULL,
date VARCHAR(10),
start VARCHAR(5),
end VARCHAR(5),
comment TEXT,
modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY(record_id) REFERENCES records(id)
);`,
`CREATE TRIGGER records_update_history
BEFORE UPDATE ON records BEFORE UPDATE ON records
WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1 WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1
AND (OLD.modified_to NOT NULL OR OLD.date ISNULL)
BEGIN BEGIN
SELECT raise(ABORT, 'Modification on changed row is not allowed'); INSERT INTO records_history(record_id, date, start, end, comment) VALUES (OLD.id, OLD.date, OLD.start, OLD.end, OLD.comment);
END;`, END;`,
"CREATE TRIGGER prevent_update \ `CREATE TRIGGER records_delete_history
BEFORE UPDATE ON records \
WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1 \
BEGIN \
INSERT INTO records(date, start, end, comment) VALUES (NEW.date, NEW.start, NEW.end, NEW.comment); \
UPDATE records SET (modified, modified_to) = (CURRENT_TIMESTAMP, last_insert_rowid()) WHERE NEW.id == id; \
SELECT raise(IGNORE); \
END;",
`CREATE TRIGGER prevent_delete
BEFORE DELETE ON records BEFORE DELETE ON records
WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1
BEGIN BEGIN
UPDATE records SET (date, start, end, comment) = (null, null, null, null) WHERE OLD.id = id; INSERT INTO records_history(record_id, date, start, end, comment) VALUES (OLD.id, OLD.date, OLD.start, OLD.end, OLD.comment);
SELECT raise(IGNORE);
END;`, END;`,
`CREATE TABLE estimates ( `CREATE TABLE estimates (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
year INTEGER NOT NULL, year INTEGER NOT NULL,
month INTEGER NOT NULL, quarter INTEGER NOT NULL,
estimate REAL NOT NULL, estimate_0 REAL,
estimate_1 REAL,
estimate_2 REAL,
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
modified DATETIME DEFAULT NULL, UNIQUE(year, quarter)
modified_to INTEGER UNIQUE DEFAULT NULL,
FOREIGN KEY(modified_to) REFERENCES estimates(id)
);`, );`,
`CREATE TRIGGER estimates_prevent_duplicates `CREATE TABLE estimates_history (
BEFORE INSERT ON estimates id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1 estimate_id INTEGER NOT NULL,
AND EXISTS(SELECT 1 FROM estimates WHERE year = NEW.year AND month = NEW.month) year INTEGER NOT NULL,
BEGIN quarter INTEGER NOT NULL,
SELECT raise (ABORT, 'Prevented INSERT of duplicate row'); estimate_0 REAL NOT NULL,
END;`, estimate_1 REAL NOT NULL,
estimate_2 REAL NOT NULL,
modified DATETIME DEFAULT NULL,
FOREIGN KEY(estimate_id) REFERENCES estimates(id)
);`,
`CREATE TRIGGER estimates_prevent_update_if_superseded `CREATE TRIGGER estimates_update_history
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 BEFORE UPDATE ON estimates
WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1 WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1
BEGIN BEGIN
INSERT INTO estimates(year, month, estimate) VALUES (NEW.year, NEW.month, NEW.estimate); INSERT INTO estimates_history(estimate_id, year, quarter, estimate_0, estimate_1, estimate_2) VALUES (OLD.id, OLD.year, OLD.quarter, OLD.estimate_0, OLD.estimate_1, OLD.estimate_2);
UPDATE estimates SET (modified, modified_to) = (CURRENT_TIMESTAMP, last_insert_rowid()) WHERE NEW.id == id;
SELECT raise(IGNORE);
END;`, END;`,
`CREATE TRIGGER estimates_prevent_delete `CREATE TRIGGER estimates_delete_history
BEFORE DELETE ON estimates BEFORE DELETE ON estimates
WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1
BEGIN BEGIN
SELECT raise(ABORT, 'DELETE is not allowed on this table'); INSERT INTO estimates_history(estimate_id, year, quarter, estimate_0, estimate_1, estimate_2) VALUES (OLD.id, OLD.year, OLD.quarter, OLD.estimate_0, OLD.estimate_1, OLD.estimate_2);
END;`, END;`,
] ]
@ -109,13 +102,13 @@ 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;" "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 id = $id;"
const ENTRY_DATABASE_GET_ENTRIES_IN_MONTH: string = 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);" "SELECT * FROM records WHERE 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 ORDER BY SUBSTR(date, 7, 4) DESC, SUBSTR(date, 4, 2) DESC, SUBSTR(date, 1, 2) DESC;" "SELECT * FROM records 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);"
@ -123,6 +116,17 @@ const ENTRY_DATABASE_ADD_ENTRY: string =
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;";
const ESTIMATES_DATABASE_GET_ALL: string =
"SELECT * FROM estimates ORDER BY year DESC, quarter DESC;"
const ESTIMATES_DATABASE_GET_QUARTERS: string =
"SELECT year, quarter FROM estimates;"
const ESTIMATES_DATABASE_GET_MONTH: string =
"SELECT estimate_0, estimate_1, estimate_2 FROM estimates WHERE year = $year AND quarter = $quarter;"
const ESTIMATES_DATABASE_INSERT: string =
"INSERT INTO estimates(year, quarter, estimate_0, estimate_1, estimate_2) VALUES ($year, $quarter, $estimate_0, $estimate_1, $estimate_2);"
export class User { export class User {
id: number; id: number;
@ -137,13 +141,20 @@ export class User {
this._database = db; this._database = db;
} }
get_months(): { year: number, month: number }[] { get_months(): { year: string, month: string }[] {
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; return res;
} }
get_quarters(): { year: number, quarter: number }[] {
const query = this._database.query(ESTIMATES_DATABASE_GET_QUARTERS)
const res = query.all();
return res;
}
get_hr_sum(year: number, month: number): number { get_hr_sum(year: number, month: number): number {
const months = this.get_entries_by_month(year, month); const months = this.get_entries_by_month(year, month);
@ -159,7 +170,7 @@ export class User {
return res; return res;
} }
get_entries_by_month(year: number, month: number): Entry[] { get_entries_by_month(year: number, month: number): RecordEntry[] {
if (!(month > 0 && month < 13)) { if (!(month > 0 && month < 13)) {
return []; return [];
} }
@ -170,7 +181,7 @@ export class User {
return res; return res;
} }
get_entry(id: number): Entry { get_entry(id: number): RecordEntry {
const query = this._database.query(ENTRY_DATABASE_GET_ENTRY_BY_ID); const query = this._database.query(ENTRY_DATABASE_GET_ENTRY_BY_ID);
const res = query.get({ id: id }); const res = query.get({ id: id });
@ -189,10 +200,10 @@ export class User {
return res.changes == 1; return res.changes == 1;
} }
update_entry(id: number, ndate: string, nstart: string, nend: string, ncomment: string): RecordEntry | null { update_entry(id: number, ndate: string, nstart: string, nend: string, ncomment: string): boolean {
if (isNaN(id) || parseDate(ndate) == null || !isTimeValidHHMM(nstart) || !isTimeValidHHMM(nend)) { if (isNaN(id) || parseDate(ndate) == null || !isTimeValidHHMM(nstart) || !isTimeValidHHMM(nend)) {
return null; return false;
} }
const query = this._database.query(ENTRY_DATABASE_EDIT_ENTRY); const query = this._database.query(ENTRY_DATABASE_EDIT_ENTRY);
@ -200,6 +211,31 @@ export class User {
return res.changes > 1; return res.changes > 1;
} }
get_estimates(): Array<EstimatesEntry> {
const query = this._database.query(ESTIMATES_DATABASE_GET_ALL);
const res = query.all();
return res;
}
get_estimate(year: number, month: number): number {
const query = this._database.query(ESTIMATES_DATABASE_GET_MONTH);
const res = query.all({ year: year, quarter: Math.floor(month / 4 + 1) });
return res[0]?.[`estimate_${month % 3}`] ?? NaN;
}
insert_estimate(year: number, quarter: number, estimate_0: number, estimate_1: number, estimate_2: number) {
if (isNaN(year) || isNaN(quarter) || quarter < 1 || quarter > 4 || isNaN(estimate_0) || isNaN(estimate_1) || isNaN(estimate_2)) {
return null;
}
const query = this._database.query(ESTIMATES_DATABASE_INSERT);
const res = query.run({ year: year, quarter: quarter, estimate_0: estimate_0, estimate_1: estimate_1, estimate_2: estimate_2 });
return res.changes > 1;
}
} }
@ -228,7 +264,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) => { db.query(q).run(); }); setup_queries.forEach((q) => { console.log(q); db.query(q).run(); });
} }
export function init_db() { export function init_db() {

View File

@ -1,7 +1,12 @@
const WEEKDAYS: string[] = [ "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" ] 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 function toInt(str: string): number { export function toInt(str: string): number {
if (str.length === 0) {
return NaN;
}
let value = 0; let value = 0;
for (let i = 0; i < str.length; ++i) { for (let i = 0; i < str.length; ++i) {
let c = str.charAt(i); let c = str.charAt(i);
@ -16,6 +21,10 @@ export function toInt(str: string): number {
} }
export function toFloat(str: string): number { export function toFloat(str: string): number {
if (str.length === 0) {
return NaN;
}
let value = 0; let value = 0;
let value_after_dot = 0; let value_after_dot = 0;
@ -45,6 +54,18 @@ export function toFloat(str: string): number {
return value; return value;
} }
export function padInt(num: number, upper: number, float: number, pad_char: string = '0') {
if (num > 0) {
if (float != 0) {
return num.toFixed(float).padStart(upper + 1 + float, pad_char)
} else {
return num.toFixed(float).padStart(upper, pad_char);
}
} else {
return "-" + padInt(-1 * num, upper, float);
}
}
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;
@ -87,6 +108,11 @@ export function calculateDuration(start: string, end: string): number {
let start_n = start_h * 60 + start_m; let start_n = start_h * 60 + start_m;
let end_n = end_h * 60 + end_m; let end_n = end_h * 60 + end_m;
if (end_n < start_n) {
/* Assume it's the next day */
end_n += 24 * 60;
}
let duration = (end_n - start_n) / 60; let duration = (end_n - start_n) / 60;
return duration; return duration;
@ -120,6 +146,15 @@ 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 month_of(date: Date | null): string {
if (date == null) {
return "";
}
const month = date.getMonth();
return MONTHS[month];
}
export function weekday_of(date: Date | null): string { export function weekday_of(date: Date | null): string {
if (date == null) { if (date == null) {
return ""; return "";

View File

@ -4,6 +4,8 @@
<div class="nav"> <div class="nav">
<ul> <ul>
<li><a href="/">Stundenliste</a></li>
<li><a href="/schaetzung">Stundenschätzung</a></li>
<li><a href="/dokumente">Ausdrucken</a></li> <li><a href="/dokumente">Ausdrucken</a></li>
</ul> </ul>
</div> </div>

View File

@ -62,8 +62,6 @@ export const actions = {
return_obj.end.status = invalid; return_obj.end.status = invalid;
} }
console.log(return_obj);
if (Object.keys(return_obj).some((v) => { return return_obj[v].status != ok; })) { if (Object.keys(return_obj).some((v) => { return return_obj[v].status != ok; })) {
return fail(400, { new_entry: return_obj }); return fail(400, { new_entry: return_obj });
} }

View File

@ -4,7 +4,8 @@ import { generateRecordPDF } from "$lib/server/PDFGen";
export async function load({ locals }) { export async function load({ locals }) {
return { return {
availableMonths: locals.user.get_months() availableMonths: locals.user.get_months(),
availableQuarters: locals.user.get_quarters()
} }
} }
@ -19,7 +20,7 @@ export const actions = {
return fail(400, {}); return fail(400, {});
} }
generateRecordPDF(locals.user, new Date(year, month - 1), data.get("soll")); generateRecordPDF(locals.user, new Date(year, month - 1));
return { success: true } return { success: true }
} }

View File

@ -1,19 +1,31 @@
<script lang="ts"> <script lang="ts">
import { toInt } from "$lib/util";
interface _Props { interface _Props {
data: { data: {
availableMonths: { availableMonths: {
month: number, month: string,
year: string
}[],
availableQuarters: {
quarter: number
year: number year: number
}[] }[]
} }
}; };
let { data } : _Props = $props(); let { data } : _Props = $props();
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();
</script> </script>
<table class="list"> <table>
<caption>Stundenliste</caption> <caption>Dokumente</caption>
<thead> <thead>
<tr> <tr>
@ -23,17 +35,25 @@
</thead> </thead>
<tbody> <tbody>
{#each data.availableMonths as month} {#each documents as doc}
<tr> {#if doc.month != null}
<td>Stundenliste {month.month + "-" + month.year}</td> <tr>
<td> <td>Stundenliste {doc.month}-{doc.year}</td>
<form id="create_pdf" method="POST" action="?/create_pdf"> <td>
<input type="hidden" name="month" value={month.month}> <form id="create_pdf" method="POST" action="?/create_pdf">
<input type="hidden" name="year" value={month.year}/> <input type="hidden" name="month" value={doc.month}>
<input type="text" name="soll"/> <input type="hidden" name="year" value={doc.year}/>
<button type="submit">PDF</button> <button type="submit">PDF</button>
</form> </form>
</tr> </tr>
{:else}
<tr>
<td>Stundenschätzung {doc.quarter}. Quartal {doc.year}</td>
<td>
<form></form>
</td>
</tr>
{/if}
{/each} {/each}
<tr></tr> <tr></tr>
</tbody> </tbody>

View File

@ -0,0 +1,45 @@
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()
};
}
export const actions = {
add_quarter: async ({ locals, request }) => {
const data = await request.formData();
const year = data.get("year");
const quart = data.get("quarter");
const estimate_0 = data.get("estimate_0");
const estimate_1 = data.get("estimate_1");
const estimate_2 = data.get("estimate_2");
if (year == null || quart == null || estimate_0 == null || estimate_1 == null || estimate_2 == null) {
return fail(400, { year: year, quarter: quart, estimate_0: estimate_0, estimate_1: estimate_1, estimate_2: estimate_2 });
}
const y = toInt(year);
const q = toInt(quart)
const est_0 = toFloat(estimate_0);
const est_1 = toFloat(estimate_1);
const est_2 = toFloat(estimate_2);
if (isNaN(y) || isNaN(q) || q < 1 || q > 4 || isNaN(est_0) || isNaN(est_1) || isNaN(est_2)) {
return fail(400, { year: year, quarter: quart, estimate_0: estimate_0, estimate_1: estimate_1, estimate_2: estimate_2 });
}
const res = locals.user.insert_estimate(y, q, est_0, est_1, est_2)
if (!res) {
return fail(500, {});
}
return { success: true };
}
}

View File

@ -0,0 +1,164 @@
<script lang="ts">
import { month_of } from "$lib/util";
import { enhance } from "$app/forms";
interface Quarter {
year: number;
quarter: number;
estimate_0: number;
estimate_1: number;
estimate_2: number;
}
interface Props {
form: any;
data: {
estimates: Array<Quarter>;
}
}
let { form, data }: Props = $props();
const TODAY = new Date();
const NEXT_QUART = (data?.estimates?.length > 0)
? new Date(data.estimates[0].year + (data.estimates[0].quarter === 4 ? 1 : 0), 4 * (data.estimates[0].quarter - (data.estimates[0].quarter === 4 ? -4 : 0)))
: TODAY;
const add_quart = quart_from_date(NEXT_QUART);
function quart_from_date(date: Date) {
return Math.floor((date.getMonth()+1) / 4) + 1;
}
function month_from_quart(quart: number, num: number) {
return new Date(TODAY.getFullYear(), (quart - 1) * 4 + num);
}
</script>
<form id="form_add_quart" method="POST" action="?/add_quarter" use:enhance></form>
<table>
<caption>Stundenschätzung</caption>
<thead>
<tr>
<th style:width="20ch">Quartal</th>
<th style:width="30ch">Monat</th>
<th style:width="30ch">Schätzung</th>
<th style:width="12ch">Aktion</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="3">{add_quart}. Quartal {NEXT_QUART.getFullYear()}</td>
<td>{month_of(month_from_quart(add_quart, 0))}</td>
<!-- svelte-ignore a11y_positive_tabindex -->
<td><input form="form_add_quart" type="text" tabindex="1" name="estimate_0" value={form?.data?.estimate_0}/></td>
<td rowspan="3">
<input form="form_add_quart" type="hidden" name="year" value={NEXT_QUART.getFullYear()}/>
<input form="form_add_quart" type="hidden" name="quarter" value={add_quart}/>
<button form="form_add_quart" type="submit">+</button>
</td>
</tr>
<tr>
<td>{month_of(month_from_quart(add_quart, 1))}</td>
<!-- svelte-ignore a11y_positive_tabindex -->
<td><input form="form_add_quart" type="text" tabindex="2" name="estimate_1" value={form?.data?.estimate_1}/></td>
</tr>
<tr>
<td>{month_of(month_from_quart(add_quart, 2))}</td>
<!-- svelte-ignore a11y_positive_tabindex -->
<td><input form="form_add_quart" type="text" tabindex="3" name="estimate_2" value={form?.data?.estimate_2}/></td>
</tr>
{#each data.estimates as quarter}
{@const rowspan = (quarter.estimate_0 != null ? 1 : 0) + (quarter.estimate_1 != null ? 1 : 0) + (quarter.estimate_2 != null ? 1 : 0)}
{@const months = (() => {
let arr : { i: number, estimate: number}[] = [];
for (let i = 0; i < 3; ++i) {
if (quarter[`estimate_${i}`] != null) {
arr.push({i: i, estimate: quarter[`estimate_${i}`]});
}
}
return arr;
})()}
{#if months.length > 0}
<tr>
<td rowspan={rowspan}>
{quarter.quarter}. Quartal {quarter.year}
</td>
<td>{month_of(month_from_quart(quarter.quarter, months[0].i))}</td>
<td>{months[0].estimate.toFixed(2)}</td>
<td rowspan={rowspan}>Edit</td>
</tr>
{#each months.slice(1) as month}
<tr>
<td>{month_of(month_from_quart(quarter.quarter, month.i))}</td>
<td>{month.estimate.toFixed(2)}</td>
</tr>
{/each}
{/if}
{/each}
</tbody>
{#if data.estimates === undefined || data.estimates.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) > td[rowspan="3"] {
background: lightgray;
}
tbody > tr:nth-child(even) > td[rowspan="3"] {
background: gray;
}*/
tbody > tr:nth-child(odd) {
background: gray;
}
tbody > tr:nth-child(even) {
background: lightgray;
}
tfoot {
border-top: 1px solid black;
}
.td-no-elements {
text-align: center;
}
</style>