import { Component, OnInit, signal } from "@angular/core"; import { DatePipe, DecimalPipe } from "@angular/common"; import { RouterLink } from "@angular/router"; import { FormsModule } from "@angular/forms"; import { MATERIAL } from "../shared/material"; import { ApiService } from "../core/api.service"; import { Dashboard, Entry } from "../core/models"; @Component({ standalone: true, imports: [RouterLink, FormsModule, DatePipe, DecimalPipe, ...MATERIAL], template: `

Einträge

Stunden, Kosten und Fahrten verwalten

add Neuer Eintrag
@if (running(); as timer) {
timerTimer läuft{{ elapsed(timer) }}{{ timer.praktikums_typ }} · {{ timer.entry_art }}
30 Minuten Pause abziehen } @else {
timerZeiterfassungTimer starten
Ausbildung@for(t of types;track t){{{t}}} Art@for(a of timerArts();track a){{{a}}} }
SuchesearchAusbildungAlle @for (t of types; track t) { {{ t }} } Von JahrAlle@for(y of years();track y){{{y}}}Bis JahrAlle@for(y of years();track y){{{y}}} @if (loading()) { }
Datum {{ e.date | date: "dd.MM.yyyy" }} Ausbildung {{ e.praktikums_typ }} Art{{e.entry_art}}Zeit {{ e.hours }} h {{ e.minutes }} min Details {{ e.beschreibung || "–" }} Kilometer{{e.distance_km || 0 | number:"1.0-2"}} kmPauschale{{e.kilometer_pauschale || 0 | number:"1.2-2"}} €Kosten{{e.kosten || 0 | number:"1.2-2"}} €Fortbildung{{e.zaehlt_als_fortbildung ? "check_circle" : "remove"}}
edit
@if (!loading() && !entries().length) {
event_busy

Noch keine passenden Einträge vorhanden.

}
Angezeigt: {{visibleEntries().length}} · Gesamt: {{entries().length}}
`, styles: [ ` .filters { margin-bottom: 20px; } .entries-page { max-width: 1720px; } .timer-card { margin-bottom: 20px; } .timer-card mat-card-content { display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap; } .timer-card mat-card-content > div { display:grid;grid-template-columns:auto 1fr;gap:3px 10px;margin-right:auto; } .timer-card mat-card-content > div > mat-icon { grid-row:1/4; align-self:center; display:grid; place-items:center; width:40px; height:40px; border-radius:12px; color:var(--app-primary); background:color-mix(in srgb,var(--app-primary) 13%,transparent); } .timer-card button mat-icon { color:inherit; } .timer-card mat-card-content > button, .filters mat-card-content > button { align-self:flex-start; min-height:48px; margin-top:4px; } .timer-start mat-icon, .timer-stop mat-icon { opacity:1; } .timer-card strong { font-size:1.25rem; } .filters mat-card-content { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; } .filters mat-form-field { min-width: 220px; } .table-card { padding: 0; overflow: hidden; } .entry-count { padding:12px 18px;color:#687287;border-top:1px solid rgba(120,130,145,.2);font-size:.85rem; } .training-state { color:var(--app-muted);vertical-align:middle; } .training-state.is-active { color:#198754; } .mat-column-actions { width:108px;min-width:108px;white-space:nowrap; } .mat-column-time { width:128px;min-width:128px;white-space:nowrap; } .entry-actions { display:flex;align-items:center;justify-content:flex-end;gap:8px;white-space:nowrap; } .entry-actions .mat-mdc-icon-button { flex:0 0 40px;width:40px;height:40px;padding:8px;border-radius:10px; } .edit-action { color:#0d6efd;background:rgba(13,110,253,.11); } .delete-action { color:#c62828;background:rgba(198,40,40,.11); } .edit-action:hover { background:rgba(13,110,253,.19); } .delete-action:hover { background:rgba(198,40,40,.19); } td strong, td small { display: block; } td small { color: #687287; margin-top: 4px; } th { font-weight: 700; } tr.mat-mdc-row > td:first-child { border-left: 4px solid transparent; } tr.mat-mdc-row.type-propaedeutikum:nth-of-type(odd) > td { background-color: rgba(13, 110, 253, 0.055); } tr.mat-mdc-row.type-propaedeutikum:nth-of-type(even) > td { background-color: rgba(13, 110, 253, 0.115); } tr.mat-mdc-row.type-fachspezifikum:nth-of-type(odd) > td { background-color: rgba(25, 135, 84, 0.055); } tr.mat-mdc-row.type-fachspezifikum:nth-of-type(even) > td { background-color: rgba(25, 135, 84, 0.115); } tr.mat-mdc-row.type-mediation:nth-of-type(odd) > td { background-color: rgba(253, 126, 20, 0.065); } tr.mat-mdc-row.type-mediation:nth-of-type(even) > td { background-color: rgba(253, 126, 20, 0.135); } tr.mat-mdc-row.type-propaedeutikum:hover > td { background-color: rgba(13, 110, 253, 0.17); } tr.mat-mdc-row.type-fachspezifikum:hover > td { background-color: rgba(25, 135, 84, 0.17); } tr.mat-mdc-row.type-mediation:hover > td { background-color: rgba(253, 126, 20, 0.19); } tr.mat-mdc-row.type-propaedeutikum > td:first-child { border-left-color: #0d6efd; } tr.mat-mdc-row.type-fachspezifikum > td:first-child { border-left-color: #198754; } tr.mat-mdc-row.type-mediation > td:first-child { border-left-color: #fd7e14; } tr.mat-mdc-row.entry-today { font-weight: 700; } tr.mat-mdc-row.entry-today > td { box-shadow: inset 0 3px 0 #7c3aed, inset 0 -3px 0 #7c3aed; } tr.mat-mdc-row.entry-today > td:first-child { border-left:6px solid #7c3aed; } tr.mat-mdc-row.entry-today > td:last-child { border-right:3px solid #7c3aed; } :host-context(body.dark) tr.mat-mdc-row.type-propaedeutikum:nth-of-type(odd) > td { background-color: rgba(62, 139, 255, 0.12); } :host-context(body.dark) tr.mat-mdc-row.type-propaedeutikum:nth-of-type(even) > td { background-color: rgba(62, 139, 255, 0.18); } :host-context(body.dark) tr.mat-mdc-row.type-fachspezifikum:nth-of-type(odd) > td { background-color: rgba(53, 184, 116, 0.12); } :host-context(body.dark) tr.mat-mdc-row.type-fachspezifikum:nth-of-type(even) > td { background-color: rgba(53, 184, 116, 0.18); } :host-context(body.dark) tr.mat-mdc-row.type-mediation:nth-of-type(odd) > td { background-color: rgba(255, 143, 51, 0.13); } :host-context(body.dark) tr.mat-mdc-row.type-mediation:nth-of-type(even) > td { background-color: rgba(255, 143, 51, 0.2); } :host-context(body.dark) tr.mat-mdc-row.type-propaedeutikum:hover > td { background-color: rgba(62, 139, 255, 0.25); } :host-context(body.dark) tr.mat-mdc-row.type-fachspezifikum:hover > td { background-color: rgba(53, 184, 116, 0.25); } :host-context(body.dark) tr.mat-mdc-row.type-mediation:hover > td { background-color: rgba(255, 143, 51, 0.27); } :host-context(body.dark) tr.mat-mdc-row.entry-today > td { box-shadow: inset 0 3px 0 #b794f4, inset 0 -3px 0 #b794f4; } :host-context(body.dark) tr.mat-mdc-row.entry-today > td:first-child { border-left-color:#b794f4; } :host-context(body.dark) tr.mat-mdc-row.entry-today > td:last-child { border-right-color:#b794f4; } :host-context(body.dark) .edit-action { color:#8bb7ff;background:rgba(62,139,255,.2); } :host-context(body.dark) .delete-action { color:#ff9b9b;background:rgba(255,90,90,.18); } @media (max-width: 700px) { .filters mat-form-field { width: 100%; } .mat-column-details { display: none; } } `, ], }) export class EntriesComponent implements OnInit { readonly entries = signal([]); readonly loading = signal(false); readonly columns = ["date", "time", "type", "art", "details", "distance", "allowance", "cost", "training", "actions"]; readonly types = ["propädeutikum", "fachspezifikum", "mediation"]; readonly arts: Record = {propädeutikum:["Praktikum","Selbsterfahrung","Supervision","Fortbildung","Semesterkosten"],fachspezifikum:["Praktikum","Selbsterfahrung","Supervision","Fortbildung","Semesterkosten","Gruppenselbsterfahrung","Theorie/Methodikseminare","Peergruppensupervision","Einzel-/Kleingruppensupervision","Eigenständige Tätigkeit"],mediation:["Präsenzmodul","Selbsterfahrung","Supervision","Peergruppenarbeit","Fallarbeit","Praxisseminare","Literatur- und Selbststudium","Fortbildung","Semesterkosten"]}; readonly running=signal(null); readonly clock=signal(Date.now()); timerType="propädeutikum"; timerArt="Praktikum"; lunchBreak=false; search = ""; typ = ""; minYear: number | "" = ""; maxYear: number | "" = ""; constructor(private api: ApiService) {} ngOnInit() { this.load(); setInterval(()=>this.clock.set(Date.now()),30_000); } load() { this.loading.set(true); this.api.entries({ search: this.search, typ: this.typ }).subscribe({ next: (v) => { this.entries.set(v); this.loading.set(false); }, error: () => this.loading.set(false), }); this.api.dashboard().subscribe((d:Dashboard)=>this.running.set(d.running_entry)); } timerArts(){const available=this.arts[this.timerType]||[];if(!available.includes(this.timerArt))this.timerArt=available[0];return available;} startTimer(){this.api.startTimer(this.timerType,this.timerArt).subscribe(e=>{this.running.set(e);this.load();});} stopTimer(entry:Entry){this.api.stopTimer(entry.id,this.lunchBreak).subscribe(()=>{this.running.set(null);this.lunchBreak=false;this.load();});} elapsed(entry:Entry){this.clock();if(!entry.start_time)return "0 h 0 min";const minutes=Math.max(0,Math.floor((Date.now()-new Date(entry.start_time).getTime())/60000));return `${Math.floor(minutes/60)} h ${minutes%60} min`;} reset() { this.search = ""; this.typ = ""; this.minYear = ""; this.maxYear = ""; this.load(); } years(){return [...new Set(this.entries().map(e=>Number(e.date.slice(0,4))))].sort((a,b)=>b-a);} visibleEntries(){return this.entries().filter(e=>{const year=Number(e.date.slice(0,4));return(!this.minYear||year>=this.minYear)&&(!this.maxYear||year<=this.maxYear);});} entryRowClass(entry: Entry): string { const normalized = entry.praktikums_typ ?.toLocaleLowerCase("de-AT") .normalize("NFD") .replace(/[\u0300-\u036f]/g, ""); const type = normalized === "propadeutikum" ? "propaedeutikum" : normalized?.replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); const classes = [`type-${type || "unknown"}`]; const now = new Date(); const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; if (entry.date === today) { classes.push("entry-today"); } return classes.join(" "); } remove(e: Entry) { if (confirm(`Eintrag vom ${e.date} wirklich löschen?`)) this.api.deleteEntry(e.id).subscribe(() => this.load()); } download() { this.api.exportCsv().subscribe((blob) => { const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = `eintraege-${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(a.href); }); } }