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

@@ -0,0 +1,55 @@
"""Add archive lifecycle fields to academic_periods
Revision ID: a7b8c9d0e1f2
Revises: 910951fd300a
Create Date: 2026-03-31 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'a7b8c9d0e1f2'
down_revision: Union[str, None] = '910951fd300a'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# Add archive lifecycle fields to academic_periods table
op.add_column('academic_periods', sa.Column('is_archived', sa.Boolean(), nullable=False, server_default='0'))
op.add_column('academic_periods', sa.Column('archived_at', sa.TIMESTAMP(timezone=True), nullable=True))
op.add_column('academic_periods', sa.Column('archived_by', sa.Integer(), nullable=True))
# Add foreign key for archived_by
op.create_foreign_key(
'fk_academic_periods_archived_by_users_id',
'academic_periods',
'users',
['archived_by'],
['id'],
ondelete='SET NULL'
)
# Add indexes for performance
op.create_index('ix_academic_periods_archived', 'academic_periods', ['is_archived'])
op.create_index('ix_academic_periods_name_not_archived', 'academic_periods', ['name', 'is_archived'])
def downgrade() -> None:
"""Downgrade schema."""
# Drop indexes
op.drop_index('ix_academic_periods_name_not_archived', 'academic_periods')
op.drop_index('ix_academic_periods_archived', 'academic_periods')
# Drop foreign key
op.drop_constraint('fk_academic_periods_archived_by_users_id', 'academic_periods')
# Drop columns
op.drop_column('academic_periods', 'archived_by')
op.drop_column('academic_periods', 'archived_at')
op.drop_column('academic_periods', 'is_archived')

View File

@@ -0,0 +1,28 @@
"""merge academic periods and client monitoring heads
Revision ID: dd100f3958dc
Revises: a7b8c9d0e1f2, c1d2e3f4g5h6
Create Date: 2026-03-31 07:55:09.999917
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'dd100f3958dc'
down_revision: Union[str, None] = ('a7b8c9d0e1f2', 'c1d2e3f4g5h6')
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

View File

@@ -0,0 +1,54 @@
"""scope school holidays to academic periods
Revision ID: f3c4d5e6a7b8
Revises: dd100f3958dc
Create Date: 2026-03-31 12:20:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'f3c4d5e6a7b8'
down_revision: Union[str, None] = 'dd100f3958dc'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column('school_holidays', sa.Column('academic_period_id', sa.Integer(), nullable=True))
op.create_index(
op.f('ix_school_holidays_academic_period_id'),
'school_holidays',
['academic_period_id'],
unique=False,
)
op.create_foreign_key(
'fk_school_holidays_academic_period_id',
'school_holidays',
'academic_periods',
['academic_period_id'],
['id'],
ondelete='SET NULL',
)
op.drop_constraint('uq_school_holidays_unique', 'school_holidays', type_='unique')
op.create_unique_constraint(
'uq_school_holidays_unique',
'school_holidays',
['name', 'start_date', 'end_date', 'region', 'academic_period_id'],
)
def downgrade() -> None:
op.drop_constraint('uq_school_holidays_unique', 'school_holidays', type_='unique')
op.create_unique_constraint(
'uq_school_holidays_unique',
'school_holidays',
['name', 'start_date', 'end_date', 'region'],
)
op.drop_constraint('fk_school_holidays_academic_period_id', 'school_holidays', type_='foreignkey')
op.drop_index(op.f('ix_school_holidays_academic_period_id'), table_name='school_holidays')
op.drop_column('school_holidays', 'academic_period_id')