feat: 2026.1.0-alpha.16 – dashboard banner refactor, period auto-activation, text & docs
Dashboard (dashboard/src/dashboard.tsx, settings.tsx, apiAcademicPeriods.ts): - Refactor loadHolidayStatus to useCallback with stable empty-deps reference; removes location.pathname dependency that caused overlapping API calls at mount and left the banner unresolved via request-sequence cancellation - Add key prop derived from severity:text to Syncfusion MessageComponent to force remount on state change, fixing stale banner that ignored React prop/children updates - Correct German transliterated text to proper Umlauts throughout visible UI strings (fuer -> für, oe -> ö, ae -> ä etc. across dashboard and settings views) Backend (server/init_academic_periods.py): - Refactor to idempotent two-phase flow: seed default periods only when table is empty; on every run activate exactly the non-archived period covering date.today() - Enforces single-active invariant by deactivating all periods before promoting match - Emits explicit warning when no period covers current date instead of doing nothing Deployment (docker-compose.prod.yml): - Add init_academic_periods.py to server startup chain after migrations and defaults; eliminates manual post-deploy step to set an active academic period Release docs: - program-info.json: bump to 2026.1.0-alpha.16; fix JSON parse error caused by typographic curly quotes in the new changelog entry - TECH-CHANGELOG.md: detailed alpha.16 section with root-cause motivation for both dashboard refactoring decisions (unstable callback ref + Syncfusion stale render) - DEV-CHANGELOG.md: document dashboard refactor, Syncfusion key fix, Umlaut changes, and program-info JSON regression and fix - README.md: add Latest Release Highlights section for alpha.16 - .github/copilot-instructions.md: sync file map, prod bootstrap note, backend and frontend pattern additions for academic period init and Syncfusion remount pattern
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"appName": "Infoscreen-Management",
|
||||
"version": "2026.1.0-alpha.15",
|
||||
"version": "2026.1.0-alpha.16",
|
||||
"copyright": "© 2026 Third-Age-Applications",
|
||||
"supportContact": "support@third-age-applications.com",
|
||||
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
|
||||
@@ -30,6 +30,16 @@
|
||||
"commitId": "9f2ae8b44c3a"
|
||||
},
|
||||
"changelog": [
|
||||
{
|
||||
"version": "2026.1.0-alpha.16",
|
||||
"date": "2026-04-02",
|
||||
"changes": [
|
||||
"✅ Dashboard: Der Ferienstatus-Banner zeigt die aktive akademische Periode jetzt zuverlässig nach Hard-Refresh und beim Wechsel zwischen Dashboard und Einstellungen.",
|
||||
"🧭 Navigation: Der Link vom Ferienstatus-Banner zu den Einstellungen bleibt stabil und funktioniert konsistent für Admin-Rollen.",
|
||||
"🚀 Deployment: Akademische Perioden werden nach Initialisierung automatisch für das aktuelle Datum aktiviert (kein manueller Aktivierungsschritt direkt nach Rollout mehr nötig).",
|
||||
"🔤 Sprache: Mehrere deutsche UI-Texte im Dashboard wurden auf korrekte Umlaute umgestellt (zum Beispiel für, prüfen, Vorfälle und Ausfälle)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2026.1.0-alpha.15",
|
||||
"date": "2026-03-31",
|
||||
|
||||
@@ -19,8 +19,25 @@ export type PeriodUsage = {
|
||||
blockers: string[];
|
||||
};
|
||||
|
||||
function normalizeAcademicPeriod(period: any): AcademicPeriod {
|
||||
return {
|
||||
id: Number(period.id),
|
||||
name: period.name,
|
||||
displayName: period.displayName ?? period.display_name ?? null,
|
||||
startDate: period.startDate ?? period.start_date,
|
||||
endDate: period.endDate ?? period.end_date,
|
||||
periodType: period.periodType ?? period.period_type,
|
||||
isActive: Boolean(period.isActive ?? period.is_active),
|
||||
isArchived: Boolean(period.isArchived ?? period.is_archived),
|
||||
archivedAt: period.archivedAt ?? period.archived_at ?? null,
|
||||
archivedBy: period.archivedBy ?? period.archived_by ?? null,
|
||||
createdAt: period.createdAt ?? period.created_at,
|
||||
updatedAt: period.updatedAt ?? period.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
async function api<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(url, { credentials: 'include', ...init });
|
||||
const res = await fetch(url, { credentials: 'include', cache: 'no-store', ...init });
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
try {
|
||||
@@ -35,10 +52,10 @@ async function api<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
|
||||
export async function getAcademicPeriodForDate(date: Date): Promise<AcademicPeriod | null> {
|
||||
const iso = date.toISOString().slice(0, 10);
|
||||
const { period } = await api<{ period: AcademicPeriod | null }>(
|
||||
const { period } = await api<{ period: any | null }>(
|
||||
`/api/academic_periods/for_date?date=${iso}`
|
||||
);
|
||||
return period ?? null;
|
||||
return period ? normalizeAcademicPeriod(period) : null;
|
||||
}
|
||||
|
||||
export async function listAcademicPeriods(options?: {
|
||||
@@ -53,20 +70,20 @@ export async function listAcademicPeriods(options?: {
|
||||
params.set('archivedOnly', '1');
|
||||
}
|
||||
const query = params.toString();
|
||||
const { periods } = await api<{ periods: AcademicPeriod[] }>(
|
||||
const { periods } = await api<{ periods: any[] }>(
|
||||
`/api/academic_periods${query ? `?${query}` : ''}`
|
||||
);
|
||||
return Array.isArray(periods) ? periods : [];
|
||||
return Array.isArray(periods) ? periods.map(normalizeAcademicPeriod) : [];
|
||||
}
|
||||
|
||||
export async function getAcademicPeriod(id: number): Promise<AcademicPeriod> {
|
||||
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}`);
|
||||
return period;
|
||||
const { period } = await api<{ period: any }>(`/api/academic_periods/${id}`);
|
||||
return normalizeAcademicPeriod(period);
|
||||
}
|
||||
|
||||
export async function getActiveAcademicPeriod(): Promise<AcademicPeriod | null> {
|
||||
const { period } = await api<{ period: AcademicPeriod | null }>(`/api/academic_periods/active`);
|
||||
return period ?? null;
|
||||
const { period } = await api<{ period: any | null }>(`/api/academic_periods/active`);
|
||||
return period ? normalizeAcademicPeriod(period) : null;
|
||||
}
|
||||
|
||||
export async function createAcademicPeriod(payload: {
|
||||
@@ -76,12 +93,12 @@ export async function createAcademicPeriod(payload: {
|
||||
endDate: string;
|
||||
periodType: 'schuljahr' | 'semester' | 'trimester';
|
||||
}): Promise<AcademicPeriod> {
|
||||
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods`, {
|
||||
const { period } = await api<{ period: any }>(`/api/academic_periods`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return period;
|
||||
return normalizeAcademicPeriod(period);
|
||||
}
|
||||
|
||||
export async function updateAcademicPeriod(
|
||||
@@ -94,36 +111,36 @@ export async function updateAcademicPeriod(
|
||||
periodType: 'schuljahr' | 'semester' | 'trimester';
|
||||
}>
|
||||
): Promise<AcademicPeriod> {
|
||||
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}`, {
|
||||
const { period } = await api<{ period: any }>(`/api/academic_periods/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return period;
|
||||
return normalizeAcademicPeriod(period);
|
||||
}
|
||||
|
||||
export async function setActiveAcademicPeriod(id: number): Promise<AcademicPeriod> {
|
||||
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}/activate`, {
|
||||
const { period } = await api<{ period: any }>(`/api/academic_periods/${id}/activate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
return period;
|
||||
return normalizeAcademicPeriod(period);
|
||||
}
|
||||
|
||||
export async function archiveAcademicPeriod(id: number): Promise<AcademicPeriod> {
|
||||
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}/archive`, {
|
||||
const { period } = await api<{ period: any }>(`/api/academic_periods/${id}/archive`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
return period;
|
||||
return normalizeAcademicPeriod(period);
|
||||
}
|
||||
|
||||
export async function restoreAcademicPeriod(id: number): Promise<AcademicPeriod> {
|
||||
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}/restore`, {
|
||||
const { period } = await api<{ period: any }>(`/api/academic_periods/${id}/restore`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
return period;
|
||||
return normalizeAcademicPeriod(period);
|
||||
}
|
||||
|
||||
export async function getAcademicPeriodUsage(id: number): Promise<PeriodUsage> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,12 +21,14 @@ import {
|
||||
type PeriodUsage
|
||||
} from './apiAcademicPeriods';
|
||||
import { formatIsoDateForDisplay } from './dateFormatting';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
// Minimal event type for Syncfusion Tab 'selected' callback
|
||||
type TabSelectedEvent = { selectedIndex?: number };
|
||||
|
||||
const Einstellungen: React.FC = () => {
|
||||
const location = useLocation();
|
||||
|
||||
// Presentation settings state
|
||||
const [presentationInterval, setPresentationInterval] = React.useState(10);
|
||||
const [presentationPageProgress, setPresentationPageProgress] = React.useState(true);
|
||||
@@ -670,6 +672,8 @@ const Einstellungen: React.FC = () => {
|
||||
const isAdmin = !!(user && ['admin', 'superadmin'].includes(user.role));
|
||||
const isSuperadmin = !!(user && user.role === 'superadmin');
|
||||
|
||||
const [rootTabIndex, setRootTabIndex] = React.useState(0);
|
||||
|
||||
// Preserve selected nested-tab indices to avoid resets on parent re-render
|
||||
const [academicTabIndex, setAcademicTabIndex] = React.useState(0);
|
||||
const [displayTabIndex, setDisplayTabIndex] = React.useState(0);
|
||||
@@ -678,6 +682,22 @@ const Einstellungen: React.FC = () => {
|
||||
const [usersTabIndex, setUsersTabIndex] = React.useState(0);
|
||||
const [systemTabIndex, setSystemTabIndex] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const focus = params.get('focus');
|
||||
|
||||
if (focus === 'holidays') {
|
||||
setRootTabIndex(0);
|
||||
setAcademicTabIndex(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (focus === 'academic-periods') {
|
||||
setRootTabIndex(0);
|
||||
setAcademicTabIndex(0);
|
||||
}
|
||||
}, [location.search]);
|
||||
|
||||
// ---------- Leaf content functions (second-level tabs) ----------
|
||||
// Academic Calendar
|
||||
// (Old separate Import/List tab contents removed in favor of combined tab)
|
||||
@@ -1695,7 +1715,11 @@ const Einstellungen: React.FC = () => {
|
||||
|
||||
<h2 style={{ marginBottom: 20, fontSize: '24px', fontWeight: 600 }}>Einstellungen</h2>
|
||||
|
||||
<TabComponent heightAdjustMode="Auto">
|
||||
<TabComponent
|
||||
heightAdjustMode="Auto"
|
||||
selectedItem={rootTabIndex}
|
||||
selected={(e: TabSelectedEvent) => setRootTabIndex(e.selectedIndex ?? 0)}
|
||||
>
|
||||
<TabItemsDirective>
|
||||
<TabItemDirective header={{ text: '📅 Akademischer Kalender' }} content={AcademicCalendarTabs} />
|
||||
{isAdmin && (
|
||||
|
||||
Reference in New Issue
Block a user