Conversation
feat(timetable): implement update and delete routes feat(ui): add timetable selection in filter bar
|
The preview deployment for iris is ready. 🟢 Open Preview | Open Build Logs | Open Application Logs Last updated at: 2026-03-13 12:47:46 CET |
|
The preview deployment for chronos is ready. 🟢 Open Preview | Open Build Logs | Open Application Logs Last updated at: 2026-03-13 12:47:38 CET |
There was a problem hiding this comment.
Pull request overview
Adds multi-timetable support across Iris and Chronos by introducing timetable selection in the public timetable view and a new admin UI/API for managing imported timetables, while also consolidating common i18n strings and improving OpenAPI metadata for several endpoints.
Changes:
- Iris: add timetable selection (URL + UI) and an admin “Manage timetables” page (edit/delete).
- Chronos: add PATCH/DELETE timetable management endpoints and adjust timetable/cohort import & access patterns.
- General: move repeated UI strings to
common.*translations and enrich OpenAPI route parameter/response schemas.
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/iris/src/routes/_public/index.tsx | Adds timetable to validated search params for the public timetable route. |
| apps/iris/src/routes/_private/admin/timetable/substitutions.tsx | Switches some labels to common.* translation keys. |
| apps/iris/src/routes/_private/admin/timetable/moved-lessons.tsx | Switches some labels to common.* translation keys. |
| apps/iris/src/routes/_private/admin/timetable/manage.tsx | New admin page to list/edit/delete timetables. |
| apps/iris/src/routes/_private/admin/news/announcements.tsx | Switches some labels to common.* translation keys. |
| apps/iris/src/route-tree.gen.ts | Generated route-tree update to include the new manage page route. |
| apps/iris/src/hooks/use-selected-timetable.ts | New hook to fetch/sort timetables and choose an effective selection. |
| apps/iris/src/components/timetable/index.tsx | Adds timetable selection + URL sync; scopes cohorts query by timetable. |
| apps/iris/src/components/timetable/filter-bar.tsx | Adds a timetable selector UI to the timetable filter bar. |
| apps/iris/src/components/admin/substitution-dialog.tsx | Uses common.date / common.save for some dialog labels. |
| apps/iris/src/components/admin/sidebar.tsx | Adds a sidebar link to the new “Manage timetables” admin page. |
| apps/iris/src/components/admin/roles-table.tsx | Uses common.actions / common.delete for roles table labels. |
| apps/iris/src/components/admin/role-dialog.tsx | Uses common.save when editing an existing role. |
| apps/iris/src/components/admin/moved-lesson-dialog.tsx | Uses common.date / common.save for some dialog labels. |
| apps/iris/src/components/admin/announcements-dialog.tsx | Uses common.save for the submit button label. |
| apps/iris/public/locales/hu/translation.json | Adds new common.* keys and new timetable.* keys; removes duplicates from feature sections. |
| apps/iris/public/locales/en/translation.json | Adds new common.* keys and new timetable.* keys; removes duplicates from feature sections. |
| apps/chronos/src/routes/users/index.ts | Adds OpenAPI parameter docs; validates id via zod param validator. |
| apps/chronos/src/routes/timetable/manage.ts | New timetable update/delete endpoints (PATCH/DELETE). |
| apps/chronos/src/routes/timetable/import.ts | Coerces validFrom to Date in multipart form schema. |
| apps/chronos/src/routes/timetable/cohort.ts | Simplifies query and relaxes timetableId validation to string. |
| apps/chronos/src/routes/timetable/_router.ts | Wires new timetable PATCH/DELETE endpoints into the router. |
| apps/chronos/src/routes/roles/index.ts | Improves OpenAPI schemas for roles/permissions CRUD routes. |
| apps/chronos/src/routes/news/system-messages.ts | Adds OpenAPI parameter docs for pagination/id. |
| apps/chronos/src/routes/news/blogs.ts | Adds OpenAPI parameter docs for pagination/slug/id. |
| apps/chronos/src/routes/news/announcements.ts | Adds OpenAPI parameter docs for pagination/id/includeExpired. |
| apps/chronos/src/routes/doorlock/websocket-handler.ts | Adds OpenAPI metadata for WS upgrade route. |
| apps/chronos/src/routes/doorlock/stats.ts | Adds OpenAPI path parameter docs for device stats. |
| apps/chronos/src/routes/doorlock/self.ts | Adds OpenAPI path parameter docs for card operations. |
| apps/chronos/src/routes/doorlock/ota.ts | Adds OpenAPI path parameter docs for triggering OTA. |
| apps/chronos/src/routes/doorlock/devices.ts | Adds OpenAPI path parameter docs for device update/delete. |
| apps/chronos/src/routes/doorlock/cards.ts | Adds OpenAPI path parameter docs for card update/delete. |
| .devcontainer/Dockerfile | Adds ripgrep to the devcontainer image. |
You can also share your feedback on Copilot code review. Take the survey.
| updateMutation.mutate({ | ||
| id: editTarget.id, | ||
| name: editName.trim(), | ||
| validFrom: editValidFrom ? dateToYYYYMMDD(editValidFrom) : '', |
| // Timetable selection | ||
| const { | ||
| currentTimetableId, | ||
| isLoading: timetablesLoading, | ||
| selectedTimetableId, | ||
| setSelectedTimetableId, | ||
| timetables, | ||
| } = useSelectedTimetable(search.timetable); | ||
|
|
||
| // Queries — cohorts are now scoped to the selected timetable | ||
| const cohortsQuery = useQuery({ | ||
| ...QUERY_OPTIONS, | ||
| queryFn: fetchCohorts, | ||
| queryKey: ['cohorts'], | ||
| enabled: !!selectedTimetableId, | ||
| queryFn: () => { | ||
| if (!selectedTimetableId) { | ||
| return Promise.resolve([] as CohortItem[]); | ||
| } | ||
| return fetchCohortsForTimetable(selectedTimetableId); | ||
| }, | ||
| queryKey: ['cohorts', selectedTimetableId], | ||
| }); |
| const [selectedId, setSelectedId] = useState<string | null>( | ||
| initialTimetableId ?? null | ||
| ); | ||
|
|
||
| // Effective selection: use explicit selection, or fall back to current | ||
| const effectiveId = selectedId ?? currentTimetableId; | ||
|
|
| @@ -54,8 +54,7 @@ export const getCohortsForTimetable = timetableFactory.createHandlers( | |||
| const cohorts = await db | |||
| .select() | |||
| .from(cohort) | |||
| .leftJoin(timetable, eq(cohort.timetableId, timetable.id)) | |||
| .where(eq(timetable.id, timetableId)); | |||
| .where(eq(cohort.timetableId, timetableId)); | |||
|
|
|||
| tags: ['Timetable'], | ||
| }), | ||
| zValidator('param', z.object({ id: z.string() })), | ||
| zValidator('json', updateSchema), | ||
| requireAuthentication, | ||
| requireAuthorization('import:timetable'), | ||
| async (c) => { | ||
| const { id } = c.req.valid('param'); | ||
| const body = c.req.valid('json'); | ||
|
|
| const updateSchema = z.object({ | ||
| name: z.string().optional(), | ||
| validFrom: z.string().optional(), |
| // Reset selections when timetable changes | ||
| useEffect(() => { | ||
| if (!initialized) { | ||
| previousTimetableIdRef.current = selectedTimetableId ?? null; | ||
| return; | ||
| } | ||
|
|
||
| const previousTimetableId = previousTimetableIdRef.current; | ||
| const nextTimetableId = selectedTimetableId ?? null; | ||
|
|
||
| if (previousTimetableId === nextTimetableId) { | ||
| return; | ||
| } | ||
|
|
||
| previousTimetableIdRef.current = nextTimetableId; | ||
| setSelections({ class: null, classroom: null, teacher: null }); | ||
| setInitialized(false); | ||
| }, [initialized, selectedTimetableId]); |
| const formatDate = (dateStr: string | null) => { | ||
| if (!dateStr) { | ||
| return '—'; | ||
| } | ||
| return new Date(dateStr).toLocaleDateString(undefined, { | ||
| day: 'numeric', | ||
| month: 'short', | ||
| year: 'numeric', | ||
| }); | ||
| }; |
| const formatTimetableLabel = (tt: TimetableItem): string => { | ||
| if (tt.validFrom) { | ||
| const date = new Date(tt.validFrom); | ||
| const formatted = date.toLocaleDateString(undefined, { | ||
| day: 'numeric', | ||
| month: 'short', | ||
| year: 'numeric', | ||
| }); | ||
| return `${tt.name} (${formatted})`; |
| const [selectedId, setSelectedId] = useState<string | null>( | ||
| initialTimetableId ?? null | ||
| ); |
Closes #135