0%
balqalam Logo
balqalam
FeaturesCommunityPricingDocumentation
Login
  • Introduction
  • Pitch
  • Hogwarts
  • Live Demo
  • MVP
  • Roadmap
  • Launch Sprint
  • PRD
  • Get Started
  • Localhost
  • Architecture
  • Structure
  • Pattern
  • Page
  • Layout
  • Content
  • Types
  • Config
  • Actions
  • Queries
  • Authorization
  • Validation
  • Form
  • Table
  • Detail
  • Card
  • Util
  • Hooks
  • List Params
  • Views
  • README.md
  • ISSUE.md
  • Technology Stack
  • Database
  • File
  • CDN Assets
  • Entry Points
  • Dashboard
  • Authentication
  • Credentials
  • OAuth
  • Flow Diagrams
  • Multi-Tenancy
  • Offline
  • Onboarding
  • Onboarding Videos
  • Add Values
  • Admission
  • Application
  • Attendance
  • Compliance
  • Profile
  • Exams
  • Exam Wizard
  • Timetable
  • Classrooms
  • Notifications
  • Conference
  • LMS (Lumos)
Finance
  • Finance
  • Fee Management
  • Invoice
  • Wallet
  • Salary
  • Payroll
  • Timesheet
  • Expenses
  • Budget
  • Receipt
  • Accounts
  • Banking
  • Reports
  • Dashboard
  • Permissions
  • Messages
  • Integration Flow
  • Provision
  • AI Document Processing
  • Document Intelligence
  • Internationalization
  • Translation
  • Translation Guide
  • Icons
  • Docs Factory
  • Inspiration
  • Listings
  • Teachers
  • Students
  • Catalog
  • Library
  • Contributing
  • Code of conduct
  • GitHub Workflow
  • Database Seeds
  • Database Safety
  • Test Accounts
  • Playwright
  • Prettier
  • Block Rebound
Sales & GTM
  • Marketing Brief
  • Sales
  • Go-to-market
  • Marketing
  • Admission — Feature Spotlight
  • Pilot Program
  • Leads
  • Proposal
  • Outreach Templates
  • Case Study
  • Competitors
  • Landing-page teardown
  • Competitor FAQ
  • Business model
  • Shared economy
  • Traction
Fundraising & Ecosystem
  • Get Support
  • Investor Deck
  • Data Room
  • Investors
  • Accelerators
  • Incubators
  • Grants
  • Sponsors
  • Partners
  • Competitions & Hackathons
  • Universities & Training Centers

Timetable

PreviousNext

How the timetable works today — where it lives, how a schedule is built and generated, how each role sees it, and what is built but not yet wired.

The timetable module builds and serves weekly school schedules. An admin picks a structure during onboarding, configures working days and periods, assigns or auto-generates weekly slots, resolves conflicts, and exports to PDF; every other role gets a read view tailored to who they are. Schedules are section-based end-to-end — each section (Grade 1-A, Grade 7-B) gets a full week, with Timetable rows carrying sectionId, subjectId, classroomId, and a nullable teacherId. The slot editor writes section + subject (no new classId data is created); editing a legacy classId row backfills its section fields, migrating it in place. Slot deletion is id-based, so section-based and legacy slots delete alike. Student and guardian reads resolve Student.sectionId alongside legacy StudentClass enrollments, so section-generated schedules are visible the moment a student is placed. Default terms are calendar-aware — ACADEMIC_CALENDARS in calendars.ts derives term count and boundaries from the school's country and structure (see Provision). All operations are scoped by schoolId.

Where it lives

src/app/[lang]/s/[subdomain]/(school-dashboard)/timetable/— routes — mirror the component tree
page.tsx— Overview / Today — role-routed
layout.tsx— role-based tab nav (PageNav)
error.tsx— error boundary (top level only)
loading.tsx— skeleton
full/
page.tsx— Full-week tab (defaultTab="full")
analytics/
page.tsx
loading.tsx
conflicts/
page.tsx
loading.tsx
generate/
page.tsx
loading.tsx
settings/
page.tsx
loading.tsx
src/components/school-dashboard/timetable/— the block — all UI and logic live here
content.tsx— entry: SessionProvider → RoleRouter
actions.ts— server actions + queries ("use server", ~6k lines)
validation.ts— Zod schemas
types.ts— domain + UI types
config.ts— options, labels, defaults + constants
permissions-config.ts— client-safe PERMISSION_MATRIX + role checks
permissions.ts— server-side access guards
structures.ts— predefined school structures (sd-gov-default, …)
util.ts— pure helpers (findAvailableSlots, …)
live-class-join.ts— live-class join/start resolver (conference link)
slot-editor-dialog.tsx— assign subject / teacher / room to a slot
print.css— A4 print styles
views/— one view per role
role-router.tsx— dispatch by role + editable flag
admin-view.tsx— room / teacher grid, editable, cookie-persistent filter
teacher-view.tsx
student-view.tsx
guardian-view.tsx
simple-grid.tsx
preview.tsx
live-join-button.tsx
start-live-class-button.tsx
index.ts
generate/— auto-generation
content.tsx— Generate page
algorithm.ts— section-based scheduler
analytics/
content.tsx— utilization reporting
conflicts/
content.tsx— conflict page
settings/
content.tsx— config page
substitutions/— BUILT but not wired to any route or tab
content.tsx
absence-form.tsx
substitute-finder.tsx
substitution-list.tsx
index.ts
export/— PDF export
timetable-pdf.tsx
use-timetable-export.ts
index.ts
README.md— block docs (decisions, danger zones)
ISSUE.md
FEATURES.md
CLAUDE.md
src/tests/school-dashboard/timetable/— Vitest suites
actions.test.ts
structures.test.ts
validation.test.ts
production-readiness.test.ts
prisma/models/— data model
timetable.prisma— Timetable, TeacherConstraint, RoomConstraint, TimetableTemplate, ScheduleException, TeacherAbsence, SubstitutionRecord
school.prisma— Period (+ core school models)
schedule.prisma— SchoolWeekConfig
src/app/api/mobile/— REST — mobile only (web uses server actions directly)
timetable/[userId]/route.ts
guardian/children/[childId]/timetable/route.ts
src/components/onboarding/schedule/— onboarding step (order 5, optional)
content.tsx— structure picker
structure-preview.tsx— visual timeline
actions.ts— getSchoolScheduleData / saveScheduleChoice
config.ts— constants
validation.ts— Zod schema

The block is unusually centralized: actions.ts is a single ~6,000-line file holding all ~70 server actions and every read query — there is no separate queries.ts. The route tree under app/.../timetable/ mirrors the component tree, and every route is reachable from the UI — the admin's room/teacher axes are a filter inside one view, not separate routes (see Routes).

Permissions

Roles map to actions through PERMISSION_MATRIX in permissions-config.ts (client-safe, imported by both the layout and the views). "Modify" covers create / edit / delete / import / bulk-edit.

RoleViewModifyConflictsSettingsAnalyticsExport
DEVELOPERAllYYYYY
ADMINAllYYYYY
TEACHERAll + own———YY
ACCOUNTANTAll (read-only)———YY
STAFFAll (read-only)————Y
STUDENTOwn grade————Y
GUARDIANChild————Y
USERNone—————

Every role except USER can export. manage_substitutions is granted to ADMIN / DEVELOPER in the matrix, but the substitution UI has no route yet (see Substitutions).

Routes

layout.tsx renders role-based tabs via PageNav, each tab individually permission-gated. Admins see Overview, Analytics, Generate, Conflicts, Settings. Non-admins see Today and Full; the Analytics tab additionally appears for any role with view_analytics (teacher, accountant).

There is no separate /my-timetable route. Students and guardians use the same /timetable and /timetable/full routes — both render TimetableContent, differing only by defaultTab ("today" vs "full"), and RoleRouter dispatches to the correct view by role.

RouteComponentIn nav?
/timetableTimetableContentYes — Overview/Today
/timetable/fullTimetableContentYes — Full
/timetable/analyticsTimetableAnalyticsContentYes — Analytics
/timetable/generateGenerateTimetableContentYes — Generate
/timetable/conflictsTimetableConflictsContentYes — Conflicts
/timetable/settingsTimetableSettingsContentYes — Settings

Earlier builds carried standalone by-class, by-teacher, by-room, and templates routes that nothing linked to. They were removed: the admin already sees a single role-routed grid (AdminView) whose classroom / teacher selection is a cookie-persistent filter — one route serves every admin axis, and the choice survives refresh and navigation. (The template actions remain in actions.ts; only the unreached UI was deleted.)

Boundaries are uneven: there is a single top-level error.tsx, the remaining sub-routes ship a loading.tsx, and full/ has neither. Sidebar entry: key: "timetable", href: "/timetable", icon clock, roles ADMIN / TEACHER / STUDENT.

Role-based views

content.tsx wraps RoleRouter in SessionProvider. On mount RoleRouter calls getActiveTerm() then getPersonalizedTimetable() to resolve viewType + editable, and renders the matching view, which fetches its own data.

RoleViewData sourceSees
DEVELOPERAdminViewgetTimetableByRoom / getTimetableByTeacherRoom / teacher grid, editable slots, slot editor
ADMINAdminViewSame as DEVELOPERSame as DEVELOPER
TEACHERTeacherViewgetTimetableByTeacher + getTodaySchedulePersonal schedule, workload, current / next class
STUDENTStudentViewgetTimetableByStudentGrade + getTodayScheduleEnrolled subjects (grade-based), PDF export
GUARDIANGuardianViewgetGuardianChildren + getChildTimetableChild selector + each child's timetable
ACCOUNTANTAdminViewSame as ADMINFull admin grid, read-only (editable={false})
STAFFAdminViewSame as ADMINFull admin grid, read-only

getPersonalizedTimetable sets editable: false for ACCOUNTANT / STAFF; AdminView forwards editable to SimpleGrid and disables onSlotClick when read-only. AdminView's classroom / teacher selection is saved to a cookie (tt_filter), so the chosen axis and entity persist across refresh and navigation. The classId prop on StudentView is legacy and unused — student data is grade-resolved.

Student grade resolution

getTimetableByStudentGrade resolves a student's schedule for the active term along both axes:

User (session)
  → Student (db.student.findFirst where userId, schoolId)
    → Student.sectionId            → section-based Timetable rows   (primary)
    → StudentClass (per termId)    → classIds[] → legacy Timetable rows
      → OR of both in one query

A student placed in a section but not yet enrolled in course classes still gets a full schedule. If no StudentClass rows match the term, the legacy axis falls back to grade-name pattern matching — db.class.findMany where name ends with " - {gradeName}" — for backward compatibility with older data. The grade label comes from Student → StudentYearLevel → YearLevel.levelName (with lang for the localized name).

Onboarding schedule step

The schedule picker is an optional onboarding step (order 5), between Capacity and Branding.

1. Read school country, schoolType, schoolLevel
2. getSchoolScheduleData() — recommended structures + alternatives
3. User picks (saveScheduleChoice → school.system) or skips
4. "Next" is always enabled — the step is optional

Each structure card shows name, description, period count, time range, working days, and a color-coded timeline (blue = class, amber = break, green = lunch), with a "Recommended" badge on the top matches.

FilePurpose
src/app/[lang]/onboarding/[id]/schedule/page.tsxRoute
src/components/onboarding/schedule/content.tsxStep UI
src/components/onboarding/schedule/structure-preview.tsxVisual timeline
src/components/onboarding/schedule/actions.tsgetSchoolScheduleData(), saveScheduleChoice()
src/components/onboarding/schedule/{config,validation}.tsConstants + Zod schema

Applying a structure

applyTimetableStructure() turns a named structure into Period rows and a SchoolWeekConfig.

await applyTimetableStructure({
  yearId: "clx...",
  structureSlug: "sd-gov-default",
  replaceExisting: true,
})
 
// Legacy template names map automatically via LEGACY_TEMPLATE_MAP
await applyTimetableStructure({
  yearId: "clx...",
  structureSlug: "standard_8", // → sd-gov-default
})

It validates admin + tenant, looks up the structure (resolving legacy names), optionally deletes existing periods (replaceExisting), creates a Period row per entry (class / break / lunch), upserts SchoolWeekConfig with the working days + lunch position, and logs an audit entry.

In Settings, the schedule configurator mirrors the onboarding schedule step: a preset <Select> (defaulted to the country-recommended structure) drives a live StructurePreview, with quick-config knobs — weekend, periods/day, duration, start — for fine-tuning. Applying writes the periods; the per-period editor below handles row-level manual tweaks. The StructurePreview is shared between onboarding and Settings so both surfaces look identical.

The seed reads the school's declared structure rather than hand-rolling its own period list, so a school that says sd-private actually gets sd-private. Periods a previous structure left behind are pruned — but only when nothing references them; a period still holding timetable slots is kept and reported, since deleting it would take the slots with it.

Breaks are typed, never inferred

Period.isBreak is the source of truth for whether a period is teaching time. Never re-derive it from Period.name: the name is user-editable free text, and testing it for the English substrings "break"/"lunch" means an Arabic «فسحة» matches nothing — the generator then treats the break as teaching time and schedules classes into it. Writers set it from StructurePeriod.type !== "class"; the grid renders break rows straight from the period data, so every break shows in its real time slot.

The Sudanese school day

A Sudanese school day has exactly one break: the mid-morning فسحة (40 min), when فطور is eaten. There is no school lunch — الغداء is eaten at home after dismissal, so a midday "Lunch" period is a Western import and is not modelled. sd-gov-default runs 07:30–14:40 (8×45min) and sd-private 07:15–14:10 (7×50min), both Sun–Thu with a single فسحة after period 3. sd-british is the one SD structure that keeps a real lunch — it follows the British curriculum, Mon–Fri.

Sections and homerooms follow the school's language

Section letters, section names, and homeroom codes are generated from School.preferredLanguage via sectionLetters() / defaultSectionName() in catalog/room-naming.ts — shared by autoProvisionSections and the classrooms Configure tab so the two provisioning paths cannot drift. An Arabic school gets أ/ب/ج (أبجد order), sections named الصف الأول - أ composed from the grade's own name, and homerooms أ01…أ12; digits stay Latin to match how the UI renders numbers everywhere else (الحصة 1, 07:15). English schools are unchanged (Grade 1-A, A01). Both paths persist lang on the rows they create — a row whose stored lang disagrees with the language it is actually written in cannot be fixed at read time, because the translation layer trusts the tag.

Generating a timetable

The Generate tab is a two-step, preview-then-commit flow:

  1. generateTimetablePreview() runs generateSectionTimetable() (generate/algorithm.ts) over every section. For each section it fills the week from the grade's subject allocations, assigns a teacher from that teacher's subject expertise (left Unassigned when none is free), routes regular subjects to the section's homeroom (the room coupled to the section via Section.classroomId — a 1:1 nullable FK, never more than one) and assigns lab/hall/field rooms per-period via Timetable.classroomId (these shared rooms are NOT owned by the section), and prevents a section from being double-booked in the same period. Three unique constraints enforce double-booking prevention: teacher, room, and section cannot appear more than once in the same period.
  2. applyGeneratedTimetable() commits the preview into Timetable rows.

See Classrooms › Integration.

Zero-click auto-provision

You don't have to press Generate to get a timetable. Every school auto-provisions one as part of the provisioning doctor (repairProvisioning → autoGenerateTimetableForSchool), which runs at onboarding completion, on publish, and via the operator "Repair Provisioning" action.

The demo seed uses this same generator (2026-07-16), so seeded and real schools get identical, section-based timetables from one code path. The hand-rolled seedTimetable it replaced scheduled classes into whatever room happened to be free — offices, labs, even the football field — and wrote legacy classId-only rows the section-based reads can't see.

Anything the seed writes must be scoped to the active term, not terms[0]. Which term is active is derived from today's date, so seeding onto the first term while a later one is active leaves the grid reading an empty term on a school with a full timetable. Term pickers likewise default to the active term (getTermsForSelection orders isActive first), never simply the newest.

Curriculum (the grade → subject allocations) plus the schedule (periods + working days) is enough to fill the grid with teacher-less slots; teacher names appear once teachers are assigned, and students see their schedule the moment they're placed in a section. The active term is detected from today's date, and generation targets the same term the grid reads (resolveActiveTerm) so the two never disagree. A school that never picked a schedule structure still provisions — the doctor derives a country-recommended default and saves it.

detectTimetableConflicts() backs the Conflicts tab — teacher, room, and section double-booking — and suggestFreeSlots() powers the slot editor's free-period suggestions. getTimetableAnalytics() backs the Analytics tab.

Substitutions

The substitution subsystem is fully built in the data and action layers — the TeacherAbsence and SubstitutionRecord models, and the createTeacherAbsence, findAvailableSubstitutes, assignSubstitute, respondToSubstitution, getSubstitutionRecords, and cancelSubstitution server actions — plus a complete UI under substitutions/ (absence form, substitute finder, record list).

None of it is wired. There is no route page and no tab, and nothing imports substitutions/content. The manage_substitutions permission exists for ADMIN / DEVELOPER but has no surface. Adding a route + tab is the remaining step to ship it.

What is wired vs. not

Working end-to-end: builder grid, the five role views (admin room / teacher grid with a cookie-persistent filter), conflict detection, generate (preview + apply), analytics, settings, PDF export, the onboarding schedule step, and the mobile endpoints. The web app calls server actions directly — the old /api/timetable/* REST routes (thin wrappers consumed only by a now-deleted client store) were removed; only the mobile feeds under /api/mobile/ remain.

Built but not surfaced: the substitution subsystem (above). The template actions (createTemplateFromTerm, applyTemplateToTerm, …) also remain in actions.ts with no UI, after the unreached standalone templates route (and the by-class / by-teacher / by-room routes) were removed and folded into the role-routed view + filter.

Open (from the block's ISSUE.md): drag-and-drop slot editing (currently click-based), mobile-view polish, the full ARIA grid pattern + keyboard navigation (the conflict indicator and heading-nesting fixes have landed; the grid interaction model has not), splitting the 6.5k-line actions.ts into queries.ts/actions.ts, the content-file i18n long-tail, and print tuning for varied day counts.

Production-readiness hardening (2026-06-13)

A 9-dimension adversarial audit drove a security/correctness pass:

  • Tenant isolation — the validate*Constraints helpers were exported from a "use server" file with a caller-supplied schoolId (a cross-tenant read surface); they are now internal-only. Constraint writes verify the teacher/parent-constraint belongs to the school first, and filterTimetableByRole throws rather than silently dropping the tenant filter.
  • Correctness — detectTimetableConflicts no longer crashes on section-based slots (it dereferenced a null class) and batches its detail fetch into two queries; moveTimetableSlot blocks a section double-book at the target cell; applyTemplateToTerm and setActiveTerm are now atomic ($transaction).
  • Validation — ~12 previously-unvalidated mutations now Zod-parse their input with bounds.
  • i18n — server-side substitution/move/delete notifications are localized by School.preferredLanguage (they were hardcoded Arabic).
  • a11y — invalid heading nesting removed, decorative elements hidden from assistive tech, and conflicts signalled with an icon + screen-reader text rather than colour alone.

All mutations are admin-gated and schoolId-scoped; 180 timetable tests pass and the block is tsc-clean.

See also

Onboarding

The wizard that runs the schedule-picker step.

Attendance

Section-based consumer — periods drive auto-detection.

Multi-tenancy

How schoolId scoping isolates every query.

Catalog

Subjects and grade allocations the generator reads.

Exam WizardClassrooms

On This Page

Where it livesPermissionsRoutesRole-based viewsStudent grade resolutionOnboarding schedule stepApplying a structureBreaks are typed, never inferredThe Sudanese school daySections and homerooms follow the school's languageGenerating a timetableZero-click auto-provisionSubstitutionsWhat is wired vs. notProduction-readiness hardening (2026-06-13)See also

Built by Databayt ·

Welcome to balqalam.

A great journey is about to begin.