- 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
140 lines
4.2 KiB
TypeScript
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' },
|
|
});
|
|
}
|