Added estimates, populating database with csv and started pdf generation
This commit is contained in:
parent
d273e659e7
commit
2cedcbcee9
|
|
@ -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()
|
||||
|
|
@ -15,7 +15,14 @@ export interface RecordEntry {
|
|||
end: string;
|
||||
comment: 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
import b from "bun";
|
||||
|
||||
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"
|
||||
|
||||
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) {
|
||||
export async function generateRecordPDF(user: User, date: Date) {
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
|
||||
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 estimate = user.get_estimate(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();
|
||||
|
|
@ -26,21 +25,26 @@ export async function generateRecordPDF(user: User, date: Date, soll: str) {
|
|||
|
||||
// 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) => {
|
||||
console.log(`${record.date};${weekday_of(parseDate(record.date))};${record.start};${record.end};${calculateDuration(record.start, record.end).toFixed(2)};${record.comment}`)
|
||||
})
|
||||
|
||||
return;
|
||||
|
||||
const csvfile = Bun.file(dir + file_pref + ".csv")
|
||||
const csvfilewriter = csvfile.writer()
|
||||
|
||||
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) => {
|
||||
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();
|
||||
|
||||
await b.$`pdflatex -jobname=${dir + file_pref} -output-format=pdf template.tex`;
|
||||
}
|
||||
|
||||
function generateTable(path: string) {
|
||||
|
|
|
|||
|
|
@ -26,82 +26,75 @@ const ENTRY_DATABASE_SETUP: string[] = [
|
|||
"CREATE TABLE meta (key TEXT PRIMARY KEY NOT NULL, value NUMBER);",
|
||||
"INSERT INTO meta(key, value) VALUES ('triggerActive', 1)",
|
||||
|
||||
"CREATE TABLE records ( \
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \
|
||||
date VARCHAR(10), \
|
||||
start VARCHAR(5), \
|
||||
end VARCHAR(5), \
|
||||
comment TEXT, \
|
||||
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 TABLE records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
date VARCHAR(10),
|
||||
start VARCHAR(5),
|
||||
end VARCHAR(5),
|
||||
comment TEXT,
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);`,
|
||||
|
||||
`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
|
||||
WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1
|
||||
AND (OLD.modified_to NOT NULL OR OLD.date ISNULL)
|
||||
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;`,
|
||||
|
||||
"CREATE TRIGGER prevent_update \
|
||||
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
|
||||
`CREATE TRIGGER records_delete_history
|
||||
BEFORE DELETE ON records
|
||||
BEGIN
|
||||
UPDATE records SET (date, start, end, comment) = (null, null, null, null) WHERE OLD.id = id;
|
||||
SELECT raise(IGNORE);
|
||||
WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1
|
||||
BEGIN
|
||||
INSERT INTO records_history(record_id, date, start, end, comment) VALUES (OLD.id, OLD.date, OLD.start, OLD.end, OLD.comment);
|
||||
END;`,
|
||||
|
||||
`CREATE TABLE estimates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
year INTEGER NOT NULL,
|
||||
month INTEGER NOT NULL,
|
||||
estimate REAL NOT NULL,
|
||||
quarter INTEGER NOT NULL,
|
||||
estimate_0 REAL,
|
||||
estimate_1 REAL,
|
||||
estimate_2 REAL,
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
modified DATETIME DEFAULT NULL,
|
||||
modified_to INTEGER UNIQUE DEFAULT NULL,
|
||||
FOREIGN KEY(modified_to) REFERENCES estimates(id)
|
||||
UNIQUE(year, quarter)
|
||||
);`,
|
||||
|
||||
`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 TABLE estimates_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
estimate_id INTEGER NOT NULL,
|
||||
year INTEGER NOT NULL,
|
||||
quarter INTEGER NOT NULL,
|
||||
estimate_0 REAL NOT NULL,
|
||||
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');
|
||||
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;`,
|
||||
|
||||
`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
|
||||
`CREATE TRIGGER estimates_delete_history
|
||||
BEFORE DELETE ON estimates
|
||||
BEGIN
|
||||
SELECT raise(ABORT, 'DELETE is not allowed on this table');
|
||||
WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1
|
||||
BEGIN
|
||||
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;`,
|
||||
]
|
||||
|
||||
|
|
@ -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;"
|
||||
|
||||
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 =
|
||||
"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 =
|
||||
"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 =
|
||||
"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 =
|
||||
"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 {
|
||||
id: number;
|
||||
|
|
@ -137,13 +141,20 @@ export class User {
|
|||
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 res = query.all();
|
||||
|
||||
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 {
|
||||
const months = this.get_entries_by_month(year, month);
|
||||
|
||||
|
|
@ -159,7 +170,7 @@ export class User {
|
|||
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)) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -170,7 +181,7 @@ export class User {
|
|||
return res;
|
||||
}
|
||||
|
||||
get_entry(id: number): Entry {
|
||||
get_entry(id: number): RecordEntry {
|
||||
const query = this._database.query(ENTRY_DATABASE_GET_ENTRY_BY_ID);
|
||||
const res = query.get({ id: id });
|
||||
|
||||
|
|
@ -189,10 +200,10 @@ export class User {
|
|||
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)) {
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
const query = this._database.query(ENTRY_DATABASE_EDIT_ENTRY);
|
||||
|
|
@ -200,6 +211,31 @@ export class User {
|
|||
|
||||
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[]) {
|
||||
setup_queries.forEach((q) => { db.query(q).run(); });
|
||||
setup_queries.forEach((q) => { console.log(q); db.query(q).run(); });
|
||||
}
|
||||
|
||||
export function init_db() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
if (str.length === 0) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
let value = 0;
|
||||
for (let i = 0; i < str.length; ++i) {
|
||||
let c = str.charAt(i);
|
||||
|
|
@ -16,6 +21,10 @@ export function toInt(str: string): number {
|
|||
}
|
||||
|
||||
export function toFloat(str: string): number {
|
||||
if (str.length === 0) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
let value = 0;
|
||||
let value_after_dot = 0;
|
||||
|
||||
|
|
@ -45,6 +54,18 @@ export function toFloat(str: string): number {
|
|||
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 {
|
||||
if (str.length != 2+1+2+1+4) {
|
||||
return null;
|
||||
|
|
@ -87,6 +108,11 @@ export function calculateDuration(start: string, end: string): number {
|
|||
let start_n = start_h * 60 + start_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;
|
||||
|
||||
return duration;
|
||||
|
|
@ -120,6 +146,15 @@ export function isTimeValidHHMM(str: string): boolean {
|
|||
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 {
|
||||
if (date == null) {
|
||||
return "";
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
<div class="nav">
|
||||
<ul>
|
||||
<li><a href="/">Stundenliste</a></li>
|
||||
<li><a href="/schaetzung">Stundenschätzung</a></li>
|
||||
<li><a href="/dokumente">Ausdrucken</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -62,8 +62,6 @@ export const actions = {
|
|||
return_obj.end.status = invalid;
|
||||
}
|
||||
|
||||
console.log(return_obj);
|
||||
|
||||
if (Object.keys(return_obj).some((v) => { return return_obj[v].status != ok; })) {
|
||||
return fail(400, { new_entry: return_obj });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { generateRecordPDF } from "$lib/server/PDFGen";
|
|||
|
||||
export async function load({ locals }) {
|
||||
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, {});
|
||||
}
|
||||
|
||||
generateRecordPDF(locals.user, new Date(year, month - 1), data.get("soll"));
|
||||
generateRecordPDF(locals.user, new Date(year, month - 1));
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,31 @@
|
|||
<script lang="ts">
|
||||
import { toInt } from "$lib/util";
|
||||
|
||||
interface _Props {
|
||||
data: {
|
||||
availableMonths: {
|
||||
month: number,
|
||||
year: number
|
||||
}[]
|
||||
month: string,
|
||||
year: string
|
||||
}[],
|
||||
availableQuarters: {
|
||||
quarter: number
|
||||
year: number
|
||||
}[]
|
||||
}
|
||||
};
|
||||
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>
|
||||
|
||||
|
||||
<table class="list">
|
||||
<caption>Stundenliste</caption>
|
||||
<table>
|
||||
<caption>Dokumente</caption>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -23,17 +35,25 @@
|
|||
</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 documents as doc}
|
||||
{#if doc.month != null}
|
||||
<tr>
|
||||
<td>Stundenliste {doc.month}-{doc.year}</td>
|
||||
<td>
|
||||
<form id="create_pdf" method="POST" action="?/create_pdf">
|
||||
<input type="hidden" name="month" value={doc.month}>
|
||||
<input type="hidden" name="year" value={doc.year}/>
|
||||
<button type="submit">PDF</button>
|
||||
</form>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td>Stundenschätzung {doc.quarter}. Quartal {doc.year}</td>
|
||||
<td>
|
||||
<form></form>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
<tr></tr>
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue