This commit is contained in:
parent
4650cb84c3
commit
88041b8133
|
|
@ -123,6 +123,9 @@ 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 ENTRY_DATABASE_REMOVE_ENTRY: string =
|
||||
"DELETE FROM records WHERE id = $id;";
|
||||
|
||||
const ESTIMATES_DATABASE_GET_ALL: string =
|
||||
"SELECT * FROM estimates ORDER BY year DESC, quarter DESC;"
|
||||
|
||||
|
|
@ -228,6 +231,17 @@ export class User {
|
|||
return res.changes > 1;
|
||||
}
|
||||
|
||||
remove_entry(id: number): boolean {
|
||||
if (isNaN(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const query = this._database.query(ENTRY_DATABASE_REMOVE_ENTRY);
|
||||
const res = query.run({ id: id });
|
||||
|
||||
return res.changes > 1;
|
||||
}
|
||||
|
||||
get_estimates(): Array<EstimatesEntry> {
|
||||
const query = this._database.query(ESTIMATES_DATABASE_GET_ALL);
|
||||
const res = query.all();
|
||||
|
|
@ -256,6 +270,7 @@ export class User {
|
|||
|
||||
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 });
|
||||
console.log(res)
|
||||
|
||||
return res.changes > 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,40 @@ export async function getAllFiles(user: User) {
|
|||
return files;
|
||||
}
|
||||
|
||||
export async function getRecordFiles(user: User) {
|
||||
const path = `${DOCUMENTS_PATH}/user-${user.id}/${RECORDS_FOLD}`;
|
||||
|
||||
let file_names = await fs.readdir(path).catch((err) => { console.log(err); return [] });
|
||||
|
||||
let files = await Promise.all(file_names.map(async (v) => {
|
||||
return {
|
||||
identifier: v.replace(/^Stundenliste-/, "").replace(/\.pdf$/, ""),
|
||||
filename: v,
|
||||
path: `${RECORDS_FOLD}/${v}`,
|
||||
cdate: (await fs.stat(`${path}/${v}`)).ctime,
|
||||
}
|
||||
}))
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export async function getEstimateFiles(user: User) {
|
||||
const path = `${DOCUMENTS_PATH}/user-${user.id}/${ESTIMATES_FOLD}`;
|
||||
|
||||
let file_names = await fs.readdir(path).catch((err) => { console.log(err); return [] });
|
||||
|
||||
let files = await Promise.all(file_names.map(async (v) => {
|
||||
return {
|
||||
identifier: v.match(/\d.Quartal_\d\d\d\d/)[0].replace(/.Quartal_/, ".").split(".").reverse().join("-"),
|
||||
filename: v,
|
||||
path: `${ESTIMATES_FOLD}/${v}`,
|
||||
cdate: (await fs.stat(`${path}/${v}`)).ctime,
|
||||
}
|
||||
}))
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export async function getFile(user: User, filename: string) {
|
||||
const path = `${DOCUMENTS_PATH}/user-${user.id}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,16 @@
|
|||
export const MONTHS: string[] = [ "Jänner", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember" ];
|
||||
export const WEEKDAYS: string[] = [ "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" ];
|
||||
|
||||
|
||||
/*
|
||||
* Converts a string containing a decimal number to number.
|
||||
* If a non numeric (0-9) character is encountered returns NaN.
|
||||
*
|
||||
* @param str: string to be converted
|
||||
* @returns the converted string or NaN if failed
|
||||
*
|
||||
*/
|
||||
|
||||
export function toInt(str: string): number {
|
||||
if (str.length === 0) {
|
||||
return NaN;
|
||||
|
|
@ -10,8 +20,8 @@ export function toInt(str: string): number {
|
|||
let value = 0;
|
||||
for (let i = 0; i < str.length; ++i) {
|
||||
let c = str.charAt(i);
|
||||
let n = Number(c);
|
||||
if (isNaN(n)) {
|
||||
let n = c.charCodeAt(0) - "0".charCodeAt(0);
|
||||
if (isNaN(n) || n < 0 || n > 9) {
|
||||
return NaN;
|
||||
}
|
||||
value = value*10 + n;
|
||||
|
|
@ -20,6 +30,15 @@ export function toInt(str: string): number {
|
|||
return value;
|
||||
}
|
||||
|
||||
/*
|
||||
* Converts a string containing a decimal floating point number to number.
|
||||
* If a invalid character is encountered returns NaN.
|
||||
* Only one decimal separator may be present and may be one of "." or ",".
|
||||
*
|
||||
* @param str: string to convert to a number
|
||||
* @returns the floating point number or NaN if failed
|
||||
*
|
||||
*/
|
||||
export function toFloat(str: string): number {
|
||||
if (str.length === 0) {
|
||||
return NaN;
|
||||
|
|
@ -31,11 +50,12 @@ export function toFloat(str: string): number {
|
|||
let i = 0;
|
||||
for (i = 0; i < str.length; ++i) {
|
||||
let c = str.charAt(i);
|
||||
console.log("c", c)
|
||||
if (c === ',' || c === '.') {
|
||||
break;
|
||||
}
|
||||
let n = Number(c);
|
||||
if (isNaN(n)) {
|
||||
let n = c.charCodeAt(0) - "0".charCodeAt(0);
|
||||
if (isNaN(n) || n < 0 || n > 9) {
|
||||
return NaN;
|
||||
}
|
||||
value = value * 10 + n;
|
||||
|
|
@ -44,8 +64,8 @@ export function toFloat(str: string): number {
|
|||
let dec = 1;
|
||||
for (++i; i < str.length; ++i, ++dec) {
|
||||
let c = str.charAt(i);
|
||||
let n = Number(c);
|
||||
if (isNaN(n)) {
|
||||
let n = c.charCodeAt(0) - "0".charCodeAt(0);
|
||||
if (isNaN(n) || n < 0 || n > 9) {
|
||||
return NaN;
|
||||
}
|
||||
value += n / Math.pow(10, dec);
|
||||
|
|
@ -54,15 +74,25 @@ export function toFloat(str: string): number {
|
|||
return value;
|
||||
}
|
||||
|
||||
export function padInt(num: number, upper: number, float: number = 0, pad_char: string = '0') {
|
||||
export function padInt(num: number, upper: number, float: number = 0, pad_char: string = '0', add_plus_if_pos: boolean = false, show_dec_point_if_round: boolean = true, pad_char_back: string = "\u2007") {
|
||||
if (num == null) {
|
||||
return padInt(0, upper, float, pad_char, add_plus_if_pos, show_dec_point_if_round, pad_char_back)
|
||||
}
|
||||
if (num >= 0) {
|
||||
if (add_plus_if_pos) {
|
||||
return "+" + padInt(num, upper, float, pad_char, false, show_dec_point_if_round, pad_char_back);
|
||||
}
|
||||
if (float != 0) {
|
||||
return num.toFixed(float).padStart(upper + 1 + float, pad_char)
|
||||
if (show_dec_point_if_round || num.toFixed(float).search(".*\.0+") == -1) {
|
||||
return num.toFixed(float).padStart(upper + 1 + float, pad_char)
|
||||
} else {
|
||||
return (num.toFixed(0) + pad_char_back.repeat(1 + float)).padStart(upper + 1 + float, pad_char);
|
||||
}
|
||||
} else {
|
||||
return num.toFixed(float).padStart(upper, pad_char);
|
||||
}
|
||||
} else {
|
||||
return "-" + padInt(-1 * num, upper, float);
|
||||
return "-" + padInt(-1 * num, upper, float, pad_char, false, show_dec_point_if_round, pad_char_back);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,43 +2,77 @@
|
|||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="nav">
|
||||
{#snippet nav(classlist: string)}
|
||||
<div class={classlist}>
|
||||
<h1>Navigation</h1>
|
||||
<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">Dokumente</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div style:display="flex">
|
||||
{@render nav("navpos nav")}
|
||||
{@render nav("navpseudo nav")}
|
||||
|
||||
<div class="content">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
.nav {
|
||||
width: 100%;
|
||||
.navpos {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.navpseudo {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: 10%;
|
||||
height: 100%;
|
||||
|
||||
min-width: 130px;
|
||||
|
||||
padding: 8px;
|
||||
|
||||
border-right: 1px black solid;
|
||||
}
|
||||
|
||||
.nav ul {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
|
||||
width: 80%;
|
||||
|
||||
list-style-type: none;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
padding-right: 10px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.nav ul li {
|
||||
border: black;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
.nav h1 {
|
||||
width: 100%;
|
||||
|
||||
margin-top: 10px;
|
||||
|
||||
text-align: center;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content :global(h1) {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,60 @@
|
|||
import type { SQLiteError } from "bun:sqlite"
|
||||
import type { PageServerLoad, Actions } from "./$types";
|
||||
import type { SQLiteError } from "bun:sqlite";
|
||||
|
||||
import type { RecordEntry, EstimatesEntry } from "$lib/db_types";
|
||||
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
|
||||
import { toInt, parseDate, isTimeValidHHMM } from "$lib/util"
|
||||
import { MONTHS, toInt, parseDate, isTimeValidHHMM, month_of } from "$lib/util"
|
||||
import { get_user, User } from "$lib/server/database";
|
||||
import { getRecordFiles } from "$lib/server/docstore";
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
let records: { entry: RecordEntry, parsedDate: Date } = locals.user.get_entries().map((v) => ({ entry: v, parsedDate: parseDate(v.date) }));
|
||||
let estimates: EstimatesEntry = locals.user.get_estimates()
|
||||
let documents = (await getRecordFiles(locals.user)).map((v) => { return { year: toInt(v.identifier.substring(0, 4)), month: toInt(v.identifier.substring(5, 7)), file: v }; });
|
||||
|
||||
let records_grouped = Map.groupBy(records, (v) => { return v.parsedDate.getFullYear() } );
|
||||
|
||||
records_grouped.forEach((value, key, map) => {
|
||||
let m = Map.groupBy(value, (v) => v.parsedDate.getMonth());
|
||||
// remove parsed date
|
||||
m.forEach((value, key, map) => {
|
||||
map.set(key, value.map((v) => v.entry));
|
||||
});
|
||||
map.set(key, m);
|
||||
});
|
||||
|
||||
let estimates_grouped = Map.groupBy(estimates, (v) => { return v.year });
|
||||
estimates_grouped.forEach((value, key, map) => {
|
||||
let arr = value.map((v) => [
|
||||
{ key: ((v.quarter - 1) * 3 + 2), value: v["estimate_2"] },
|
||||
{ key: ((v.quarter - 1) * 3 + 1), value: v["estimate_1"] },
|
||||
{ key: ((v.quarter - 1) * 3 + 0), value: v["estimate_0"] },
|
||||
]).flat();
|
||||
|
||||
let m = Map.groupBy(arr, (e) => e.key);
|
||||
m.forEach((value, key, map) => {
|
||||
map.set(key, value[0].value);
|
||||
})
|
||||
map.set(key, m);
|
||||
})
|
||||
|
||||
let documents_grouped = Map.groupBy(documents, (v) => v.year );
|
||||
documents_grouped.forEach((value, key, map) => {
|
||||
let m = Map.groupBy(value, (v) => v.month);
|
||||
m.forEach((value, key, map) => {
|
||||
map.set(key, value.map((v) => v.file)[0]);
|
||||
})
|
||||
map.set(key, m);
|
||||
})
|
||||
|
||||
console.log(documents_grouped)
|
||||
|
||||
export async function load({ locals }) {
|
||||
return {
|
||||
records: locals.user.get_entries()
|
||||
records: records_grouped,
|
||||
estimates: estimates_grouped,
|
||||
documents: documents_grouped
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -140,6 +187,8 @@ export const actions = {
|
|||
return fail(400, { edit_entry: return_obj });
|
||||
}
|
||||
|
||||
id = toInt(id);
|
||||
|
||||
let current = locals.user.get_entry(id);
|
||||
|
||||
if (!current) {
|
||||
|
|
@ -167,4 +216,34 @@ export const actions = {
|
|||
redirect(303, '/');
|
||||
return { success: true };
|
||||
},
|
||||
}
|
||||
remove_entry: async ({ locals, request })=> {
|
||||
const data = await request.formData();
|
||||
|
||||
let id = data.get("id");
|
||||
|
||||
if (id == null) {
|
||||
return fail(400, { id: id });
|
||||
}
|
||||
|
||||
id = toInt(id);
|
||||
if (isNaN(id)) {
|
||||
return fail(400, { id: id });
|
||||
}
|
||||
|
||||
let res = false;
|
||||
|
||||
try {
|
||||
res = locals.user.remove_entry(id);
|
||||
} catch (e) {
|
||||
if (!(e instanceof SQLiteError)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
return fail(500, { });
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
} satisfies Actions;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
<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 { toInt, parseDate, calculateDuration, weekday_of } from "$lib/util";
|
||||
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 } : { data: { records: RecordEntry[] }, form: any } = $props();
|
||||
let { data, form } : PageProps = $props();
|
||||
|
||||
//$inspect(data);
|
||||
|
||||
const HEADERS: string[] = [ "Datum", "Wochentag", "Beginn", "Ende", "Dauer", "Anmerkung" ];
|
||||
|
||||
|
|
@ -64,7 +68,7 @@
|
|||
editing = form.edit_entry
|
||||
} else if (page.url.searchParams.get("edit")) {
|
||||
let id = toInt(page.url.searchParams.get("edit")!)
|
||||
let entry = data.records.find((entry) => { return entry.id == id; })
|
||||
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;
|
||||
|
|
@ -102,77 +106,174 @@
|
|||
|
||||
</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>
|
||||
<caption>Stundenliste</caption>
|
||||
|
||||
<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 style:width="20ch">Datum</th>
|
||||
<th style:width="25ch">Wochentag</th>
|
||||
<th style:width="12ch">Beginn</th>
|
||||
<th style:width="12ch">Ende</th>
|
||||
<th style:width="12ch">Dauer</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th>Datum</th>
|
||||
<th>Wochentag</th>
|
||||
<th>Beginn</th>
|
||||
<th>Ende</th>
|
||||
<th>Dauer</th>
|
||||
<th>Anmerkung</th>
|
||||
<th style:width="12ch">Actions</th>
|
||||
<th>Actions</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<RecordInputRow targetForm="form_new_entry" bind:states={new_state} enabled={editing == null}>
|
||||
<button
|
||||
type="submit"
|
||||
form="form_new_entry"
|
||||
disabled={editing != null}>
|
||||
+
|
||||
</button>
|
||||
</RecordInputRow>
|
||||
|
||||
{#each data.records as entry}
|
||||
{#if editing?.id != entry.id }
|
||||
<tr>
|
||||
<td>{entry.date}</td>
|
||||
<td>{weekday_of(parseDate(entry.date))}</td>
|
||||
<td>{entry.start}</td>
|
||||
<td>{entry.end}</td>
|
||||
<td>{calculateDuration(entry.start, entry.end).toFixed(2)}</td>
|
||||
<td>{entry.comment ?? ""}</td>
|
||||
<td>
|
||||
<form
|
||||
id="form_edit"
|
||||
method="GET"
|
||||
onsubmit={(event) => {
|
||||
event.preventDefault();
|
||||
setEditing(entry);
|
||||
}}
|
||||
>
|
||||
<button type="submit" name="edit" value={entry.id}>Edit</button>
|
||||
</form>
|
||||
</td>
|
||||
<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}>
|
||||
<button
|
||||
type="submit"
|
||||
form="form_new_entry"
|
||||
disabled={editing != null}>
|
||||
+
|
||||
</button>
|
||||
</RecordInputRow>
|
||||
</tr>
|
||||
{: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>
|
||||
</RecordInputRow>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#each data.estimates as estimates_pair}
|
||||
{@const year = estimates_pair[0] }
|
||||
{@const mmonths = estimates_pair[1] }
|
||||
{@const yentries = data.records.get(year)}
|
||||
|
||||
{#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)}
|
||||
|
||||
<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}`}>
|
||||
<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}
|
||||
|
|
@ -198,28 +299,71 @@ table {
|
|||
margin: auto;
|
||||
|
||||
border-collapse: collapse;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
table caption {
|
||||
font-size: 25px;
|
||||
font-weight: bold;
|
||||
tbody > :global(tr) {
|
||||
border-top: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
tbody > tr:nth-child(odd) {
|
||||
background: gray;
|
||||
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 > tr:nth-child(even) {
|
||||
background: lightgray;
|
||||
tbody .year {
|
||||
vertical-align: middle;
|
||||
writing-mode: sideways-lr;
|
||||
text-orientation: mixed;
|
||||
}
|
||||
|
||||
tbody > tr > td:last-of-type {
|
||||
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 black;
|
||||
border-top: 1px solid red;
|
||||
}
|
||||
|
||||
.td-no-elements {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import type { PageServerLoad, Actions } from "./$types";
|
||||
import { fail } from "@sveltejs/kit"
|
||||
import { fail, redirect } from "@sveltejs/kit"
|
||||
|
||||
import { toInt } from "$lib/util"
|
||||
|
||||
import { getAllFiles, generateEstimatePDF, generateRecordPDF } from "$lib/server/docstore";
|
||||
import { getAllFiles, getRecordFiles, getEstimateFiles, generateEstimatePDF, generateRecordPDF } from "$lib/server/docstore";
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
return {
|
||||
documents: await getAllFiles(locals.user)
|
||||
documents: await getAllFiles(locals.user),
|
||||
records: await getRecordFiles(locals.user),
|
||||
estimates: await getEstimateFiles(locals.user)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +49,9 @@ export const actions = {
|
|||
console.log(e);
|
||||
return fail(403, { success: false, message: e.toString(), year: year, month: month });
|
||||
}
|
||||
|
||||
|
||||
redirect(303, "dokumente")
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,91 @@
|
|||
|
||||
import { isoToLocalDate, padInt } from "$lib/util";
|
||||
|
||||
import Expander from "./expander.svelte";
|
||||
|
||||
let { data, form } : PageProps = $props();
|
||||
|
||||
$inspect(data);
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h1>Dokumente</h1>
|
||||
|
||||
<form method="POST" id="create_est" action="?/create_estimate" use:enhance></form>
|
||||
<form id="form_download" method="GET" >
|
||||
|
||||
{#snippet table(first_cell_name: string, rows)}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{first_cell_name}</th>
|
||||
<th>Dateiname</th>
|
||||
<th>Erstellt am</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each rows as file}
|
||||
<tr>
|
||||
<td>{file.identifier}</td>
|
||||
<td>{file.filename}</td>
|
||||
<td>{isoToLocalDate(file.cdate.toISOString())}<br/>um {padInt(file.cdate.getHours(), 2)}:{padInt(file.cdate.getMinutes(), 2)}:{padInt(file.cdate.getSeconds(), 2)}</td>
|
||||
<td>
|
||||
<form method="GET" action={`dokumente/${file.path}`} target="_blank">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/snippet}
|
||||
|
||||
<div class="content">
|
||||
<Expander id="records" label="Stundenlisten">
|
||||
{@render table("Monat", data.records)}
|
||||
</Expander>
|
||||
|
||||
<div style:height="25px"></div>
|
||||
|
||||
<Expander id="estimates" label="Stundenschätzungen">
|
||||
{@render table("Quartal", data.estimates)}
|
||||
</Expander>
|
||||
|
||||
<div style:height="25px"></div>
|
||||
|
||||
<Expander id="history" label="Verlauf">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Typ</th>
|
||||
<th>Datum</th>
|
||||
<th>Dateiname</th>
|
||||
<th>Erstellt am</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.history as file}
|
||||
<tr>
|
||||
<td>{file.type}</td>
|
||||
<td>{file.identifier}</td>
|
||||
<td>{file.filename}</td>
|
||||
<td>{isoToLocalDate(file.cdate.toISOString())}<br/>um {padInt(file.cdate.getHours(), 2)}:{padInt(file.cdate.getMinutes(), 2)}:{padInt(file.cdate.getSeconds(), 2)}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{/each}
|
||||
<tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Expander>
|
||||
</div>
|
||||
|
||||
<div style:height="100px"></div>
|
||||
|
||||
|
||||
<!--<form method="POST" id="create_est" action="?/create_estimate" use:enhance></form>
|
||||
<form method="POST" id="create_rec" action="?/create_record" use:enhance></form>
|
||||
|
||||
<table>
|
||||
|
|
@ -79,13 +156,12 @@
|
|||
</tr>
|
||||
</tfoot>
|
||||
{/if}
|
||||
</table>
|
||||
</table> -->
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
h1 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
|
@ -93,6 +169,41 @@ h1 {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 80%;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
border-top: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
tbody tr th {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
tbody tr td {
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
form {
|
||||
width: fit-content;
|
||||
border: none;
|
||||
|
|
@ -128,5 +239,5 @@ tfoot {
|
|||
|
||||
.td-no-elements {
|
||||
text-align: center;
|
||||
}
|
||||
}*/
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let { id, label, children } : { label: string, children: Snippet } = $props();
|
||||
|
||||
let _id = id + "_check";
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id={_id} style:display="none">
|
||||
<label for={_id}>
|
||||
<span class="twister closed"><span class="twister-inner">▷</span></span><span class="twister open"><span class="twister-inner">▽</span></span>{label}
|
||||
</label>
|
||||
<div class="hidden">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
label {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.twister {
|
||||
width: 1ch;
|
||||
padding-right: 2ch;
|
||||
}
|
||||
|
||||
.twister-inner {
|
||||
height: 100%;
|
||||
|
||||
vertical-align: middle;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.open {
|
||||
display: none;
|
||||
}
|
||||
.closed {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
|
||||
padding-top: 5px;
|
||||
padding-left: 3ch;
|
||||
}
|
||||
|
||||
:checked + label + .hidden {
|
||||
display: block;
|
||||
}
|
||||
:checked + label .closed {
|
||||
display: none;
|
||||
}
|
||||
:checked + label .open {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -62,17 +62,15 @@
|
|||
return weekday_of(date);
|
||||
})
|
||||
let inDuration: string = $state("");
|
||||
updateDuration();
|
||||
|
||||
|
||||
function updateDuration() {
|
||||
|
||||
$effect(() => {
|
||||
states;
|
||||
if ((states?.start?.valid && states?.end?.valid)
|
||||
&& (states?.start?.value != null && states?.end?.value != null)) {
|
||||
let duration = calculateDuration(states.start.value, states.end.value);
|
||||
inDuration = isNaN(duration) ? "" : duration.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
function validateDate(element: HTMLInputElement) {
|
||||
|
||||
|
|
@ -498,7 +496,10 @@
|
|||
|
||||
</script>
|
||||
|
||||
<tr>
|
||||
<!--<tr>
|
||||
{#each { length: prepend_td }, n}
|
||||
<td></td>
|
||||
{/each}-->
|
||||
<td>
|
||||
<input
|
||||
bind:this={dateInput}
|
||||
|
|
@ -545,7 +546,6 @@
|
|||
(_) => {
|
||||
states.start.valid = validateTime(startInput);
|
||||
states.start.value = startInput.value;
|
||||
updateDuration();
|
||||
}
|
||||
}
|
||||
disabled={!enabled}
|
||||
|
|
@ -570,7 +570,6 @@
|
|||
(_) => {
|
||||
states.end.valid = validateTime(endInput);
|
||||
states.end.value = endInput.value;
|
||||
updateDuration();
|
||||
}
|
||||
}
|
||||
disabled={!enabled}
|
||||
|
|
@ -590,11 +589,11 @@
|
|||
disabled={!enabled}>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td class="action">
|
||||
{@render children?.()}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<!--</tr>-->
|
||||
|
||||
<style>
|
||||
|
||||
|
|
@ -608,10 +607,10 @@ td input {
|
|||
|
||||
vertical-align: middle;
|
||||
|
||||
border: none;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
|
||||
tr td:last-of-type {
|
||||
.action {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,41 @@
|
|||
import type { PageServerLoad, Actions } from "./$types";
|
||||
|
||||
import { fail } from '@sveltejs/kit';
|
||||
|
||||
import { toInt, toFloat } from "$lib/util";
|
||||
import { MONTHS, toInt, toFloat } from "$lib/util";
|
||||
|
||||
import { getEstimateFiles } from "$lib/server/docstore";
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
|
||||
let estimates = locals.user.get_estimates();
|
||||
let documents = await getEstimateFiles(locals.user);
|
||||
|
||||
let estimates_grouped = Map.groupBy(estimates, (v) => v.year);
|
||||
estimates_grouped.forEach((value, key, map) => {
|
||||
|
||||
let quarters = Map.groupBy(value, (v) => v.quarter)
|
||||
quarters.forEach((_value, _key, _map) => {
|
||||
let months = _value.map((v) => [
|
||||
{ month: MONTHS[(v.quarter - 1) * 3 + 2], estimate: v.estimate_2 },
|
||||
{ month: MONTHS[(v.quarter - 1) * 3 + 1], estimate: v.estimate_1 },
|
||||
{ month: MONTHS[(v.quarter - 1) * 3 + 0], estimate: v.estimate_0 },
|
||||
] ).flat();
|
||||
|
||||
_map.set(_key, months);
|
||||
})
|
||||
map.set(key, quarters);
|
||||
})
|
||||
|
||||
let documents_grouped = Map.groupBy(documents, (v) => toInt(v.identifier.slice(0, 4)))
|
||||
documents_grouped.forEach((value, key, map) => {
|
||||
let quarters = Map.groupBy(value, (v) => toInt(v.identifier.slice(5, 7)));
|
||||
map.set(key, quarters);
|
||||
})
|
||||
|
||||
export async function load({ locals }) {
|
||||
return {
|
||||
estimates: locals.user.get_estimates()
|
||||
estimates: estimates_grouped,
|
||||
documents: documents_grouped,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -24,7 +55,7 @@ export const actions = {
|
|||
}
|
||||
|
||||
const y = toInt(year);
|
||||
const q = toInt(quart)
|
||||
const q = toInt(quart);
|
||||
const est_0 = toFloat(estimate_0);
|
||||
const est_1 = toFloat(estimate_1);
|
||||
const est_2 = toFloat(estimate_2);
|
||||
|
|
@ -33,6 +64,12 @@ export const actions = {
|
|||
return fail(400, { year: year, quarter: quart, estimate_0: estimate_0, estimate_1: estimate_1, estimate_2: estimate_2 });
|
||||
}
|
||||
|
||||
if (locals.user.get_estimate(y, q)) {
|
||||
return fail(409, { reason: "Estimate already exists" } );
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
|
||||
const res = locals.user.insert_estimate(y, q, est_0, est_1, est_2)
|
||||
|
||||
if (!res) {
|
||||
|
|
@ -41,4 +78,4 @@ export const actions = {
|
|||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
} satisfies Actions;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,61 @@
|
|||
<script lang="ts">
|
||||
import { month_of } from "$lib/util";
|
||||
import { enhance } from "$app/forms";
|
||||
import type { PageProps } from "./$types";
|
||||
|
||||
interface Quarter {
|
||||
year: number;
|
||||
quarter: number;
|
||||
estimate_0: number;
|
||||
estimate_1: number;
|
||||
estimate_2: number;
|
||||
}
|
||||
interface Props {
|
||||
form: any;
|
||||
data: {
|
||||
estimates: Array<Quarter>;
|
||||
import { enhance } from "$app/forms";
|
||||
import { MONTHS, toInt, toFloat, padInt } from "$lib/util";
|
||||
|
||||
let { form, data }: PageProps = $props();
|
||||
|
||||
$inspect(data);
|
||||
|
||||
let next = $state((() => {
|
||||
if (data.estimates.size == 0) {
|
||||
return { year: (new Date()).getFullYear(), quarter: (new Date()).getMonth() / 3 + 1 };
|
||||
}
|
||||
const max_year = Math.max(...Array.from(data.estimates.keys()))
|
||||
const max_quart = Math.max(...Array.from(data.estimates.get(max_year)?.keys() ?? [0]));
|
||||
|
||||
return { year: max_quart < 4 ? max_year : max_year + 1, quarter: max_quart < 4 ? max_quart + 1 : 1 };
|
||||
})());
|
||||
|
||||
let new_quart = $state(next.quarter);
|
||||
let estimate_store = $state({
|
||||
estimate_0: { value: "" },
|
||||
estimate_1: { value: "" },
|
||||
estimate_2: { value: "" },
|
||||
editing: { value: "" }
|
||||
})
|
||||
$inspect(estimate_store);
|
||||
|
||||
function validate_year(event: InputEvent) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (event.inputType.startsWith("delete") || !isNaN(toInt(event.data ?? ""))) {
|
||||
next.year = toInt(input.value);
|
||||
} else {
|
||||
input.value = next.year.toFixed(0);
|
||||
}
|
||||
}
|
||||
let { form, data }: Props = $props();
|
||||
|
||||
const TODAY = new Date();
|
||||
function validate_quarter(event: InputEvent) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
let in_num = event.data != null ? toInt(event.data) : NaN;
|
||||
if (event.inputType.startsWith("delete") || (!isNaN(in_num) && 1 <= in_num && in_num <= 4)) {
|
||||
next.quarter = toInt(input.value);
|
||||
} else {
|
||||
input.value = next.quarter.toFixed(0);
|
||||
}
|
||||
}
|
||||
|
||||
function validate_estimate(event: InputEvent, estimate: { value: string }) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (event.inputType.startsWith("delete") || !isNaN(toFloat(input.value))) {
|
||||
estimate.value = input.value;
|
||||
} else {
|
||||
input.value = estimate.value;
|
||||
}
|
||||
}
|
||||
|
||||
/*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;
|
||||
|
|
@ -30,25 +68,116 @@
|
|||
|
||||
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>
|
||||
<form id="form_create_pdf" method="POST" action="/dokumente?/create_estimate" use:enhance></form>
|
||||
|
||||
<table>
|
||||
<caption>Stundenschätzung</caption>
|
||||
<h1>Stundenschätzung</h1>
|
||||
|
||||
<table class="add">
|
||||
|
||||
<colgroup>
|
||||
<col style:min-width="30ch" />
|
||||
<col style:width="12ch" />
|
||||
<col style:width="10ch" />
|
||||
<col style:width="7ch" />
|
||||
</colgroup>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="3">Neue Schätzung: <input form="form_add_quart" style:width="7ch" type="number" name="year" oninput={validate_year} value={next.year} tabindex="8"/> - <input form="form_add_quart" style:width="4ch" type="number" name="quarter" oninput={validate_quarter} value={new_quart} tabindex="9"/>. Quartal</td>
|
||||
|
||||
<td>{MONTHS[(new_quart - 1) * 3 + 0]}</td>
|
||||
<td><input form="form_add_quart" type="text" name="estimate_0" oninput={(e) => validate_estimate(e, estimate_store.estimate_0)} tabindex="10" /></td>
|
||||
|
||||
<td rowspan="3"><button form="form_add_quart" type="submit" tabindex="13">Erstellen</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{MONTHS[(new_quart - 1) * 3 + 1]}</td>
|
||||
<td><input form="form_add_quart" type="text" name="estimate_1" oninput={(e) => validate_estimate(e, estimate_store.estimate_1)} tabindex="11" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{MONTHS[(new_quart - 1) * 3 + 2]}</td>
|
||||
<td><input form="form_add_quart" type="text" name="estimate_2" oninput={(e) => validate_estimate(e, estimate_store.estimate_2)} tabindex="12" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
<div style:width="100%" style:height="40px"></div>
|
||||
|
||||
<table class="list">
|
||||
|
||||
<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>
|
||||
<th style:width="5em"></th>
|
||||
<th style:width="10ch">Quartal</th>
|
||||
<th style:width="20ch">Monat</th>
|
||||
<th style:width="10ch">Soll</th>
|
||||
<th style:width="12ch" colspan="2">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
{#each data.estimates as year_pair}
|
||||
{@const year = year_pair[0]}
|
||||
{@const quarter_map = year_pair[1]}
|
||||
{#each quarter_map as quarter_pair, quarter_i}
|
||||
{@const quarter = quarter_pair[0]}
|
||||
{@const months = quarter_pair[1]}
|
||||
|
||||
{@const fill_str = "\u2014\u2007\u2007\u2007"}
|
||||
<tr>
|
||||
{#if quarter_i === 0}
|
||||
<td class="year" rowspan={quarter_map.size * 3}>{year}</td>
|
||||
{/if}
|
||||
|
||||
<td class="quarter" rowspan="3">{quarter}.</td>
|
||||
|
||||
<td>{months[0].month}</td>
|
||||
<td class="estimate">{months[0].estimate != null ? padInt(months[0].estimate, 3, 2, "\u2007", false, false) : fill_str}</td>
|
||||
|
||||
<td class="action" rowspan="3">
|
||||
{#if data.documents?.get(year)?.get(quarter)?.[0] != null}
|
||||
{@const document = data.documents.get(year).get(quarter)[0]}
|
||||
<form method="GET" action={`/dokumente/${document.path}`}>
|
||||
<button type="submit">Download PDF</button>
|
||||
</form>
|
||||
{:else}
|
||||
<form method="POST" action="/dokumente?/create_estimate" use:enhance>
|
||||
<input type="hidden" name="year" value={year} />
|
||||
<input type="hidden" name="quarter" value={quarter} />
|
||||
<button type="submit">Create PDF</button>
|
||||
</form>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{months[1].month}</td>
|
||||
<td class="estimate">{months[1].estimate != null ? padInt(months[0].estimate, 3, 2, "\u2007", false, false) : fill_str}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{months[2].month}</td>
|
||||
<td class="estimate">{months[2].estimate != null ? padInt(months[0].estimate, 3, 2, "\u2007", false, false) : fill_str}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
{#if data.estimates === undefined || data.estimates.size === 0}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class="td-no-elements" colspan="999">No records</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{/if}
|
||||
</table>
|
||||
|
||||
|
||||
{"" /*<tr>
|
||||
<td rowspan="3">{add_quart}. Quartal {NEXT_QUART.getFullYear()}</td>
|
||||
|
||||
<td>{month_of(month_from_quart(add_quart, 0))}</td>
|
||||
|
|
@ -70,19 +199,9 @@
|
|||
<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;
|
||||
})()}
|
||||
</tr>*/}
|
||||
|
||||
{"" /*
|
||||
{#if months.length > 0}
|
||||
<tr>
|
||||
<td rowspan={rowspan}>
|
||||
|
|
@ -100,19 +219,7 @@
|
|||
<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>
|
||||
|
||||
{/if}*/}
|
||||
|
||||
<style>
|
||||
|
||||
|
|
@ -122,43 +229,67 @@ form {
|
|||
}
|
||||
|
||||
table {
|
||||
width: 50%;
|
||||
margin: auto;
|
||||
|
||||
border-collapse: collapse;
|
||||
border: 1px solid;
|
||||
border: none;
|
||||
}
|
||||
|
||||
table caption {
|
||||
font-size: 25px;
|
||||
font-weight: bold;
|
||||
.add {
|
||||
|
||||
tr {
|
||||
border-bottom: solid 1px black;
|
||||
}
|
||||
tr:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
padding-left: 10px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
tbody > tr > td {
|
||||
text-align: center;
|
||||
}
|
||||
.list {
|
||||
/*width: 50%;*/
|
||||
margin: auto;
|
||||
|
||||
/*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:has(> td.year):has(td.quarter) {
|
||||
border-top: 3px black double;
|
||||
}
|
||||
|
||||
tbody > tr:nth-child(even) {
|
||||
background: lightgray;
|
||||
}
|
||||
tbody > tr:has(> td.quarter) {
|
||||
border-top: 1px black solid;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
border-top: 1px solid black;
|
||||
}
|
||||
tbody .year {
|
||||
vertical-align: middle;
|
||||
writing-mode: sideways-lr;
|
||||
text-orientation: mixed;
|
||||
}
|
||||
|
||||
.td-no-elements {
|
||||
text-align: center;
|
||||
tbody .estimate {
|
||||
padding-right: 1ch;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
tbody > tr > td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
border-top: 1px solid black;
|
||||
}
|
||||
|
||||
.td-no-elements {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
body {
|
||||
background: #BBBBBB;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
/*body * {
|
||||
|
|
|
|||
Loading…
Reference in New Issue