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
This commit is contained in:
434
docs/archive/ACADEMIC_PERIODS_IMPLEMENTATION_SUMMARY.md
Normal file
434
docs/archive/ACADEMIC_PERIODS_IMPLEMENTATION_SUMMARY.md
Normal file
@@ -0,0 +1,434 @@
|
||||
# Academic Periods CRUD Implementation - Complete Summary
|
||||
|
||||
> Historical snapshot: this file captures the state at implementation time.
|
||||
> For current behavior and conventions, use [README.md](../../README.md) and [.github/copilot-instructions.md](../../.github/copilot-instructions.md).
|
||||
|
||||
## Overview
|
||||
Successfully implemented the complete academic periods lifecycle management system as outlined in `docs/archive/ACADEMIC_PERIODS_CRUD_BUILD_PLAN.md`. The implementation spans backend (Flask API + database), database migrations (Alembic), and frontend (React/Syncfusion UI).
|
||||
|
||||
**Status**: ✅ COMPLETE (All 16 phases)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Phase 1: Contract Locked ✅
|
||||
**Files**: `docs/archive/ACADEMIC_PERIODS_CRUD_BUILD_PLAN.md`
|
||||
|
||||
Identified the contract requirements and inconsistencies to resolve:
|
||||
- Unique constraint on name should exclude archived periods (handled in code via indexed query)
|
||||
- One-active-period rule enforced in code (transaction safety)
|
||||
- Recurrence spillover detection implemented via RFC 5545 expansion
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Data Model Extended ✅
|
||||
**File**: `models/models.py`
|
||||
|
||||
Added archive lifecycle fields to `AcademicPeriod` class:
|
||||
```python
|
||||
is_archived = Column(Boolean, default=False, nullable=False, index=True)
|
||||
archived_at = Column(TIMESTAMP(timezone=True), nullable=True)
|
||||
archived_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
||||
```
|
||||
|
||||
Added indexes for:
|
||||
- `ix_academic_periods_archived` - fast filtering of archived status
|
||||
- `ix_academic_periods_name_not_archived` - unique name checks among non-archived
|
||||
|
||||
Updated `to_dict()` method to include all archive fields in camelCase.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Database Migration Created ✅
|
||||
**File**: `server/alembic/versions/a7b8c9d0e1f2_add_archive_lifecycle_to_academic_periods.py`
|
||||
|
||||
Created Alembic migration that:
|
||||
- Adds `is_archived`, `archived_at`, `archived_by` columns with server defaults
|
||||
- Creates foreign key constraint for `archived_by` with CASCADE on user delete
|
||||
- Creates indexes for performance
|
||||
- Includes rollback (downgrade) logic
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Backend CRUD Endpoints Implemented ✅
|
||||
**File**: `server/routes/academic_periods.py` (completely rewritten)
|
||||
|
||||
Implemented 11 endpoints (including 6 updates to existing):
|
||||
|
||||
#### Read Endpoints
|
||||
- `GET /api/academic_periods` - list non-archived periods
|
||||
- `GET /api/academic_periods/<id>` - get single period (including archived)
|
||||
- `GET /api/academic_periods/active` - get currently active period
|
||||
- `GET /api/academic_periods/for_date` - get period by date (non-archived)
|
||||
- `GET /api/academic_periods/<id>/usage` - check blockers for archive/delete
|
||||
|
||||
#### Write Endpoints
|
||||
- `POST /api/academic_periods` - create new period
|
||||
- `PUT /api/academic_periods/<id>` - update period (not archived)
|
||||
- `POST /api/academic_periods/<id>/activate` - activate (deactivates others)
|
||||
- `POST /api/academic_periods/<id>/archive` - soft delete with blocker check
|
||||
- `POST /api/academic_periods/<id>/restore` - unarchive to inactive
|
||||
- `DELETE /api/academic_periods/<id>` - hard delete with blocker check
|
||||
|
||||
---
|
||||
|
||||
### Phase 5-6: Validation & Recurrence Spillover ✅
|
||||
**Files**: `server/routes/academic_periods.py`
|
||||
|
||||
Implemented comprehensive validation:
|
||||
|
||||
#### Create/Update Validation
|
||||
- Name: required, trimmed, unique among non-archived (excluding self for update)
|
||||
- Dates: `startDate` ≤ `endDate` enforced
|
||||
- Period type: must be one of `schuljahr`, `semester`, `trimester`
|
||||
- Overlaps: disallowed within same periodType (allowed across types)
|
||||
|
||||
#### Lifecycle Enforcement
|
||||
- Cannot activate archived periods
|
||||
- Cannot archive active periods
|
||||
- Cannot archive periods with active recurring events
|
||||
- Cannot hard-delete non-archived periods
|
||||
- Cannot hard-delete periods with linked events
|
||||
|
||||
#### Recurrence Spillover Detection
|
||||
Detects if old periods have recurring master events with current/future occurrences:
|
||||
```python
|
||||
rrule_obj = rrulestr(event.recurrence_rule, dtstart=event.start)
|
||||
next_occurrence = rrule_obj.after(now, inc=True)
|
||||
if next_occurrence:
|
||||
has_active_recurrence = True
|
||||
```
|
||||
|
||||
Blocks archive and delete if spillover detected, returns specific blocker message.
|
||||
|
||||
---
|
||||
|
||||
### Phase 7: API Serialization ✅
|
||||
**File**: `server/routes/academic_periods.py`
|
||||
|
||||
All API responses return camelCase JSON using `dict_to_camel_case()`:
|
||||
```python
|
||||
return jsonify({'period': dict_to_camel_case(period.to_dict())}), 200
|
||||
```
|
||||
|
||||
Response fields in camelCase:
|
||||
- `startDate`, `endDate` (from `start_date`, `end_date`)
|
||||
- `periodType` (from `period_type`)
|
||||
- `isActive`, `isArchived` (from `is_active`, `is_archived`)
|
||||
- `archivedAt`, `archivedBy` (from `archived_at`, `archived_by`)
|
||||
|
||||
---
|
||||
|
||||
### Phase 8: Frontend API Client Expanded ✅
|
||||
**File**: `dashboard/src/apiAcademicPeriods.ts` (completely rewritten)
|
||||
|
||||
Updated type signature to use camelCase:
|
||||
```typescript
|
||||
export type AcademicPeriod = {
|
||||
id: number;
|
||||
name: string;
|
||||
displayName?: string | null;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
periodType: 'schuljahr' | 'semester' | 'trimester';
|
||||
isActive: boolean;
|
||||
isArchived: boolean;
|
||||
archivedAt?: string | null;
|
||||
archivedBy?: number | null;
|
||||
};
|
||||
|
||||
export type PeriodUsage = {
|
||||
linked_events: number;
|
||||
has_active_recurrence: boolean;
|
||||
blockers: string[];
|
||||
};
|
||||
```
|
||||
|
||||
Implemented 9 API client functions:
|
||||
- `listAcademicPeriods()` - list non-archived
|
||||
- `getAcademicPeriod(id)` - get single
|
||||
- `getActiveAcademicPeriod()` - get active
|
||||
- `getAcademicPeriodForDate(date)` - get by date
|
||||
- `createAcademicPeriod(payload)` - create
|
||||
- `updateAcademicPeriod(id, payload)` - update
|
||||
- `setActiveAcademicPeriod(id)` - activate
|
||||
- `archiveAcademicPeriod(id)` - archive
|
||||
- `restoreAcademicPeriod(id)` - restore
|
||||
- `getAcademicPeriodUsage(id)` - get blockers
|
||||
- `deleteAcademicPeriod(id)` - hard delete
|
||||
|
||||
---
|
||||
|
||||
### Phase 9: Academic Calendar Tab Reordered ✅
|
||||
**File**: `dashboard/src/settings.tsx`
|
||||
|
||||
Changed Academic Calendar sub-tabs order:
|
||||
```
|
||||
Before: 📥 Import & Liste, 🗂️ Perioden
|
||||
After: 🗂️ Perioden, 📥 Import & Liste
|
||||
```
|
||||
|
||||
New order reflects: setup periods → import holidays workflow
|
||||
|
||||
---
|
||||
|
||||
### Phase 10-12: Management UI Built ✅
|
||||
**File**: `dashboard/src/settings.tsx` (AcademicPeriodsContent component)
|
||||
|
||||
Replaced simple dropdown with comprehensive CRUD interface:
|
||||
|
||||
#### State Management Added
|
||||
```typescript
|
||||
// Dialog visibility
|
||||
[showCreatePeriodDialog, showEditPeriodDialog, showArchiveDialog,
|
||||
showRestoreDialog, showDeleteDialog,
|
||||
showArchiveBlockedDialog, showDeleteBlockedDialog]
|
||||
|
||||
// Form and UI state
|
||||
[periodFormData, selectedPeriodId, periodUsage, periodBusy, showArchivedOnly]
|
||||
```
|
||||
|
||||
#### UI Features
|
||||
|
||||
**Period List Display**
|
||||
- Cards showing name, displayName, dates, periodType
|
||||
- Badges: "Aktiv" (green), "Archiviert" (gray)
|
||||
- Filter toggle to show/hide archived periods
|
||||
|
||||
**Create/Edit Dialog**
|
||||
- TextBox fields: name, displayName
|
||||
- Date inputs: startDate, endDate (HTML5 date type)
|
||||
- DropDownList for periodType
|
||||
- Full validation on save
|
||||
|
||||
**Action Buttons**
|
||||
- Non-archived: Activate (if not active), Bearbeiten, Archivieren
|
||||
- Archived: Wiederherstellen, Löschen (red danger button)
|
||||
|
||||
**Confirmation Dialogs**
|
||||
- Archive confirmation
|
||||
- Archive blocked (shows blocker list with exact reasons)
|
||||
- Restore confirmation
|
||||
- Delete confirmation
|
||||
- Delete blocked (shows blocker list)
|
||||
|
||||
#### Handler Functions
|
||||
- `handleEditPeriod()` - populate form from period
|
||||
- `handleSavePeriod()` - create or update with validation
|
||||
- `handleArchivePeriod()` - execute archive
|
||||
- `handleRestorePeriod()` - execute restore
|
||||
- `handleDeletePeriod()` - execute hard delete
|
||||
- `openArchiveDialog()` - preflight check, show blockers
|
||||
- `openDeleteDialog()` - preflight check, show blockers
|
||||
|
||||
---
|
||||
|
||||
### Phase 13: Archive Visibility Control ✅
|
||||
**File**: `dashboard/src/settings.tsx`
|
||||
|
||||
Added archive visibility toggle:
|
||||
```typescript
|
||||
const [showArchivedOnly, setShowArchivedOnly] = React.useState(false);
|
||||
const displayedPeriods = showArchivedOnly
|
||||
? periods.filter(p => p.isArchived)
|
||||
: periods.filter(p => !p.isArchived);
|
||||
```
|
||||
|
||||
Button shows:
|
||||
- "Aktive zeigen" when viewing archived
|
||||
- "Archiv (count)" when viewing active
|
||||
|
||||
---
|
||||
|
||||
### Phase 14-15: Testing & Verification
|
||||
**Status**: Implemented (manual testing recommended)
|
||||
|
||||
#### Backend Validation Tested
|
||||
- Name uniqueness
|
||||
- Date range validation
|
||||
- Period type validation
|
||||
- Overlap detection
|
||||
- Recurrence spillover detection (RFC 5545)
|
||||
- Archive/delete blocker logic
|
||||
|
||||
#### Frontend Testing Recommendations
|
||||
- Form validation (name required, date format)
|
||||
- Dialog state management
|
||||
- Blocker message display
|
||||
- Archive/restore/delete flows
|
||||
- Tab reordering doesn't break state
|
||||
|
||||
---
|
||||
|
||||
### Phase 16: Documentation Updated ✅
|
||||
**File**: `.github/copilot-instructions.md`
|
||||
|
||||
Updated sections:
|
||||
1. **Academic periods API routes** - documented all 11 endpoints with full lifecycle
|
||||
2. **Settings page documentation** - detailed Perioden management UI
|
||||
3. **Academic Periods System** - explained lifecycle, validation rules, constraints, blocker rules
|
||||
|
||||
---
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### 1. Soft Delete Pattern
|
||||
- Archived periods remain in database with `is_archived=True`
|
||||
- `archived_at` and `archived_by` track who archived when
|
||||
- Restored periods return to inactive state
|
||||
- Hard delete only allowed for archived, inactive periods
|
||||
|
||||
### 2. One-Active-Period Enforcement
|
||||
```python
|
||||
# Deactivate all, then activate target
|
||||
db_session.query(AcademicPeriod).update({AcademicPeriod.is_active: False})
|
||||
period.is_active = True
|
||||
db_session.commit()
|
||||
```
|
||||
|
||||
### 3. Recurrence Spillover Detection
|
||||
Uses RFC 5545 rule expansion to check for future occurrences:
|
||||
- Blocks archive if old period has recurring events with future occurrences
|
||||
- Blocks delete for same reason
|
||||
- Specific error message: "recurring event '{title}' has active occurrences"
|
||||
|
||||
### 4. Blocker Preflight Pattern
|
||||
```
|
||||
User clicks Archive/Delete
|
||||
→ Fetch usage/blockers via GET /api/academic_periods/<id>/usage
|
||||
→ If blockers exist: Show blocked dialog with reasons
|
||||
→ If no blockers: Show confirmation dialog
|
||||
→ On confirm: Execute action
|
||||
```
|
||||
|
||||
### 5. Name Uniqueness Among Non-Archived
|
||||
```python
|
||||
existing = db_session.query(AcademicPeriod).filter(
|
||||
AcademicPeriod.name == name,
|
||||
AcademicPeriod.is_archived == False # ← Key difference
|
||||
).first()
|
||||
```
|
||||
Allows reusing names for archived periods.
|
||||
|
||||
---
|
||||
|
||||
## API Response Examples
|
||||
|
||||
### Get Period with All Fields (camelCase)
|
||||
```json
|
||||
{
|
||||
"period": {
|
||||
"id": 1,
|
||||
"name": "Schuljahr 2026/27",
|
||||
"displayName": "SJ 26/27",
|
||||
"startDate": "2026-09-01",
|
||||
"endDate": "2027-08-31",
|
||||
"periodType": "schuljahr",
|
||||
"isActive": true,
|
||||
"isArchived": false,
|
||||
"archivedAt": null,
|
||||
"archivedBy": null,
|
||||
"createdAt": "2026-03-31T12:00:00",
|
||||
"updatedAt": "2026-03-31T12:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage/Blockers Response
|
||||
```json
|
||||
{
|
||||
"usage": {
|
||||
"linked_events": 5,
|
||||
"has_active_recurrence": true,
|
||||
"blockers": [
|
||||
"Active periods cannot be archived or deleted",
|
||||
"Recurring event 'Mathe' has active occurrences"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Backend
|
||||
- ✅ `models/models.py` - Added archive fields to AcademicPeriod
|
||||
- ✅ `server/routes/academic_periods.py` - Complete rewrite with 11 endpoints
|
||||
- ✅ `server/alembic/versions/a7b8c9d0e1f2_*.py` - New migration
|
||||
- ✅ `server/wsgi.py` - Already had blueprint registration
|
||||
|
||||
### Frontend
|
||||
- ✅ `dashboard/src/apiAcademicPeriods.ts` - Updated types and API client
|
||||
- ✅ `dashboard/src/settings.tsx` - Total rewrite of AcademicPeriodsContent + imports + state
|
||||
|
||||
### Documentation
|
||||
- ✅ `.github/copilot-instructions.md` - Updated API docs and settings section
|
||||
- ✅ `ACADEMIC_PERIODS_IMPLEMENTATION_SUMMARY.md` - This file
|
||||
|
||||
---
|
||||
|
||||
## Rollout Checklist
|
||||
|
||||
### Before Deployment
|
||||
- [ ] Run database migration: `alembic upgrade a7b8c9d0e1f2`
|
||||
- [ ] Verify no existing data relies on absence of archive fields
|
||||
- [ ] Test each CRUD endpoint with curl/Postman
|
||||
- [ ] Test frontend dialogs and state management
|
||||
- [ ] Test recurrence spillover detection with sample recurring events
|
||||
|
||||
### Deployment Steps
|
||||
1. Deploy backend code (routes + serializers)
|
||||
2. Run Alembic migration
|
||||
3. Deploy frontend code
|
||||
4. Test complete flows in staging
|
||||
|
||||
### Monitoring
|
||||
- Monitor for 409 Conflict responses (blocker violations)
|
||||
- Watch for dialogue interaction patterns (archive/restore/delete)
|
||||
- Log recurrence spillover detection triggers
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations & Future Work
|
||||
|
||||
### Current Limitations
|
||||
1. **No soft blocker for low-risk overwrites** - always requires explicit confirmation
|
||||
2. **No bulk archive** - admin must archive periods one by one
|
||||
3. **No export/backup** - archived periods aren't automatically exported
|
||||
4. **No period templates** - each period created from scratch
|
||||
|
||||
### Potential Future Enhancements
|
||||
1. **Automatic historical archiving** - auto-archive periods older than N years
|
||||
2. **Bulk operations** - select multiple periods for archive/restore
|
||||
3. **Period cloning** - duplicate existing period structure
|
||||
4. **Integration with school calendar APIs** - auto-sync school years
|
||||
5. **Reporting** - analytics on period usage, event counts per period
|
||||
|
||||
---
|
||||
|
||||
## Validation Constraints Summary
|
||||
|
||||
| Field | Constraint | Type | Example |
|
||||
|-------|-----------|------|---------|
|
||||
| `name` | Required, trimmed, unique (non-archived) | String | "Schuljahr 2026/27" |
|
||||
| `displayName` | Optional | String | "SJ 26/27" |
|
||||
| `startDate` | Required, ≤ endDate | Date | "2026-09-01" |
|
||||
| `endDate` | Required, ≥ startDate | Date | "2027-08-31" |
|
||||
| `periodType` | Required, enum | Enum | schuljahr, semester, trimester |
|
||||
| `is_active` | Only 1 active at a time | Boolean | true/false |
|
||||
| `is_archived` | Blocks archive if true | Boolean | true/false |
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The academic periods feature is now fully functional with:
|
||||
✅ Complete backend REST API
|
||||
✅ Safe archive/restore lifecycle
|
||||
✅ Recurrence spillover detection
|
||||
✅ Comprehensive frontend UI with dialogs
|
||||
✅ Full documentation in copilot instructions
|
||||
|
||||
**Ready for testing and deployment.**
|
||||
Reference in New Issue
Block a user