You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
341 lines
11 KiB
341 lines
11 KiB
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: `<section class="page">
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>Kalender</h1>
|
|
<p class="muted">{{ periodLabel() }}</p>
|
|
</div>
|
|
<div class="actions">
|
|
<button mat-stroked-button [class.selected]="mode()==='month'" (click)="setMode('month')">Monat</button>
|
|
<button mat-stroked-button [class.selected]="mode()==='week'" (click)="setMode('week')">Woche</button>
|
|
<button mat-icon-button (click)="move(-1)">
|
|
<mat-icon>chevron_left</mat-icon></button
|
|
><button mat-stroked-button (click)="today()">Heute</button
|
|
><button mat-icon-button (click)="move(1)">
|
|
<mat-icon>chevron_right</mat-icon>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<mat-card class="calendar-filters"><mat-card-content>
|
|
<mat-form-field appearance="outline"><mat-label>Ausbildung</mat-label><mat-select [(ngModel)]="typeFilter">
|
|
<mat-option value="">Alle</mat-option>
|
|
@for (type of types(); track type) { <mat-option [value]="type">{{ typeLabel(type) }}</mat-option> }
|
|
</mat-select></mat-form-field>
|
|
<mat-form-field appearance="outline"><mat-label>Art</mat-label><mat-select [(ngModel)]="artFilter">
|
|
<mat-option value="">Alle</mat-option>
|
|
@for (art of arts(); track art) { <mat-option [value]="art">{{ art }}</mat-option> }
|
|
</mat-select></mat-form-field>
|
|
<button mat-button (click)="clearFilters()">Zurücksetzen</button>
|
|
</mat-card-content></mat-card>
|
|
@if (loading()) {
|
|
<mat-progress-bar mode="indeterminate" />
|
|
}
|
|
<mat-card class="calendar"
|
|
><div class="weekdays" aria-hidden="true">
|
|
@for (d of weekdays; track d) {
|
|
<strong>{{ d }}</strong>
|
|
}
|
|
</div>
|
|
<div class="days">
|
|
@for (day of days(); track day.key) {
|
|
<div
|
|
class="day"
|
|
[class.outside]="!day.current"
|
|
[class.today]="day.key === todayKey"
|
|
>
|
|
<div class="day-head">
|
|
<span>{{ day.date.getDate() }}</span
|
|
><a
|
|
mat-icon-button
|
|
[routerLink]="['/entries/new']"
|
|
[queryParams]="{ date: day.key }"
|
|
><mat-icon>add</mat-icon></a
|
|
>
|
|
</div>
|
|
@for (e of forDay(day.key); track e.id) {
|
|
<a class="event" [class]="'event ' + typeClass(e.praktikums_typ)" [routerLink]="['/entries', e.id, 'edit']" [attr.aria-label]="e.entry_art + ', ' + e.hours + ' Stunden ' + e.minutes + ' Minuten'"
|
|
><strong>{{ e.entry_art }}</strong
|
|
><span>{{ e.hours }}h {{ e.minutes }}min</span></a
|
|
>
|
|
}
|
|
</div>
|
|
}
|
|
</div></mat-card
|
|
>
|
|
</section>`,
|
|
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<Entry[]>([]);
|
|
readonly loading = signal(false);
|
|
readonly weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
|
readonly types = signal<string[]>([]);
|
|
readonly arts = signal<string[]>([]);
|
|
readonly typeLabels = signal<Record<string,string>>({});
|
|
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")}`;
|
|
}
|
|
}
|