diff --git a/populate.py b/populate.py new file mode 100644 index 0000000..e02d9c6 --- /dev/null +++ b/populate.py @@ -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 = 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() diff --git a/src/lib/db_types.ts b/src/lib/db_types.ts index 5dd4ec7..821c0a4 100644 --- a/src/lib/db_types.ts +++ b/src/lib/db_types.ts @@ -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; +} diff --git a/src/lib/server/PDFGen.ts b/src/lib/server/PDFGen.ts index 537b20f..853e5a3 100644 --- a/src/lib/server/PDFGen.ts +++ b/src/lib/server/PDFGen.ts @@ -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) { diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index 9825820..172c8b2 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -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 { + 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() { diff --git a/src/lib/util.ts b/src/lib/util.ts index 6deac4d..78194f6 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -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 ""; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index fc56442..a537654 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,6 +4,8 @@ diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 8131ae6..2441ce9 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -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 }); } diff --git a/src/routes/dokumente/+page.server.ts b/src/routes/dokumente/+page.server.ts index 79d1583..6391ce4 100644 --- a/src/routes/dokumente/+page.server.ts +++ b/src/routes/dokumente/+page.server.ts @@ -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 } } diff --git a/src/routes/dokumente/+page.svelte b/src/routes/dokumente/+page.svelte index 18c6b93..36df376 100644 --- a/src/routes/dokumente/+page.svelte +++ b/src/routes/dokumente/+page.svelte @@ -1,19 +1,31 @@ - - +
Stundenliste
+ @@ -23,17 +35,25 @@ - {#each data.availableMonths as month} - - - + {#each documents as doc} + {#if doc.month != null} + + + + {:else} + + + + + {/if} {/each} diff --git a/src/routes/schaetzung/+page.server.ts b/src/routes/schaetzung/+page.server.ts new file mode 100644 index 0000000..ea2c1ba --- /dev/null +++ b/src/routes/schaetzung/+page.server.ts @@ -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 }; + } +} diff --git a/src/routes/schaetzung/+page.svelte b/src/routes/schaetzung/+page.svelte new file mode 100644 index 0000000..adc7aff --- /dev/null +++ b/src/routes/schaetzung/+page.svelte @@ -0,0 +1,164 @@ + + + + +
Dokumente
Stundenliste {month.month + "-" + month.year} -
- - - - -
-
Stundenliste {doc.month}-{doc.year} +
+ + + +
+
Stundenschätzung {doc.quarter}. Quartal {doc.year} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {#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} + + + + + + + + + {#each months.slice(1) as month} + + + + + {/each} + {/if} + {/each} + + +{#if data.estimates === undefined || data.estimates.length === 0} + + + + + +{/if} +
Stundenschätzung
QuartalMonatSchätzungAktion
{add_quart}. Quartal {NEXT_QUART.getFullYear()}{month_of(month_from_quart(add_quart, 0))} + + + +
{month_of(month_from_quart(add_quart, 1))}
{month_of(month_from_quart(add_quart, 2))}
+ {quarter.quarter}. Quartal {quarter.year} + {month_of(month_from_quart(quarter.quarter, months[0].i))}{months[0].estimate.toFixed(2)}Edit
{month_of(month_from_quart(quarter.quarter, month.i))}{month.estimate.toFixed(2)}
No records
+ + +