374 lines
9.8 KiB
Svelte
374 lines
9.8 KiB
Svelte
<script lang="ts">
|
|
import type { PageProps } from "./$types";
|
|
|
|
import { untrack } from "svelte";
|
|
import { enhance } from "$app/forms";
|
|
import { page } from "$app/state"
|
|
|
|
import type { RecordEntry } from "$lib/db_types"
|
|
import { MONTHS, toInt, padInt, parseDate, calculateDuration, weekday_of } from "$lib/util";
|
|
|
|
import type { RowState } from "./record_input_row.svelte"
|
|
import RecordInputRow from "./record_input_row.svelte"
|
|
|
|
|
|
let { data, form } : PageProps = $props();
|
|
|
|
//$inspect(data);
|
|
|
|
const HEADERS: string[] = [ "Datum", "Wochentag", "Beginn", "Ende", "Dauer", "Anmerkung" ];
|
|
|
|
const status_ok = "ok";
|
|
const status_missing = "missing";
|
|
const status_invalid = "invalid";
|
|
|
|
let new_state = $state() as RowState;
|
|
|
|
let editing: RecordEntry | null = $state(null);
|
|
let edit_state = $state() as RowState;
|
|
|
|
setNewState();
|
|
setEditing();
|
|
|
|
$effect(() => {
|
|
if (form) {
|
|
untrack(() => setNewState());
|
|
}
|
|
})
|
|
$effect(() => {
|
|
if (form) {
|
|
untrack(() => setEditing(null));
|
|
}
|
|
})
|
|
|
|
function setNewState() {
|
|
new_state = {
|
|
date: {
|
|
valid: form?.new_entry?.date?.value !== "" || form?.new_entry?.date?.valid,
|
|
value: form?.new_entry?.date?.value ?? "",
|
|
},
|
|
start: {
|
|
valid: form?.new_entry?.start?.value !== "" || form?.new_entry?.start?.valid,
|
|
value: form?.new_entry?.start?.value ?? "",
|
|
},
|
|
end: {
|
|
valid: form?.new_entry?.end?.value !== "" || form?.new_entry?.end?.valid,
|
|
value: form?.new_entry?.end?.value ?? "",
|
|
},
|
|
comment: {
|
|
value: form?.new_entry?.date?.comment ?? "",
|
|
},
|
|
}
|
|
}
|
|
|
|
function setEditing(entry: RecordEntry | null = null) {
|
|
if (entry) {
|
|
editing = entry;
|
|
} else if (form?.edit_entry) {
|
|
editing = form.edit_entry
|
|
} else if (page.url.searchParams.get("edit")) {
|
|
let id = toInt(page.url.searchParams.get("edit")!)
|
|
let entry = Array.from(data.records.values().map((months) => Array.from(months.values()))).flat(2).find((entry) => entry.id == id);
|
|
editing = entry ?? null;
|
|
} else {
|
|
editing = null;
|
|
}
|
|
|
|
edit_state = {
|
|
date: {
|
|
valid: editing?.id != form?.edit_entry?.id || !(form?.edit_entry?.date?.status && form.edit_entry.date.status != status_ok),
|
|
value: editing?.date ?? ""
|
|
},
|
|
start: {
|
|
valid: editing?.id != form?.edit_entry?.id || !(form?.edit_entry?.start?.status && form.edit_entry.start.status != status_ok),
|
|
value: editing?.start ?? ""
|
|
},
|
|
end: {
|
|
valid: editing?.id != form?.edit_entry?.id || !(form?.edit_entry?.end?.status && form.edit_entry.end.status != status_ok),
|
|
value: editing?.end ?? ""
|
|
},
|
|
comment: {
|
|
value: editing?.comment ?? ""
|
|
},
|
|
}
|
|
}
|
|
|
|
function validateForm(event: Event, state: RowState): boolean {
|
|
let valid = state.date.valid && state.date.value.length !== 0
|
|
&& state.start.valid && state.start.value.length !== 0
|
|
&& state.end.valid && state.end.value.length !== 0;
|
|
|
|
if (!valid) {
|
|
event.preventDefault();
|
|
}
|
|
return valid;
|
|
}
|
|
|
|
</script>
|
|
|
|
<form id="form_new_entry" method="POST" action="?/new_entry" onsubmit={(e) => validateForm(e, new_state)} use:enhance></form>
|
|
<form id="form_edit_entry" method="POST" action="?/edit_entry" onsubmit={(e) => validateForm(e, edit_state)} use:enhance></form>
|
|
<form id="form_remove_entry" method="POST" action="?/remove_entry" use:enhance></form>
|
|
|
|
<h1>Stundenliste</h1>
|
|
|
|
<table>
|
|
<colgroup>
|
|
<col style:width="5ex" class="cyear" />
|
|
<col style:width="5ex" class="cmonth" />
|
|
<col style:width="12ch" class="cdate" />
|
|
<col style:width="12ch" class="cweekday" />
|
|
<col style:width="7ch" class="cstart" />
|
|
<col style:width="7ch" class="cend" />
|
|
<col style:width="7ch" class="cduration" />
|
|
<col class="ccomment" />
|
|
<col style:width="12ch" class="caction" />
|
|
<col style:width="fit-content" class="month_stat" />
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<th></th>
|
|
<th>Datum</th>
|
|
<th>Wochentag</th>
|
|
<th>Beginn</th>
|
|
<th>Ende</th>
|
|
<th>Dauer</th>
|
|
<th>Anmerkung</th>
|
|
<th>Actions</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr style:border="none">
|
|
<td style:border="none"></td>
|
|
<td style:border="none"></td>
|
|
<RecordInputRow targetForm="form_new_entry" bind:states={new_state} enabled={editing == null} autofocus={true}>
|
|
<button
|
|
type="submit"
|
|
form="form_new_entry"
|
|
disabled={editing != null}>
|
|
+
|
|
</button>
|
|
</RecordInputRow>
|
|
</tr>
|
|
|
|
{#each data.estimates as estimates_pair}
|
|
{@const year = estimates_pair[0] }
|
|
{@const mmonths = estimates_pair[1] }
|
|
{@const yentries = data.records.get(year) ?? new Map()}
|
|
|
|
{#each mmonths as month_pair, mindex}
|
|
{@const month = month_pair[0] }
|
|
{@const estimate = month_pair[1] }
|
|
{@const entries = yentries.get(month) ?? [ null ] }
|
|
|
|
{#each entries as entry, eindex}
|
|
{@const record_sum = entries.reduce((acc, val) => acc + (val != null ? calculateDuration(val.start, val.end) : 0), 0)}
|
|
<!--<tr><td>{$inspect(year)}{$inspect(months)}{$inspect(month)}{$inspect(entries)}</td></tr>-->
|
|
<tr style:border-bottom={entries[0] == null ? "none" : ""} >
|
|
{#if mindex == 0 && eindex == 0}
|
|
<td class="year" rowspan={Array.from(yentries.values()).reduce((acc, val) => acc + ((val?.length ?? 0) < 4 ? 4 : val.length), 0) + 4 * (mmonths.size - yentries.size)}>{year}</td>
|
|
{/if}
|
|
{#if eindex == 0}
|
|
<td class={["month", month % 3 == 2 ? "quarter" : ""]} rowspan={entries.length < 4 ? 4 : entries.length }>{MONTHS[month]}</td>
|
|
{/if}
|
|
{#if entry == null}
|
|
<td style:text-align="center" colspan="7"></td>
|
|
{:else if editing?.id != entry.id }
|
|
<td>{entry.date}</td>
|
|
<td>{weekday_of(parseDate(entry.date))}</td>
|
|
<td class="start">{entry.start}</td>
|
|
<td class="end">{entry.end}</td>
|
|
<td class="duration">{padInt(calculateDuration(entry.start, entry.end), 2, 2, '\u2007;')}</td>
|
|
<td>{entry.comment ?? ""}</td>
|
|
<td class="action">
|
|
<form
|
|
id="form_edit"
|
|
method="GET"
|
|
onsubmit={(event) => {
|
|
event.preventDefault();
|
|
setEditing(entry);
|
|
}}
|
|
>
|
|
<button type="submit" name="edit" value={entry.id}>Edit</button>
|
|
</form>
|
|
</td>
|
|
{:else}
|
|
<RecordInputRow targetForm="form_edit_entry" bind:states={edit_state} enabled={true} >
|
|
<input type="hidden" form="form_edit_entry" name="id" value={entry.id} />
|
|
<button
|
|
type="submit"
|
|
form="form_edit_entry">
|
|
Save
|
|
</button>
|
|
<form
|
|
id="form_edit_cancel"
|
|
method="GET"
|
|
onsubmit={(event) => {
|
|
event.preventDefault();
|
|
setEditing(null);
|
|
}}
|
|
>
|
|
<button type="submit">Cancel</button>
|
|
</form>
|
|
<input type="hidden" form="form_remove_entry" name="id" value={entry.id} />
|
|
<button
|
|
type="submit"
|
|
form="form_remove_entry">
|
|
Delete
|
|
</button>
|
|
</RecordInputRow>
|
|
{/if}
|
|
{#if eindex == 0}
|
|
{@const document = data?.documents?.get(year)?.get(month + 1)}
|
|
|
|
<td rowspan={entries.length < 4 ? 4 : entries.length} class="stats">
|
|
<table>
|
|
<colgroup>
|
|
<col style:width="10ch" class="col_labels" />
|
|
<col style:width="7ch" class="col_values" />
|
|
</colgroup>
|
|
<tbody>
|
|
<tr>
|
|
<td>Ist</td>
|
|
<td>{padInt(record_sum, 1, 2, "0", false, false)}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Soll</td>
|
|
<td>{padInt(estimate, 1, 2, "0", false, false)}</td>
|
|
</tr>
|
|
<tr class="diff">
|
|
<td>Differenz</td>
|
|
<td>{padInt(record_sum - estimate, 3, 2, "\u2007", true, false)}</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="2" class="doc_action">
|
|
{#if document == null}
|
|
<form id="form_create_rec" method="POST" action="/dokumente?/create_record">
|
|
<input type="hidden" name="month" value={month + 1} />
|
|
<input type="hidden" name="year" value={year} />
|
|
<button type="submit">Stundenliste erstellen</button>
|
|
</form>
|
|
{:else}
|
|
<form id="form_download" method="GET" action={`/dokumente/${document.path}`} target="_blank">
|
|
<input type="hidden" name="month" value={month + 1} />
|
|
<input type="hidden" name="year" value={year} />
|
|
<button type="submit">Download</button>
|
|
</form>
|
|
{/if}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</td>
|
|
{/if}
|
|
</tr>
|
|
{/each}
|
|
{#if entries.length > 0 && entries.length < 4}
|
|
{#each { length: 4 - entries.length }, i }
|
|
<tr style="border: none;"><td><span style:visibility="hidden">#</span></td></tr>
|
|
{/each}
|
|
{/if}
|
|
|
|
{/each} <!-- months -->
|
|
|
|
{/each} <!-- data.estimates -->
|
|
<tr></tr>
|
|
</tbody>
|
|
{#if data.records === undefined || data.records.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: 80%;
|
|
margin: auto;
|
|
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
tbody > :global(tr) {
|
|
border-top: 1px solid black;
|
|
border-bottom: 1px solid black;
|
|
}
|
|
|
|
tbody > :global(tr:has(> td.year):has(td.month)) {
|
|
border-top: 3px double black;
|
|
}
|
|
tbody > :global(tr:has(> td.month)) {
|
|
border-top: 1.5px solid black;
|
|
}
|
|
|
|
tbody .year {
|
|
vertical-align: middle;
|
|
writing-mode: sideways-lr;
|
|
text-orientation: mixed;
|
|
}
|
|
|
|
tbody .month {
|
|
vertical-align: middle;
|
|
writing-mode: sideways-lr;
|
|
text-orientation: mixed;
|
|
}
|
|
|
|
.start, .end, .duration {
|
|
text-align: right;
|
|
}
|
|
|
|
tbody .action {
|
|
display: inline-flex;
|
|
}
|
|
|
|
tbody .stats {
|
|
min-height: fit-content;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
tbody .stats table tr {
|
|
border: none;
|
|
}
|
|
tbody .stats table .diff {
|
|
border-top: 1px solid black;
|
|
}
|
|
tbody .stats table tr td:first-of-type {
|
|
text-align: right;
|
|
}
|
|
tbody .stats table tr td:last-of-type {
|
|
text-align: right;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.doc_action * {
|
|
width: 100%;
|
|
}
|
|
|
|
tbody tr td {
|
|
vertical-align: text-bottom;
|
|
padding-left: 10px;
|
|
padding-right: 10px;
|
|
}
|
|
|
|
tfoot > tr {
|
|
border-top: 1px solid red;
|
|
}
|
|
|
|
.td-no-elements {
|
|
text-align: center;
|
|
}
|
|
|
|
</style>
|