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;
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;
}

View File

@ -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) {

View File

@ -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() {

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 {
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 "";

View File

@ -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>

View File

@ -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 });
}

View File

@ -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 }
}

View File

@ -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>

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>