Files
infoscreen/dashboard/src/apiAcademicPeriods.ts
Olaf b5f5f30005 feat: period-scoped holiday management, archive lifecycle, and docs/release sync
- add period-scoped holiday architecture end-to-end
	- model: scope `SchoolHoliday` to `academic_period_id`
	- migrations: add holiday-period scoping, academic-period archive lifecycle, and merge migration head
	- API: extend holidays with manual CRUD, period validation, duplicate prevention, and overlap merge/conflict handling
	- recurrence: regenerate holiday exceptions using period-scoped holiday sets

- improve frontend settings and holiday workflows
	- bind holiday import/list/manual CRUD to selected academic period
	- show detailed import outcomes (inserted/updated/merged/skipped/conflicts)
	- fix file-picker UX (visible selected filename)
	- align settings controls/dialogs with defined frontend design rules
	- scope appointments/dashboard holiday loading to active period
	- add shared date formatting utility

- strengthen academic period lifecycle handling
	- add archive/restore/delete flow and backend validations/blocker checks
	- extend API client support for lifecycle operations

- release/docs updates and cleanup
	- bump user-facing version to `2026.1.0-alpha.15` with new changelog entry
	- add tech changelog entry for alpha.15 backend changes
	- refactor README to concise index and archive historical implementation docs
	- fix Copilot instruction link diagnostics via local `.github` design-rules reference
2026-03-31 12:25:55 +00:00

140 lines
4.2 KiB
TypeScript

export type AcademicPeriod = {
id: number;
name: string;
displayName?: string | null;
startDate: string; // YYYY-MM-DD
endDate: string; // YYYY-MM-DD
periodType: 'schuljahr' | 'semester' | 'trimester';
isActive: boolean;
isArchived: boolean;
archivedAt?: string | null;
archivedBy?: number | null;
createdAt?: string;
updatedAt?: string;
};
export type PeriodUsage = {
linked_events: number;
has_active_recurrence: boolean;
blockers: string[];
};
async function api<T>(url: string, init?: RequestInit): Promise<T> {
const res = await fetch(url, { credentials: 'include', ...init });
if (!res.ok) {
const text = await res.text();
try {
const err = JSON.parse(text);
throw new Error(err.error || `HTTP ${res.status}`);
} catch {
throw new Error(`HTTP ${res.status}: ${text}`);
}
}
return res.json();
}
export async function getAcademicPeriodForDate(date: Date): Promise<AcademicPeriod | null> {
const iso = date.toISOString().slice(0, 10);
const { period } = await api<{ period: AcademicPeriod | null }>(
`/api/academic_periods/for_date?date=${iso}`
);
return period ?? null;
}
export async function listAcademicPeriods(options?: {
includeArchived?: boolean;
archivedOnly?: boolean;
}): Promise<AcademicPeriod[]> {
const params = new URLSearchParams();
if (options?.includeArchived) {
params.set('includeArchived', '1');
}
if (options?.archivedOnly) {
params.set('archivedOnly', '1');
}
const query = params.toString();
const { periods } = await api<{ periods: AcademicPeriod[] }>(
`/api/academic_periods${query ? `?${query}` : ''}`
);
return Array.isArray(periods) ? periods : [];
}
export async function getAcademicPeriod(id: number): Promise<AcademicPeriod> {
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}`);
return period;
}
export async function getActiveAcademicPeriod(): Promise<AcademicPeriod | null> {
const { period } = await api<{ period: AcademicPeriod | null }>(`/api/academic_periods/active`);
return period ?? null;
}
export async function createAcademicPeriod(payload: {
name: string;
displayName?: string;
startDate: string;
endDate: string;
periodType: 'schuljahr' | 'semester' | 'trimester';
}): Promise<AcademicPeriod> {
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return period;
}
export async function updateAcademicPeriod(
id: number,
payload: Partial<{
name: string;
displayName: string | null;
startDate: string;
endDate: string;
periodType: 'schuljahr' | 'semester' | 'trimester';
}>
): Promise<AcademicPeriod> {
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return period;
}
export async function setActiveAcademicPeriod(id: number): Promise<AcademicPeriod> {
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}/activate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
return period;
}
export async function archiveAcademicPeriod(id: number): Promise<AcademicPeriod> {
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}/archive`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
return period;
}
export async function restoreAcademicPeriod(id: number): Promise<AcademicPeriod> {
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}/restore`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
return period;
}
export async function getAcademicPeriodUsage(id: number): Promise<PeriodUsage> {
const { usage } = await api<{ usage: PeriodUsage }>(`/api/academic_periods/${id}/usage`);
return usage;
}
export async function deleteAcademicPeriod(id: number): Promise<void> {
await api(`/api/academic_periods/${id}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
});
}