import { Component, OnInit, signal } from "@angular/core"; import { RouterLink } from "@angular/router"; import { FormsModule } from "@angular/forms"; import { MATERIAL } from "../shared/material"; import { ApiService } from "../core/api.service"; import { Entry, Settings } from "../core/models"; @Component({ standalone: true, imports: [RouterLink, FormsModule, ...MATERIAL], template: ` Kalender {{ periodLabel() }} Monat Woche chevron_leftHeute chevron_right Ausbildung Alle @for (type of types(); track type) { {{ typeLabel(type) }} } Art Alle @for (art of arts(); track art) { {{ art }} } Zurücksetzen @if (loading()) { } @for (d of weekdays; track d) { {{ d }} } @for (day of days(); track day.key) { {{ day.date.getDate() }}add @for (e of forDay(day.key); track e.id) { {{ e.entry_art }}{{ e.hours }}h {{ e.minutes }}min } } `, styles: [ ` .calendar { overflow: hidden; min-width: 0; } .calendar-filters { margin-bottom:18px; } .calendar-filters mat-card-content { display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding-bottom:0; } .calendar-filters mat-form-field { min-width:220px; } .selected { background:#dce9ff!important; } .weekdays, .days { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); min-width: 0; } .weekdays { padding: 14px; background: #edf2fb; text-align: center; color: #59657a; } .day { min-height: 145px; min-width: 0; padding: 8px; border-top: 1px solid #e1e6ef; border-right: 1px solid #e1e6ef; overflow: hidden; } .day-head { display: flex; justify-content: space-between; align-items: center; } .day-head a { transform: scale(0.8); } .outside { opacity: 0.42; } .today { background: #eef5ff; } .today .day-head > span { background: #155bd7; color: white; border-radius: 50%; width: 28px; height: 28px; display: grid; place-items: center; } :host-context(body.dark) .today { background: #202b3a; box-shadow: inset 0 0 0 2px #4f8ff7; } :host-context(body.dark) .today .day-head > span { background: #4f8ff7; color: #07111f; font-weight: 800; } :host-context(body.dark) .today .day-head a { color: #a9c9ff; } :host-context(body.dark) .today .event { color: #f7f9fc; } :host-context(body.dark) .today .event span { opacity: 0.82; } .event { display: flex; flex-direction: column; width: 100%; min-width: 0; max-width: 100%; overflow: hidden; text-decoration: none; color: inherit; background: #dce9ff; border-left: 3px solid #2868d8; border-radius: 6px; padding: 5px 7px; margin: 4px 0; font-size: 12px; } .event span { opacity: 0.75; margin-top: 2px; } .event strong, .event span { display: block; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .event.type-propaedeutikum { background:rgba(13,110,253,.14);border-left-color:#0d6efd; } .event.type-fachspezifikum { background:rgba(25,135,84,.14);border-left-color:#198754; } .event.type-mediation { background:rgba(253,126,20,.16);border-left-color:#fd7e14; } .event.type-psychotherapie { background:rgba(124,58,237,.14);border-left-color:#7c3aed; } .event.type-lsb { background:rgba(219,39,119,.14);border-left-color:#db2777; } .event.type-coaching { background:rgba(8,145,178,.14);border-left-color:#0891b2; } .event.type-supervision { background:rgba(202,138,4,.14);border-left-color:#ca8a04; } .event.type-sonstige { background:rgba(100,116,139,.14);border-left-color:#64748b; } @media (max-width: 700px) { .calendar-filters mat-card-content { gap: 8px; padding: 12px 12px 0; } .calendar-filters mat-form-field { width: 100%; min-width: 0; } .weekdays { padding: 9px 0; } .weekdays strong { font-size: 0; } .weekdays strong:first-letter { font-size: 13px; } .day { min-height: 92px; padding: 4px 3px; } .day-head a { width: 30px; height: 30px; padding: 3px; transform: none; } .day-head mat-icon { width: 20px; height: 20px; font-size: 20px; line-height: 20px; } .event { padding: 4px; border-left-width: 2px; font-size: 11px; } .event span { display: none; } } @media (max-width: 480px) { .weekdays strong:first-letter { font-size: 11px; } .day { min-height: 78px; padding: 3px 2px; } .day-head { font-size: 12px; } .day-head a { width: 24px; height: 24px; padding: 2px; } .day-head mat-icon { width: 18px; height: 18px; font-size: 18px; line-height: 18px; } .today .day-head > span { width: 24px; height: 24px; } .event { margin: 3px 0; padding: 3px 2px; border-radius: 4px; font-size: 10px; } } `, ], }) export class CalendarComponent implements OnInit { readonly month = signal( new Date(new Date().getFullYear(), new Date().getMonth(), 1), ); readonly weekAnchor = signal(new Date()); readonly mode = signal<"month"|"week">("month"); readonly entries = signal([]); readonly loading = signal(false); readonly weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; readonly types = signal([]); readonly arts = signal([]); readonly typeLabels = signal>({}); typeFilter = ""; artFilter = ""; readonly todayKey = this.key(new Date()); constructor(private api: ApiService) {} ngOnInit() { this.api.settings().subscribe((settings: Settings) => { this.types.set(settings.praktikums_typen); this.arts.set(settings.entry_arten); this.typeLabels.set(settings.praktikums_typ_labels); }); this.load(); } days() { if(this.mode()==="week"){ const start=new Date(this.weekAnchor()); start.setDate(start.getDate()-((start.getDay()+6)%7)); return Array.from({length:7},(_,i)=>{const date=new Date(start);date.setDate(start.getDate()+i);return{date,key:this.key(date),current:true};}); } const first = this.month(), start = new Date(first); const offset = (start.getDay() + 6) % 7; start.setDate(start.getDate() - offset); return Array.from({ length: 42 }, (_, i) => { const date = new Date(start); date.setDate(start.getDate() + i); return { date, key: this.key(date), current: date.getMonth() === first.getMonth(), }; }); } forDay(key: string) { return this.entries().filter((e) => e.date === key && (!this.typeFilter || e.praktikums_typ === this.typeFilter) && (!this.artFilter || e.entry_art === this.artFilter)); } clearFilters(){this.typeFilter="";this.artFilter="";} typeLabel(type:string){return this.typeLabels()[type]||type.charAt(0).toUpperCase()+type.slice(1);} typeClass(value:string){ const normalized=value?.toLocaleLowerCase("de-AT").normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""); return `type-${normalized === "propadeutikum" ? "propaedeutikum" : normalized || "unknown"}`; } move(n: number) { if(this.mode()==="week"){const d=new Date(this.weekAnchor());d.setDate(d.getDate()+n*7);this.weekAnchor.set(d);this.load();return;} const d = this.month(); this.month.set(new Date(d.getFullYear(), d.getMonth() + n, 1)); this.load(); } today() { const d = new Date(); this.month.set(new Date(d.getFullYear(), d.getMonth(), 1)); this.weekAnchor.set(d); this.load(); } setMode(mode:"month"|"week"){this.mode.set(mode);this.load();} periodLabel(){if(this.mode()==="month")return new Intl.DateTimeFormat("de-AT",{month:"long",year:"numeric"}).format(this.month());const ds=this.days();return `${new Intl.DateTimeFormat("de-AT",{day:"2-digit",month:"2-digit"}).format(ds[0].date)} – ${new Intl.DateTimeFormat("de-AT",{day:"2-digit",month:"2-digit",year:"numeric"}).format(ds[6].date)}`;} load() { const ds = this.days(); this.loading.set(true); this.api.calendar(ds[0].key, ds[ds.length-1].key).subscribe({ next: (e) => { this.entries.set(e); this.loading.set(false); }, error: () => this.loading.set(false), }); } private key(d: Date) { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } }
{{ periodLabel() }}