- 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
- 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
The classrooms module manages physical rooms across a school campus. Each classroom has a type, capacity, optional grade affinity, and integrates with sections for homeroom grouping. DEVELOPER and ADMIN have full CRUD; TEACHER and STAFF read-only.
Schema, seed data, CRUD, room detail, section generation, capacity stats, and timetable integration are production. The area is two tabs — Rooms (list + add/edit + Sync defaults, with capacity summary stats folded into the header) and Configure (sets sections-per-grade + capacity and generates each section's one main classroom). A class's room is assigned in the Classes add/edit form and per-period in the Timetable; the per-room weekly schedule lives on the room detail page (/classrooms/[id]).
Rationalized 2026-06-17 / 2026-06-18. Three redundant tabs were removed to keep the area clean: Subjects (a bulk
Class.classroomIdeditor — the same field is required in the Classes form and overridden per-period by the Timetable), Schedule (a read-only section view duplicating the Timetable feature; the real per-room view is on the room detail page), and Capacity (its summary cards moved into the Rooms page). The Configure tab was then trimmed to the lean domain — sections-per-grade + capacity + each section's main classroom; its Generate Classes from Catalog and Enroll Students in Classes blocks were removed (subjects bind to grades at catalog-bridge time; students are placed into grades/sections elsewhere). Provisioning logic (Sync defaults ↔ Configure) shares one room-name helper (catalog/room-naming.ts).
Integration
Classrooms is a lean, upstream block: it owns only grades → sections → one main classroom each. It deliberately does not know students or teachers — every other block couples to it.
| Couples with | Link | Direction | What it means |
|---|---|---|---|
| Academic structure / Grade | AcademicGrade ← Section.gradeId, Classroom.gradeId | upstream | A section belongs to a grade; rooms carry an optional grade affinity. The grade is the hinge — it implies the subjects (via the catalog bridge). |
| Catalog / Subjects | grade ⇒ subjects at bridge time | upstream | School picks country/type/level → we bridge to the right curriculum. Knowing the grade means knowing the subjects, so classrooms never generates classes from catalog. |
| Timetable | Timetable.classroomId (required) | downstream | Every slot books a room. The section's main classroom is the homeroom the generator prefers; labs/halls/fields are reached per-period here, not coupled to the section. Three unique constraints prevent double-booking. |
| Students | Student.sectionId → Section → main classroom | downstream | Students are placed into a grade + section elsewhere; their homeroom is the section's main classroom. Classrooms doesn't own students. |
| Attendance | Section.students, period-aware via timetable | downstream | The section (with its homeroom) is the roster unit; attendance is taken per period/room through timetable slots. |
| Classes (gradebook axis) | Class.classroomId (required) | downstream | Class is the de-emphasized per-subject gradebook/course axis; its room is set in the Classes form, not here. |
| Teachers | Section.homeroomTeacherId; teachers ↔ subjects elsewhere | downstream | A section may have a homeroom teacher; subject-teaching assignments live in their own flow. Classrooms doesn't own teachers. |
| Conference | Section.conferenceRecordingOptOut | downstream | Per-section live-class recording preference. |
Master classroom rule. Each section couples to exactly one main classroom — Section.classroomId is a single FK, so a section can never have more than one. Room type governs: the main room is the classroom-type homeroom; labs, halls, and fields are shared and reached through the timetable, never owned by the section. Provisioning mints a unique A0x/B0x room per section (defaultRoomName), keeping the pairing 1:1.
The flow (full chain in Integration flow):
School: country + type + level
→ bridge to catalog / curriculum (grade ⇒ subjects)
→ sections-per-grade + capacity
→ each section ↔ one main classroom ← classrooms stops here
→ timetable (section × subject × period × room, teacher-less)
→ students into sections · teachers onto subjects
→ attendance · exams · grades · reportsStatus
| Feature | Status | Notes |
|---|---|---|
| Classroom schema | Production | Full model with type, capacity, grade affinity, lang |
| Section model | Production | Homeroom groups within grades, linked to classrooms |
| Classroom types | Production | 8 types: classroom, lab, library, art, music, hall, sports, admin |
| Seed data | Production | 28 rooms across 5 buildings (A-E) |
| Configure tab | Production | Bulk section + room generation per grade |
| Room detail | Production | Detail page with per-room timetable grid, class list, utilization |
| Capacity stats | Production | Summary cards folded into the Rooms page header |
| Tab navigation | Production | 2 tabs: Rooms, Configure |
| Authorization | Production | RBAC — DEVELOPER / ADMIN write, TEACHER / STAFF read |
| Timetable integration | Production | Slot assignment with 3 unique constraints |
| Room constraints | Production | Scheduling rules, accessibility, maintenance blocks |
| Dropdown API | Production | GET /api/classrooms |
| CRUD UI | Production | DataTable with create / edit / delete |
| Room ↔ class | Production | Set in the Classes add/edit form (Class.classroomId) + Timetable |
Models
Classroom
model Classroom {
id String @id @default(cuid())
schoolId String
typeId String
gradeId String?
roomName String
capacity Int
lang String @default("ar")
school School @relation(fields: [schoolId], references: [id], onDelete: Cascade)
classroomType ClassroomType @relation(fields: [typeId], references: [id])
grade AcademicGrade? @relation(fields: [gradeId], references: [id], onDelete: SetNull)
classes Class[]
sections Section[]
timetables Timetable[]
constraints RoomConstraint[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([schoolId, roomName])
@@index([schoolId, gradeId])
@@map("classrooms")
}| Field | Type | Description |
|---|---|---|
schoolId | String | Tenant isolation |
typeId | String | FK to ClassroomType |
gradeId | String? | Optional FK to AcademicGrade (grade affinity) |
roomName | String | "A101", "Physics Lab" |
capacity | Int | Maximum student count |
lang | String | Storage language for translation (default "ar") |
Section
Homeroom group within a grade — represents "Grade 7-A", links students to a classroom and homeroom teacher.
model Section {
id String @id @default(cuid())
schoolId String
gradeId String
name String // "Grade 7-A"
letter String // "A"
lang String @default("ar")
homeroomTeacherId String?
classroomId String?
maxCapacity Int @default(30)
school School @relation(fields: [schoolId], references: [id], onDelete: Cascade)
grade AcademicGrade @relation(fields: [gradeId], references: [id], onDelete: Cascade)
homeroomTeacher Teacher? @relation("HomeroomTeacher", fields: [homeroomTeacherId], references: [id], onDelete: SetNull)
classroom Classroom? @relation(fields: [classroomId], references: [id], onDelete: SetNull)
students Student[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([schoolId, gradeId, letter])
@@unique([schoolId, name])
@@index([schoolId])
@@index([schoolId, gradeId])
@@map("sections")
}Relationship chain: AcademicGrade → Section → Student, Section → Classroom, Section → Teacher (homeroom).
ClassroomType
model ClassroomType {
id String @id @default(cuid())
schoolId String
name String
lang String @default("ar")
school School @relation(fields: [schoolId], references: [id], onDelete: Cascade)
classrooms Classroom[]
@@unique([schoolId, name])
@@map("classroom_types")
}8 seeded types: classroom, lab, library, art, music, hall, sports, admin.
RoomConstraint
Defined in prisma/models/timetable.prisma. Scheduling and accessibility metadata.
model RoomConstraint {
id String @id @default(cuid())
schoolId String
classroomId String
termId String?
allowedSubjectTypes String[] @default([])
strictCapacityLimit Boolean @default(true)
capacityBuffer Int @default(0)
wheelchairAccessible Boolean @default(false)
hasElevatorAccess Boolean @default(false)
floorLevel Int?
reservedPeriods Json @default("{}")
maintenanceBlocks Json @default("[]")
@@unique([schoolId, classroomId, termId])
@@map("room_constraints")
}Seed uses upsert — safe to run multiple times.
Integration details
Per-module specifics behind the Integration map above.
Timetable
Every timetable slot assigns a room. Three unique constraints prevent double-booking:
@@unique([schoolId, termId, dayOfWeek, periodId, classId, weekOffset])
@@unique([schoolId, termId, dayOfWeek, periodId, teacherId, weekOffset])
@@unique([schoolId, termId, dayOfWeek, periodId, classroomId, weekOffset])
The third constraint ensures no room is in two classes in the same period.
- Room detail timetable (
/classrooms/[id]) — weekly grid with class, subject, teacher, grade. Utilization =usedSlots / (teachingPeriods * workingDays). - Timetable room view (
by-room/content.tsx) — full weekly schedule + utilization widget. - Slot editor (
slot-editor-dialog.tsx) — filters rooms by occupancy so only available rooms appear. - Auto-generation (
algorithm.ts) —requiresLabheuristic routes science subjects to lab-type rooms.allowedSubjectTypesis loaded but not consumed byisRoomAvailable().
Section
AcademicGrade → Section → Classroom (via classroomId). Student → Section → Classroom (via sectionId → classroomId). Sections are created via the Configure tab's generateSections() action, which creates Section + Classroom atomically.
Default naming convention. New schools (autoProvisionSections) and the Configure wizard generate 2 grade-assigned main classrooms per grade, named {section letter}{2-digit grade number} — Grade 1 → A01 (section A) / B01 (section B), Grade 12 → A12 / B12, etc. These rooms carry a gradeId (homeroom classrooms, shown with their grade rather than the Shared badge). The section keeps its own label (Grade 1-A). Rooms without a gradeId (labs, halls, gym, seeded demo rooms) render as Shared.
List display (locale-correct without the translation API). The rooms table resolves two columns client-side so they read correctly in any language regardless of how the data was stored:
- Grade — derived as
"{Grade} {gradeNumber}"from the structured grade number (e.g.Grade 1/الصف 1), not the stored grade name. The column sorts by grade number and defaults to ascending;Shared(no grade) rows sort last. - Type — mapped through the
school.classrooms.roomTypesdictionary (keyed by the lowercased type slug:classroom,lab,hall,admin,art,music,library,sports). Unknown/custom type names fall back to their raw stored value. This is deterministic (config-like), so it does not depend on the on-demand translation cache.
The Add/Edit room form opens as a compact shadcn Dialog (not the legacy full-screen modal), laid out as two rows — room name + grade, then type + capacity. It is fed synchronously: the type/grade option lists are fetched once on the server (in content.tsx) and the edited row's current values come from the already-loaded table data (React Hook Form values), so opening an existing room does not trigger an on-open getClassroom fetch. This keeps the dialog stable on open — no blank→filled flash and no height re-center.
Class
Each Class has a required classroomId:
model Class {
classroomId String
classroom Classroom @relation(fields: [classroomId], references: [id])
}Students
- Course path:
Student → StudentClass → Class → Classroom - Homeroom path:
Student → Section → Classroom(schema only, no UI)
Teachers
- Home room:
Teacher → Class → Classroom(assigned classes' default rooms) - Per-slot:
Teacher → Timetable → Classroom(actual room per period) - Homeroom:
Teacher → Section → Classroom(viahomeroomTeacherId)
Parent portal
Guardian → Student → Timetable → Classroom (via getChildTimetable()).
Exams
No direct classroomId on Exam. Venue resolved via Exam → Class → Classroom. Conflict detection accepts optional classroomId and checks timetable + exam-to-exam conflicts.
Dashboard / facility (gaps)
UpcomingClassCard shows hardcoded mock data ("Room 101", "Hall A"). Facility management page is 100% static (42 facilities). Neither queries Classroom.
Cross-reference table
| Module | Direct FK | Path | Status |
|---|---|---|---|
| Timetable | classroomId | Direct | Production |
| Class | classroomId | Direct | Production |
| Section | classroomId | Direct | Production |
| RoomConstraint | classroomId | Direct | Production |
| Teacher | No | Teacher → Class / Timetable / Section → Classroom | Indirect |
| Student | No | Student → StudentClass → Class → Classroom | Indirect |
| Student homeroom | No | Student → Section → Classroom | Schema only |
| Parent portal | No | Guardian → Student → Timetable → Classroom | Indirect |
| Exam | No | Exam → Class → Classroom | Indirect |
| School config | No | db.classroom.count() | Read-only |
| CatalogSubject | No | No path | Gap |
| Application | No | No connection | Gap |
| Dashboard | No | Hardcoded mock | Gap |
| Facility page | No | Hardcoded static | Gap |
| Onboarding | No | Saves count to maxClasses only | Gap |
API
GET /api/classrooms — returns rooms for dropdown selection.
| Aspect | Detail |
|---|---|
| Location | src/app/api/classrooms/route.ts |
| Multi-tenant | Scoped by schoolId from tenant context (empty if missing) |
| Rate limit | API tier limits applied |
| Auth | No auth() check — only rate limit + tenant context |
| Response | { classrooms: [{ id, roomName }] } |
Key files
prisma/models/classrooms.prisma # Classroom + Section + ClassroomType
prisma/models/timetable.prisma # RoomConstraint
prisma/seeds/classrooms.ts # Seed logic (types + rooms)
prisma/seeds/constants.ts # 28 classroom definitions
src/app/api/classrooms/route.ts # Dropdown API
See also
- Timetable — slot assignment, room view
- Multi-tenancy —
schoolIdscoping - Listings — DataTable conventions