- 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
- Bulk import
- Playwright
- Prettier
- Block Rebound
- Transportation
The attendance module tracks student attendance across multiple capture methods, with role-based access, parent notifications, US DoE risk levels, intervention tracking, and CSV / Excel / PDF export. Teachers see only their assigned classes; students and guardians see only their own records.
Ship tracker: databayt/hogwarts#1 — consolidated attendance + compliance master tracker. See also Aldar Epic 01.
Status
| Feature | Status | Notes |
|---|---|---|
| Quick Attendance (teacher landing) + staff/teacher clock | Production | Absent-oriented one-tap marking; check-in/out feeds timesheets |
| Manual / Period / QR / Barcode / Bulk upload | Production | Multiple capture methods (see Capture methods table) |
| Parent notifications + excuse submission | Production | In-app + SMS + email; staff reviews |
| Early warning | Production | US DoE risk levels, intervention tracking |
| Reports export | Production | CSV, Excel (5 sheets), PDF (branded) |
| Soft delete + restore | Production | deletedAt |
| Auto-excuse | Production | Approved AbsenceIntention → EXCUSED |
| Locale-aware names | Production | getText() translates on demand |
| Geofence / Master gate / Policy engine / Compliance reports | Wired | Functional, surface still maturing |
| Settings | Production | Persists to the school-wide AttendancePolicy row |
| AI predictions / Gamification / Hall pass / Kiosk / Compliance letters | Production | Routed; surfaced via the overview Quick-access grid |
| Absence intentions / Geofence UI | Wired | Deep-link only until internals are localized |
| ADEK eSIS daily submission | Production | Generic connector at /compliance, DRY_RUN default |
| 2-hour guardian SLA cron | Production | /api/cron/absence-followup, every 30 min, UTC |
On a phone
Below the md breakpoint the overview and every sub-page follow the school dashboard's phone pattern — the same vocabulary as the phone dashboard, library and live pages: figures as one grey two-across panel, grey borderless cards with pill buttons, section doors as app tiles, rows whose details wrap under the title instead of crushing it. Tablets and desktops are unchanged. Manual marking, the kiosk and the data tables look the same at every width.
Routes
The attendance root is role-aware: TEACHER lands on Quick Attendance (mobile-first, absent-oriented — everyone defaults present, taps cycle absent → late → present, one save submits the roster and notifies guardians of absentees), ADMIN/STAFF land on the overview, students/guardians on their self-view. All under src/app/[lang]/s/[subdomain]/(school-dashboard)/attendance/. Tabs: /, /manual, /qr-code, /records, /excuses, /early-warning, /interventions, /analytics, /reports, /settings. The staff overview's Quick-access grid links /recent, /bulk-upload, /barcode, /hall-pass, /interventions/tiers, /gamification, /ai (+ /kiosk, /letters for admins). Deep-link only: /geo, /intentions, /bulk. /analysis permanently redirects to /analytics; /parent/attendance redirects to /attendance/records. See the Capture methods table below for per-route purpose; see RBAC for per-role access.
RBAC
| Action | DEVELOPER | ADMIN | TEACHER (own sections) | STAFF | STUDENT | GUARDIAN (children) |
|---|---|---|---|---|---|---|
| Mark attendance | Y | Y | Y | Y | — | — |
| Read own records | Y | Y | — | — | Y | Y |
| Read section records | Y | Y | Y | — | — | — |
| Read school-wide | Y | Y | — | Y | — | — |
| Delete | Y | Y | Y | Y | — | — |
| Restore | Y | Y | — | — | — | — |
| Bulk upload | Y | Y | Y | — | — | — |
| Export | Y | Y | Y | Y | — | — |
| Analytics | Y | Y | Y | Y | — | — |
| Compliance / Settings | Y | Y | — | — | — | — |
| Excuse review | Y | Y | Y | Y | — | — |
| Excuse submit | Y | Y | — | — | — | Y |
A teacher's sections are their homeroom, any section they hold a timetable period with, and any section they are assigned a subject in — getTeacherSectionIds() (actions/helpers.ts). sectionScopeWhere() applies them to a read: a section filter is intersected with them, never allowed to widen the scope. A grade filter goes through the section (section.gradeId).
Manual attendance
Flat inline toolbar (PlatformToolbar): section selector, date picker, bulk status buttons, Save. getCurrentPeriod() auto-selects the section matching the current timetable period; names translate on demand via getText(). Keyboard: P / A / L for present / absent / late, Cmd/Ctrl+S save. Absent-student guardians are auto-notified on save; an approved AbsenceIntention converts ABSENT → EXCUSED.
The roster unit is the section — Section.students defines who is in the class. Each section has exactly one main classroom (homeroom) via Section.classroomId; non-homeroom rooms (labs, halls, fields) are reached per-period through the timetable slot (Timetable.classroomId), which is also what getCurrentPeriod() resolves for period-aware attendance. See Classrooms › Integration.
Capture methods
| Method | Route | Notes |
|---|---|---|
| Manual | /manual | Per-student dropdown + bulk buttons |
| Period | /manual (per-period) | getCurrentPeriod(), period analytics |
| QR | /qr-code | Generated for a section — only its students can scan; default 30-min validity, dedup |
| Barcode | /barcode | Looks up StudentIdentifier; the scan records the student's own section, so the desk picks nothing first |
| Kiosk | /kiosk | Check-in records the student's own section |
| Bulk upload | /bulk-upload | CSV: studentId, date, status, checkInTime, checkOutTime, notes |
| Geofence | /geo | Location tracker (students), zones (admins) |
| Master gate | server actions | recordMasterAttendance(), getPrefillFromMaster() |
Hall passes (/hall-pass) are issued on the student's own section, so issuing one asks for the student and destination only.
Parent engagement
triggerAbsenceNotification() fires on absent: in-app (high priority), SMS via Twilio (if configured), email. Uses school preferredLanguage.
Excuse reasons: Medical, Family Emergency, Religious, School Activity, Transportation, Weather, Other. Approved → EXCUSED; rejected → unexcused. A submitted excuse — and an absence intention with "notify teachers" on — reaches the section's homeroom teacher and the teachers assigned its subjects this term (sectionTeacherUserIds). A teacher's review queue is their own sections' excuses.
Student / guardian view: percentage + trend, calendar history, status breakdown, excuse submission for unexcused absences.
Early warning
US DoE risk levels:
| Risk | Attendance rate | Absence rate |
|---|---|---|
| Satisfactory | ≥95% | <5% |
| At-risk | 90 – 94.9% | 5 – 9.9% |
| Moderately chronic | 80 – 89.9% | 10 – 19.9% |
| Severely chronic | <80% | ≥20% |
Intervention tracking at /attendance/interventions — 14 intervention types (parent contact, home visit, counsellor / social worker / administrator referral, attendance contract, truancy referral, community resource, academic support, mentorship, incentive, other) with statuses Scheduled → In Progress → Completed / Cancelled / Escalated. Policy engine in actions/policy.ts evaluates triggers and manages exemptions.
Analytics & reports
7-tab analytics at /analytics: Summary, Calendar, Trends, Patterns, Methods, Classes (sections ranked by rate), At Risk. Filters: date range and section.
Reports at /reports: filter by section, student, status, date range, period. Export: CSV, Excel (5 sheets — summary, detailed, by status, by section, by date), PDF (branded). The export follows the section filter.
Bulk upload at /bulk-upload is for one section: every student in the file must be in it.
Compliance: getComplianceDashboard(), getComplianceReport(), getScheduledReports().
Server actions
Actions are split across per-concern modules under src/components/.../attendance/actions/.
| Module | File | Surface |
|---|---|---|
| Core | core.ts | Mark, list, check-out, bulk delete + restore |
| Analytics | analytics.ts | Stats, trends, method usage, calendar, section comparison, at-risk, recent activity |
| Periods | periods.ts | Per-period mark, current-period detection, period analytics |
| QR | qr.ts | Session generate, scan, active sessions |
| Identifiers | identifiers.ts | Student barcode / card identifier CRUD + lookup |
| Excuses | excuses.ts | Submit, review, list pending / unexcused |
| Interventions | interventions.ts | Create, update, escalate, list, stats, assignees |
| Bulk | bulk.ts | CSV / Excel upload, report builder, CSV export, recent uploads |
| Dashboard | dashboard.ts | Today's overview, by-risk, early warning, unmarked sections, parent summary |
| Policy | policy.ts | Evaluate policies, triggers, exemptions, dismissals |
| Master | master.ts | School-gate entry / exit, prefill from master |
| Compliance | compliance.ts | Compliance dashboard + scheduled reports |
| Records | records.ts | Student own + guardian children |
Sample signatures:
markAttendance(input: {
sectionId: string
date: string
records: Array<{ studentId: string; status: 'present' | 'absent' | 'late' }>
})
markPeriodAttendance(input: {
sectionId: string // a teacher's own section; every student must be in it
date: string
periodId: string
timetableId?: string // must be the section's slot
records: Array<{ studentId: string; status: 'PRESENT' | 'ABSENT' | 'LATE'; notes?: string }>
})
getAttendanceList({ sectionId, date, lang? }) // lang triggers translation
deleteAttendance(id: string) // soft delete
restoreAttendance(id: string)Schema
model Attendance {
id String @id @default(cuid())
schoolId String
studentId String
sectionId String? // the section the mark is kept on
date DateTime @db.Date
status AttendanceStatus
method AttendanceMethod @default(MANUAL)
checkInTime DateTime?
checkOutTime DateTime?
notes String?
markedBy String?
markedAt DateTime @default(now())
deletedAt DateTime?
periodId String?
timetableId String?
periodName String?
@@unique([schoolId, studentId, sectionId, date, periodId])
@@index([schoolId, date, periodId])
}Soft-delete semantics. deletedAt is a tombstone, not a hard delete. A
removed record is excluded from every analytics, dashboard, export and
regulator-compliance read, and from the student / guardian mobile views — yet the
row is retained for audit and an admin can restore it (restoreAttendance).
Because the @@unique tuple still reserves a deleted row's key, re-marking a
student for the same day/period revives the original record (clears
deletedAt) instead of creating a duplicate — this holds across manual, period,
QR, barcode, geofence and kiosk capture.
Supporting models: AttendanceExcuse, AttendanceIntervention, QRCodeSession, StudentIdentifier, AbsenceIntention, PolicyTrigger, PolicyExemption, MasterAttendance, HallPass, KioskLog, AttendanceReward, StudentBadge, AttendanceStreak.
Seed
pnpm db:seed:single attendanceRegulator compliance (ADEK eSIS — UAE)
UAE schools (School.country === "AE") can opt into a daily-CSV upload to the
ADEK eSIS portal at esis.adek.gov.ae plus the ADEK-mandated 2-hour parent
contact for unreported absences. The framework is generic — a new regulator
(SEC KSA, MoE Qatar, MoE Sudan) only adds a value to the
ComplianceProvider enum and a sibling directory under
src/lib/compliance/providers/<authority>/.
| Piece | Path | Purpose |
|---|---|---|
| Settings UI | /{lang}/compliance | Toggle, mode, SLA, daily-time, submissions list |
| Cross-tenant admin | /{lang}/(saas-dashboard)/compliance (DEVELOPER) | Shared credential groups for Aldar-style piggyback |
| Daily cron | /api/cron/esis-submit (0 10 * * *) | Enqueues + fans out per school |
| Followup cron | /api/cron/absence-followup (*/30 * * * *) | 2h SLA evidence trail |
| Webhook | /api/webhooks/adek | Async ACCEPTED/REJECTED receipts |
| Worker (out of repo) | tools/adek-rpa-worker/ (Fly.io) | Playwright fallback when no API |
Connector modes — set per school × provider:
| Mode | What it does |
|---|---|
DRY_RUN | Generate CSV, persist for audit, registrar uploads to eSIS manually |
PIGGYBACK | Submit via shared-group eSIS credentials (Aldar pattern, 1 cred row × N schools, circuit breaker on cascading failures) |
OFFICIAL_API | Documented vendor API (awaiting ADEK developer access) |
RPA | External Playwright worker claims QUEUED rows and uploads via the eSIS web portal |
DISABLED | Skip in cron |
2-hour parent-contact SLA
When SchoolComplianceConfig.enabled = true,
triggerAbsenceNotification dispatches on in_app + email + whatsapp
channels (instead of in_app only) so the email + WhatsApp crons drain the
notification row. Every 30 minutes, the absence-followup cron looks for
ABSENT rows that are:
markedAt < now() - parentContactSlaMinutes(default 120, computed in UTC)- have no
AttendanceExcuse - have no approved
AbsenceIntentioncovering today - have no
AttendanceInterventionof typePARENT_PHONE_CALL/PARENT_EMAILin the past 24h
It dispatches a absence_unreported_followup notification to each guardian,
then writes an AttendanceIntervention row (PARENT_EMAIL, parentNotified=true,
contactMethod=in_app+email+whatsapp). That row is the regulator-facing
audit evidence — getParentContactSlaReport() queries it to produce
"contacted within SLA" counts for ADEK audits.
Schema (compliance side)
Five new entities in prisma/models/compliance.prisma — none of them
ADEK-specific:
ComplianceProviderenum (currentlyADEK_ESIS,CUSTOM)ConnectorModeenumComplianceSubmissionStatusenum +CircuitBreakerStateenumSchoolComplianceConfig— one row per school × provider (opt-in)SharedComplianceCredentialGroup— group-shared creds (Aldar pattern)SchoolComplianceCredential— per-school creds (OFFICIAL_API mode)ComplianceSubmission— one row per attempt, indexed by(schoolId, provider, submissionDate, attemptNumber). Retries chain viasupersededBy. The CSV is persisted inline incsvArtifactContentand downloaded via/api/compliance/artifact/[submissionId](signed S3 URL planned).
AuditLog.userId was loosened to nullable in the same epic so cron / worker /
webhook system events can be recorded — calls pass userId: null.
A new NotificationType.absence_unreported_followup covers the 2h SLA path.
Offline marking
Quick attendance (/attendance for teachers) works without a connection. The mark goes to the device outbox (kind attendance, one pending item per section and day) and is replayed through POST /api/offline/sync → actions/quick-core.ts, the same code the online action runs. A newer mark already on the server wins and the replay is dropped as a duplicate. Guardian absence notifications fire when the replay lands. See Offline.
Try it on a phone: open Attendance → Quick, switch on airplane mode, mark a section and save — the panel confirms the marks are saved on the device and the sync banner shows one pending item. Switch the network back on and the banner drains; the marks are on the server within seconds. Marking the same section again replays as a duplicate and the newer mark wins. Installing the app first (the welcome sheet's Continue button) is not required for offline marking, but it is required on iPhone for push notifications.
See also
- Multi-tenancy — RBAC + tenant scoping
- Notifications — absence dispatch
- Timetable — period auto-detection
- Compliance — regulator-submission framework (ADEK + future authorities)