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:
2026-03-31 12:25:55 +00:00
parent 2580aa5e0d
commit b5f5f30005
23 changed files with 2940 additions and 897 deletions

View File

@@ -487,7 +487,16 @@ def create_event():
if not (ev.skip_holidays and ev.recurrence_rule):
return
# Get holidays
holidays = session.query(SchoolHoliday).all()
holidays_query = session.query(SchoolHoliday)
if ev.academic_period_id is not None:
holidays_query = holidays_query.filter(
SchoolHoliday.academic_period_id == ev.academic_period_id
)
else:
holidays_query = holidays_query.filter(
SchoolHoliday.academic_period_id.is_(None)
)
holidays = holidays_query.all()
dtstart = ev.start.astimezone(UTC)
r = rrulestr(ev.recurrence_rule, dtstart=dtstart)
window_start = dtstart
@@ -588,7 +597,16 @@ def update_event(event_id):
if not (ev.skip_holidays and ev.recurrence_rule):
return
# Get holidays
holidays = session.query(SchoolHoliday).all()
holidays_query = session.query(SchoolHoliday)
if ev.academic_period_id is not None:
holidays_query = holidays_query.filter(
SchoolHoliday.academic_period_id == ev.academic_period_id
)
else:
holidays_query = holidays_query.filter(
SchoolHoliday.academic_period_id.is_(None)
)
holidays = holidays_query.all()
dtstart = ev.start.astimezone(UTC)
r = rrulestr(ev.recurrence_rule, dtstart=dtstart)
window_start = dtstart