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 =
|
const ENTRY_DATABASE_EDIT_ENTRY: string =
|
||||||
"UPDATE records SET date = $date, start = $start, end = $end, comment = $comment WHERE id = $id;";
|
"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 =
|
const ESTIMATES_DATABASE_GET_ALL: string =
|
||||||
"SELECT * FROM estimates ORDER BY year DESC, quarter DESC;"
|
"SELECT * FROM estimates ORDER BY year DESC, quarter DESC;"
|
||||||
|
|
||||||
|
|
@ -228,6 +231,17 @@ export class User {
|
||||||
return res.changes > 1;
|
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> {
|
get_estimates(): Array<EstimatesEntry> {
|
||||||
const query = this._database.query(ESTIMATES_DATABASE_GET_ALL);
|
const query = this._database.query(ESTIMATES_DATABASE_GET_ALL);
|
||||||
const res = query.all();
|
const res = query.all();
|
||||||
|
|
@ -256,6 +270,7 @@ export class User {
|
||||||
|
|
||||||
const query = this._database.query(ESTIMATES_DATABASE_INSERT);
|
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 });
|
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;
|
return res.changes > 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,40 @@ export async function getAllFiles(user: User) {
|
||||||
return files;
|
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) {
|
export async function getFile(user: User, filename: string) {
|
||||||
const path = `${DOCUMENTS_PATH}/user-${user.id}`;
|
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 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" ];
|
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 {
|
export function toInt(str: string): number {
|
||||||
if (str.length === 0) {
|
if (str.length === 0) {
|
||||||
return NaN;
|
return NaN;
|
||||||
|
|
@ -10,8 +20,8 @@ export function toInt(str: string): number {
|
||||||
let value = 0;
|
let value = 0;
|
||||||
for (let i = 0; i < str.length; ++i) {
|
for (let i = 0; i < str.length; ++i) {
|
||||||
let c = str.charAt(i);
|
let c = str.charAt(i);
|
||||||
let n = Number(c);
|
let n = c.charCodeAt(0) - "0".charCodeAt(0);
|
||||||
if (isNaN(n)) {
|
if (isNaN(n) || n < 0 || n > 9) {
|
||||||
return NaN;
|
return NaN;
|
||||||
}
|
}
|
||||||
value = value*10 + n;
|
value = value*10 + n;
|
||||||
|
|
@ -20,6 +30,15 @@ export function toInt(str: string): number {
|
||||||
return value;
|
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 {
|
export function toFloat(str: string): number {
|
||||||
if (str.length === 0) {
|
if (str.length === 0) {
|
||||||
return NaN;
|
return NaN;
|
||||||
|
|
@ -31,11 +50,12 @@ export function toFloat(str: string): number {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (i = 0; i < str.length; ++i) {
|
for (i = 0; i < str.length; ++i) {
|
||||||
let c = str.charAt(i);
|
let c = str.charAt(i);
|
||||||
|
console.log("c", c)
|
||||||
if (c === ',' || c === '.') {
|
if (c === ',' || c === '.') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let n = Number(c);
|
let n = c.charCodeAt(0) - "0".charCodeAt(0);
|
||||||
if (isNaN(n)) {
|
if (isNaN(n) || n < 0 || n > 9) {
|
||||||
return NaN;
|
return NaN;
|
||||||
}
|
}
|
||||||
value = value * 10 + n;
|
value = value * 10 + n;
|
||||||
|
|
@ -44,8 +64,8 @@ export function toFloat(str: string): number {
|
||||||
let dec = 1;
|
let dec = 1;
|
||||||
for (++i; i < str.length; ++i, ++dec) {
|
for (++i; i < str.length; ++i, ++dec) {
|
||||||
let c = str.charAt(i);
|
let c = str.charAt(i);
|
||||||
let n = Number(c);
|
let n = c.charCodeAt(0) - "0".charCodeAt(0);
|
||||||
if (isNaN(n)) {
|
if (isNaN(n) || n < 0 || n > 9) {
|
||||||
return NaN;
|
return NaN;
|
||||||
}
|
}
|
||||||
value += n / Math.pow(10, dec);
|
value += n / Math.pow(10, dec);
|
||||||
|
|
@ -54,15 +74,25 @@ export function toFloat(str: string): number {
|
||||||
return value;
|
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 (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) {
|
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 {
|
} else {
|
||||||
return num.toFixed(float).padStart(upper, pad_char);
|
return num.toFixed(float).padStart(upper, pad_char);
|
||||||
}
|
}
|
||||||
} else {
|
} 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();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="nav">
|
{#snippet nav(classlist: string)}
|
||||||
|
<div class={classlist}>
|
||||||
|
<h1>Navigation</h1>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">Stundenliste</a></li>
|
<li><a href="/">Stundenliste</a></li>
|
||||||
<li><a href="/schaetzung">Stundenschätzung</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div style:display="flex">
|
||||||
|
{@render nav("navpos nav")}
|
||||||
|
{@render nav("navpseudo nav")}
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
.nav {
|
.navpos {
|
||||||
width: 100%;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
display: flex;
|
.navpseudo {
|
||||||
justify-content: center;
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
width: 10%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
min-width: 130px;
|
||||||
|
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
border-right: 1px black solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav ul {
|
.nav ul {
|
||||||
display: flex;
|
|
||||||
justify-content: right;
|
|
||||||
|
|
||||||
width: 80%;
|
|
||||||
|
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin-top: 10px;
|
padding: 0px;
|
||||||
margin-bottom: 10px;
|
margin: 0px;
|
||||||
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav ul li {
|
.nav ul li {
|
||||||
border: black;
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
margin-left: 10px;
|
.nav h1 {
|
||||||
margin-right: 10px;
|
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>
|
</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 { 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 { 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 {
|
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 });
|
return fail(400, { edit_entry: return_obj });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
id = toInt(id);
|
||||||
|
|
||||||
let current = locals.user.get_entry(id);
|
let current = locals.user.get_entry(id);
|
||||||
|
|
||||||
if (!current) {
|
if (!current) {
|
||||||
|
|
@ -167,4 +216,34 @@ export const actions = {
|
||||||
redirect(303, '/');
|
redirect(303, '/');
|
||||||
return { success: true };
|
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">
|
<script lang="ts">
|
||||||
|
import type { PageProps } from "./$types";
|
||||||
|
|
||||||
import { untrack } from "svelte";
|
import { untrack } from "svelte";
|
||||||
import { enhance } from "$app/forms";
|
import { enhance } from "$app/forms";
|
||||||
import { page } from "$app/state"
|
import { page } from "$app/state"
|
||||||
|
|
||||||
import type { RecordEntry } from "$lib/db_types"
|
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 type { RowState } from "./record_input_row.svelte"
|
||||||
import RecordInputRow 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" ];
|
const HEADERS: string[] = [ "Datum", "Wochentag", "Beginn", "Ende", "Dauer", "Anmerkung" ];
|
||||||
|
|
||||||
|
|
@ -64,7 +68,7 @@
|
||||||
editing = form.edit_entry
|
editing = form.edit_entry
|
||||||
} else if (page.url.searchParams.get("edit")) {
|
} else if (page.url.searchParams.get("edit")) {
|
||||||
let id = toInt(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;
|
editing = entry ?? null;
|
||||||
} else {
|
} else {
|
||||||
editing = null;
|
editing = null;
|
||||||
|
|
@ -102,77 +106,174 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<form id="form_new_entry" method="POST" action="?/new_entry" onsubmit={(e) => validateForm(e, new_state)} use:enhance></form>
|
<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_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>
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style:width="20ch">Datum</th>
|
<th></th>
|
||||||
<th style:width="25ch">Wochentag</th>
|
<th></th>
|
||||||
<th style:width="12ch">Beginn</th>
|
<th>Datum</th>
|
||||||
<th style:width="12ch">Ende</th>
|
<th>Wochentag</th>
|
||||||
<th style:width="12ch">Dauer</th>
|
<th>Beginn</th>
|
||||||
|
<th>Ende</th>
|
||||||
|
<th>Dauer</th>
|
||||||
<th>Anmerkung</th>
|
<th>Anmerkung</th>
|
||||||
<th style:width="12ch">Actions</th>
|
<th>Actions</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<RecordInputRow targetForm="form_new_entry" bind:states={new_state} enabled={editing == null}>
|
<tr style:border="none">
|
||||||
<button
|
<td style:border="none"></td>
|
||||||
type="submit"
|
<td style:border="none"></td>
|
||||||
form="form_new_entry"
|
<RecordInputRow targetForm="form_new_entry" bind:states={new_state} enabled={editing == null}>
|
||||||
disabled={editing != null}>
|
<button
|
||||||
+
|
type="submit"
|
||||||
</button>
|
form="form_new_entry"
|
||||||
</RecordInputRow>
|
disabled={editing != null}>
|
||||||
|
+
|
||||||
{#each data.records as entry}
|
</button>
|
||||||
{#if editing?.id != entry.id }
|
</RecordInputRow>
|
||||||
<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>
|
</tr>
|
||||||
{:else}
|
|
||||||
<RecordInputRow targetForm="form_edit_entry" bind:states={edit_state} enabled={true} >
|
{#each data.estimates as estimates_pair}
|
||||||
<input type="hidden" form="form_edit_entry" name="id" value={entry.id} />
|
{@const year = estimates_pair[0] }
|
||||||
<button
|
{@const mmonths = estimates_pair[1] }
|
||||||
type="submit"
|
{@const yentries = data.records.get(year)}
|
||||||
form="form_edit_entry">
|
|
||||||
Save
|
{#each mmonths as month_pair, mindex}
|
||||||
</button>
|
{@const month = month_pair[0] }
|
||||||
<form
|
{@const estimate = month_pair[1] }
|
||||||
id="form_edit_cancel"
|
{@const entries = yentries.get(month) ?? [ null ] }
|
||||||
method="GET"
|
|
||||||
onsubmit={(event) => {
|
{#each entries as entry, eindex}
|
||||||
event.preventDefault();
|
{@const record_sum = entries.reduce((acc, val) => acc + (val != null ? calculateDuration(val.start, val.end) : 0), 0)}
|
||||||
setEditing(null);
|
<!--<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}
|
||||||
<button type="submit">Cancel</button>
|
<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>
|
||||||
</form>
|
{/if}
|
||||||
</RecordInputRow>
|
{#if eindex == 0}
|
||||||
{/if}
|
<td class={["month", month % 3 == 2 ? "quarter" : ""]} rowspan={entries.length < 4 ? 4 : entries.length }>{MONTHS[month]}</td>
|
||||||
{/each}
|
{/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>
|
<tr></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
{#if data.records === undefined || data.records.length === 0}
|
{#if data.records === undefined || data.records.length === 0}
|
||||||
|
|
@ -198,28 +299,71 @@ table {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border: 1px solid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table caption {
|
tbody > :global(tr) {
|
||||||
font-size: 25px;
|
border-top: 1px solid black;
|
||||||
font-weight: bold;
|
border-bottom: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody > tr:nth-child(odd) {
|
tbody > :global(tr:has(> td.year):has(td.month)) {
|
||||||
background: gray;
|
border-top: 3px double black;
|
||||||
|
}
|
||||||
|
tbody > :global(tr:has(> td.month)) {
|
||||||
|
border-top: 1.5px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody > tr:nth-child(even) {
|
tbody .year {
|
||||||
background: lightgray;
|
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;
|
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 {
|
tfoot > tr {
|
||||||
border-top: 1px solid black;
|
border-top: 1px solid red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.td-no-elements {
|
.td-no-elements {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import type { PageServerLoad, Actions } from "./$types";
|
import type { PageServerLoad, Actions } from "./$types";
|
||||||
import { fail } from "@sveltejs/kit"
|
import { fail, redirect } from "@sveltejs/kit"
|
||||||
|
|
||||||
import { toInt } from "$lib/util"
|
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 }) => {
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
return {
|
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);
|
console.log(e);
|
||||||
return fail(403, { success: false, message: e.toString(), year: year, month: month });
|
return fail(403, { success: false, message: e.toString(), year: year, month: month });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
redirect(303, "dokumente")
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,91 @@
|
||||||
|
|
||||||
import { isoToLocalDate, padInt } from "$lib/util";
|
import { isoToLocalDate, padInt } from "$lib/util";
|
||||||
|
|
||||||
|
import Expander from "./expander.svelte";
|
||||||
|
|
||||||
let { data, form } : PageProps = $props();
|
let { data, form } : PageProps = $props();
|
||||||
|
|
||||||
|
$inspect(data);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1>Dokumente</h1>
|
<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>
|
<form method="POST" id="create_rec" action="?/create_record" use:enhance></form>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
|
|
@ -79,13 +156,12 @@
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
{/if}
|
{/if}
|
||||||
</table>
|
</table> -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -93,6 +169,41 @@ h1 {
|
||||||
font-weight: bold;
|
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 {
|
form {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -128,5 +239,5 @@ tfoot {
|
||||||
|
|
||||||
.td-no-elements {
|
.td-no-elements {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}*/
|
||||||
</style>
|
</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);
|
return weekday_of(date);
|
||||||
})
|
})
|
||||||
let inDuration: string = $state("");
|
let inDuration: string = $state("");
|
||||||
updateDuration();
|
|
||||||
|
$effect(() => {
|
||||||
|
states;
|
||||||
function updateDuration() {
|
|
||||||
if ((states?.start?.valid && states?.end?.valid)
|
if ((states?.start?.valid && states?.end?.valid)
|
||||||
&& (states?.start?.value != null && states?.end?.value != null)) {
|
&& (states?.start?.value != null && states?.end?.value != null)) {
|
||||||
let duration = calculateDuration(states.start.value, states.end.value);
|
let duration = calculateDuration(states.start.value, states.end.value);
|
||||||
inDuration = isNaN(duration) ? "" : duration.toFixed(2);
|
inDuration = isNaN(duration) ? "" : duration.toFixed(2);
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
|
|
||||||
function validateDate(element: HTMLInputElement) {
|
function validateDate(element: HTMLInputElement) {
|
||||||
|
|
||||||
|
|
@ -498,7 +496,10 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<tr>
|
<!--<tr>
|
||||||
|
{#each { length: prepend_td }, n}
|
||||||
|
<td></td>
|
||||||
|
{/each}-->
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
bind:this={dateInput}
|
bind:this={dateInput}
|
||||||
|
|
@ -545,7 +546,6 @@
|
||||||
(_) => {
|
(_) => {
|
||||||
states.start.valid = validateTime(startInput);
|
states.start.valid = validateTime(startInput);
|
||||||
states.start.value = startInput.value;
|
states.start.value = startInput.value;
|
||||||
updateDuration();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
|
|
@ -570,7 +570,6 @@
|
||||||
(_) => {
|
(_) => {
|
||||||
states.end.valid = validateTime(endInput);
|
states.end.valid = validateTime(endInput);
|
||||||
states.end.value = endInput.value;
|
states.end.value = endInput.value;
|
||||||
updateDuration();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
|
|
@ -590,11 +589,11 @@
|
||||||
disabled={!enabled}>
|
disabled={!enabled}>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td class="action">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
</tr>
|
<!--</tr>-->
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
|
|
@ -608,10 +607,10 @@ td input {
|
||||||
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
border: none;
|
border: 1px solid gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr td:last-of-type {
|
.action {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,41 @@
|
||||||
|
import type { PageServerLoad, Actions } from "./$types";
|
||||||
|
|
||||||
import { fail } from '@sveltejs/kit';
|
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 {
|
return {
|
||||||
estimates: locals.user.get_estimates()
|
estimates: estimates_grouped,
|
||||||
|
documents: documents_grouped,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,7 +55,7 @@ export const actions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const y = toInt(year);
|
const y = toInt(year);
|
||||||
const q = toInt(quart)
|
const q = toInt(quart);
|
||||||
const est_0 = toFloat(estimate_0);
|
const est_0 = toFloat(estimate_0);
|
||||||
const est_1 = toFloat(estimate_1);
|
const est_1 = toFloat(estimate_1);
|
||||||
const est_2 = toFloat(estimate_2);
|
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 });
|
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)
|
const res = locals.user.insert_estimate(y, q, est_0, est_1, est_2)
|
||||||
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
|
|
@ -41,4 +78,4 @@ export const actions = {
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
}
|
} satisfies Actions;
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,61 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { month_of } from "$lib/util";
|
import type { PageProps } from "./$types";
|
||||||
import { enhance } from "$app/forms";
|
|
||||||
|
|
||||||
interface Quarter {
|
import { enhance } from "$app/forms";
|
||||||
year: number;
|
import { MONTHS, toInt, toFloat, padInt } from "$lib/util";
|
||||||
quarter: number;
|
|
||||||
estimate_0: number;
|
let { form, data }: PageProps = $props();
|
||||||
estimate_1: number;
|
|
||||||
estimate_2: number;
|
$inspect(data);
|
||||||
}
|
|
||||||
interface Props {
|
let next = $state((() => {
|
||||||
form: any;
|
if (data.estimates.size == 0) {
|
||||||
data: {
|
return { year: (new Date()).getFullYear(), quarter: (new Date()).getMonth() / 3 + 1 };
|
||||||
estimates: Array<Quarter>;
|
}
|
||||||
|
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)
|
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)))
|
? 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;
|
: TODAY;
|
||||||
|
|
@ -30,25 +68,116 @@
|
||||||
|
|
||||||
function month_from_quart(quart: number, num: number) {
|
function month_from_quart(quart: number, num: number) {
|
||||||
return new Date(TODAY.getFullYear(), (quart - 1) * 4 + num);
|
return new Date(TODAY.getFullYear(), (quart - 1) * 4 + num);
|
||||||
}
|
}*/
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form id="form_add_quart" method="POST" action="?/add_quarter" use:enhance></form>
|
<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>
|
<h1>Stundenschätzung</h1>
|
||||||
<caption>Stundenschätzung</caption>
|
|
||||||
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style:width="20ch">Quartal</th>
|
<th style:width="5em"></th>
|
||||||
<th style:width="30ch">Monat</th>
|
<th style:width="10ch">Quartal</th>
|
||||||
<th style:width="30ch">Schätzung</th>
|
<th style:width="20ch">Monat</th>
|
||||||
<th style:width="12ch">Aktion</th>
|
<th style:width="10ch">Soll</th>
|
||||||
|
<th style:width="12ch" colspan="2">Aktion</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<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>
|
<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 rowspan="3">{add_quart}. Quartal {NEXT_QUART.getFullYear()}</td>
|
||||||
|
|
||||||
<td>{month_of(month_from_quart(add_quart, 0))}</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>
|
<td>{month_of(month_from_quart(add_quart, 2))}</td>
|
||||||
<!-- svelte-ignore a11y_positive_tabindex -->
|
<!-- svelte-ignore a11y_positive_tabindex -->
|
||||||
<td><input form="form_add_quart" type="text" tabindex="3" name="estimate_2" value={form?.data?.estimate_2}/></td>
|
<td><input form="form_add_quart" type="text" tabindex="3" name="estimate_2" value={form?.data?.estimate_2}/></td>
|
||||||
</tr>
|
</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}
|
{#if months.length > 0}
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan={rowspan}>
|
<td rowspan={rowspan}>
|
||||||
|
|
@ -100,19 +219,7 @@
|
||||||
<td>{month.estimate.toFixed(2)}</td>
|
<td>{month.estimate.toFixed(2)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/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>
|
<style>
|
||||||
|
|
||||||
|
|
@ -122,43 +229,67 @@ form {
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 50%;
|
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border: 1px solid;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
table caption {
|
.add {
|
||||||
font-size: 25px;
|
|
||||||
font-weight: bold;
|
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 {
|
.list {
|
||||||
text-align: center;
|
/*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) {
|
tbody > tr:has(> td.year):has(td.quarter) {
|
||||||
background: gray;
|
border-top: 3px black double;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody > tr:nth-child(even) {
|
tbody > tr:has(> td.quarter) {
|
||||||
background: lightgray;
|
border-top: 1px black solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
tfoot {
|
tbody .year {
|
||||||
border-top: 1px solid black;
|
vertical-align: middle;
|
||||||
}
|
writing-mode: sideways-lr;
|
||||||
|
text-orientation: mixed;
|
||||||
|
}
|
||||||
|
|
||||||
.td-no-elements {
|
tbody .estimate {
|
||||||
text-align: center;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: #BBBBBB;
|
background: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*body * {
|
/*body * {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue