Doc 12 — Tier C Addendum (Multi-Tenant + 5 Personas + P2 Modules)
Purpose: Supplements docs 08 (Flutter Handoff Map), 09 (Route Specification), and 10 (Widget Component Spec) with all Tier C scope additions. Reflects scope upgrade from Rp 145 juta (MVP 6 personas) → Rp 226 juta (Complete: multi-tenant + 11 personas + P2 features).
Audience: Flutter dev, product owner, client.
Scope snapshot (new):
- Kepsek → Kepala Yayasan refactor (rename + content-refocus, 14 screens)
- Multi-tenant auth flow (Kode Lembaga pattern B): 3 new screens
- Splash screen unified (Parent + Staff converged to emerald gradient)
- 5 new personas: Bendahara Yayasan (10), Pembina Asrama (8), Pembimbing Hafalan (6), PPDB (7), plus existing personas untouched
- 3 P2 feature modules: Kamar Management (6), Pelanggaran + Surat (5), Payroll Management (6)
Total new screens: 51 (+ 14 renamed). Combined with original 101 screens = 152 screens total.
1. Kepsek → Kepala Yayasan Refactor
Why this matters architecturally
The Laravel backend separates kepala_sekolah (lembaga-scoped, reads one institution) from kepala_yayasan (yayasan-scoped, reads consolidated across all child institutions via parent_tenant_id). The original Figma conflated both under "Kepsek" persona, which would have created route-guard ambiguity in Flutter. Post-refactor, every screen name encodes its scope via suffix.
Screen rename map
| Old (Kepsek) | New (Kepala Yayasan) | Scope suffix signals |
|---|---|---|
| Kepsek / Executive Dashboard | Kepala Yayasan / Executive Dashboard (Lembaga View) | drill-into one flagship |
| Kepsek / Laporan Akademik | Kepala Yayasan / Laporan Akademik Lembaga | single-lembaga |
| Kepsek / Laporan Keuangan | Kepala Yayasan / Laporan Keuangan Lembaga | single-lembaga |
| Kepsek / Monitoring Guru | Kepala Yayasan / Monitoring Guru Lembaga | single-lembaga |
| Kepsek / Executive Dashboard (Dark Mode) | Kepala Yayasan / Executive Dashboard (Dark Mode) | unchanged |
| Kepsek / Notifikasi Center | Kepala Yayasan / Notifikasi Center | unchanged |
| Kepsek / Dashboard | Kepala Yayasan / Dashboard Ringkas | unchanged |
| Kepsek / Laporan Analytics | Kepala Yayasan / Laporan Analytics Konsolidasi | cross-lembaga |
| Kepsek / Keuangan Overview | Kepala Yayasan / Keuangan Konsolidasi | cross-lembaga |
| Kepsek / Profil | Kepala Yayasan / Profil | unchanged |
| Kepsek / Yayasan Dashboard | Kepala Yayasan / Dashboard Konsolidasi | yayasan-level |
| Kepsek / Manajemen Guru | Kepala Yayasan / Manajemen Guru Yayasan | yayasan-level |
| Kepsek / Unit Pendidikan | Kepala Yayasan / Unit Pendidikan | yayasan-level |
| Kepsek / Audit Trail | Kepala Yayasan / Audit Trail Yayasan | yayasan-level |
Flutter widget class rename
Old: KepsekExecutiveDashboard → New: KepalaYayasanExecutiveDashboardLembaga
Old: KepsekLaporanKeuangan → New: KepalaYayasanLaporanKeuanganLembaga
Old: KepsekYayasanDashboard → New: KepalaYayasanDashboardKonsolidasi
Route guard policy mapping
// app/router/guards.dart
const _lembagaViewRoles = ['kepala_yayasan']; // can also drill-into one lembaga
const _konsolidasiRoles = ['kepala_yayasan']; // cross-lembaga only
const _yayasanLevelRoles = ['kepala_yayasan']; // yayasan-wide auth
// Route pattern parses the suffix:
String guardFor(String screenName) {
if (screenName.endsWith('Lembaga')) return 'view-lembaga-scope';
if (screenName.endsWith('Konsolidasi')) return 'view-yayasan-consolidated';
if (screenName.endsWith('Yayasan')) return 'view-yayasan-scope';
return 'view-default';
}
2. Multi-Tenant Authentication Flow
The three new auth screens
2.1 Lembaga Onboarding (first launch)
- Figma node:
509:2(page8. Shared) - Route:
/auth/onboarding - Trigger:
SharedPreferences.getString('kode_lembaga')returns null - Outcome: persists selected
kode_lembagato localStorage, navigates to/auth/login
2.2 Parent + Staff Login with Kode Lembaga field
- Figma nodes:
9:2(Parent, page2. Parent) +495:2(Staff, page8. Shared) - Route:
/auth/login - New field ID:
KodeLembagaInputclones the EmailInput style, prepends to the stack - API:
POST /api/auth/loginwith payload{ kode_lembaga, email, password } - Server behaviour:
kode_lembagaresolves →tenant_idvia LaravelLembagaModel::where('kode','=',$r->kode)->first()
2.3 Lembaga Switcher (multi-lembaga staff)
- Figma node:
512:2(page8. Shared) - Route:
/auth/switch-lembaga - Trigger:
AuthResponse.lembaga_memberships.length > 1 - Outcome: writes
selectedLembagaIdto in-memory state +X-Tenantheader for all subsequent requests
Flutter widget catalogue (new)
// widgets/auth/kode_lembaga_field.dart
class KodeLembagaField extends StatefulWidget {
final String? prefilledCode;
final Function(String code, LembagaSuggest? suggestion) onChange;
// ...
}
// widgets/auth/lembaga_suggest_dropdown.dart
class LembagaSuggestDropdown extends StatelessWidget {
final List<LembagaSuggest> suggestions;
final LembagaSuggest? selectedOne;
// Shows 5 matching lembaga with color avatar + jenjang pill
}
// widgets/auth/lembaga_card.dart
class LembagaCard extends StatelessWidget {
final Lembaga lembaga;
final bool isDefault; // → ⭐ "Default" chip + emerald border
final String lastAccess;
final VoidCallback onTap;
}
3. Bendahara Yayasan Persona — 10 Screens
Page reference
- Figma page:
6. Bendahara Yayasan(node513:2) - Palette: Emerald primary (
#2e7d32) matching brand, same as Parent
Screen inventory
| # | Screen | Figma ID | Flutter class |
|---|---|---|---|
| 1 | Dashboard Konsolidasi | 513:3 | BendYayasanDashboardKonsolidasi |
| 2 | Neraca Konsolidasi (ISAK 35) | 513:89 | BendYayasanNeracaKonsolidasi |
| 3 | Laporan Aktivitas Konsolidasi | 515:2 | BendYayasanLaporanAktivitasKonsolidasi |
| 4 | Arus Kas Konsolidasi | 515:91 | BendYayasanArusKasKonsolidasi |
| 5 | Perubahan Aset Neto | 515:180 | BendYayasanPerubahanAsetNeto |
| 6 | Approval Payroll Queue | 516:2 | BendYayasanApprovalPayrollQueue |
| 7 | Approval Payroll Detail | 516:77 | BendYayasanApprovalPayrollDetail |
| 8 | Audit Trail Konsolidasi | 517:2 | BendYayasanAuditTrailKonsolidasi |
| 9 | Lembaga Comparison | 517:94 | BendYayasanLembagaComparison |
| 10 | Profil Bendahara Yayasan | 517:163 | BendYayasanProfil |
Routes
GET /bendahara-yayasan → Dashboard
GET /bendahara-yayasan/neraca → Screen 2
GET /bendahara-yayasan/aktivitas → Screen 3
GET /bendahara-yayasan/arus-kas → Screen 4
GET /bendahara-yayasan/aset-neto → Screen 5
GET /bendahara-yayasan/approval → Screen 6
GET /bendahara-yayasan/approval/:batchId → Screen 7
GET /bendahara-yayasan/audit → Screen 8
GET /bendahara-yayasan/comparison → Screen 9
GET /bendahara-yayasan/profil → Screen 10
POST /bendahara-yayasan/approval/:id/approve → mutation
POST /bendahara-yayasan/approval/:id/reject → mutation
ISAK 35 compliance — critical terminology
Flutter text widgets must use the following exactly (other terms fail PAP 2024 audit):
- ✅
Aset Neto(neverEkuitas) - ✅
Laporan Aktivitas(neverLaba Rugi Komprehensif) - ✅
Aset Neto Tidak Terikat,Terikat Temporer,Terikat Permanen - ✅
Eliminasi Inter-Entitaswith redEbadge on line items
New widgets
// widgets/finance/konsolidasi_row.dart
class KonsolidasiRow extends StatelessWidget {
final String label;
final String value;
final bool isTotal;
final bool isEliminasi; // → red E badge trailing
final bool isIndented; // → deeper left padding for sub-items
}
// widgets/finance/aset_neto_card.dart
class AsetNetoCard extends StatelessWidget {
final String kategori; // "Tidak Terikat" | "Terikat Temporer" | "Terikat Permanen (Wakaf)"
final String saldoAwal;
final List<RollforwardItem> movements;
final String saldoAkhir;
final Color aksenWarna; // emerald / amber / purple per kategori
}
// widgets/finance/approval_batch_card.dart
class ApprovalBatchCard extends StatelessWidget {
final Lembaga lembaga; // → avatar color + initial
final String periode;
final String totalAmount;
final int staffCount;
final DateTime due;
final bool isUrgent; // < 3 days → red border + "Urgent" pill
final String makerName;
}
4. Pembina Asrama Persona — 8 Screens
Page reference
- Figma page:
9. Pembina Asrama(node519:2) - Palette: Teal
#0f766eprimary, darkest#134e4a(evokes malam tenang)
Screen inventory
| # | Screen | Figma ID | Flutter class |
|---|---|---|---|
| 1 | Dashboard Asrama | 519:3 | PembinaAsramaDashboard |
| 2 | Absensi Shalat Jama'ah | — | PembinaAsramaAbsensiShalatJamaah |
| 3 | Check-in Malam | — | PembinaAsramaCheckinMalam |
| 4 | Laporan Kesehatan | — | PembinaAsramaLaporanKesehatan |
| 5 | Kegiatan Malam | — | PembinaAsramaKegiatanMalam |
| 6 | Pelanggaran | — | PembinaAsramaPelanggaran |
| 7 | Profil Kamar | — | PembinaAsramaProfilKamar |
| 8 | Profil Pembina | — | PembinaAsramaProfil |
Data model (new tables)
-- Server-side (tenant DB)
CREATE TABLE absensi_shalat_jamaah (
id, santri_id, waktu ENUM('subuh','zuhur','ashar','magrib','isya'),
tanggal, hadir BOOL, keterangan, recorded_by, created_at
);
CREATE TABLE check_in_malam (
id, santri_id, tanggal, jam_kembali, status ENUM('hadir','izin','sakit','terlambat'),
keterangan, recorded_by
);
CREATE TABLE laporan_kesehatan (
id, santri_id, tanggal_awal, tanggal_akhir NULL, gejala TEXT,
pengobatan TEXT, urgency ENUM('ringan','sedang','berat'), monitoring_note TEXT
);
Routes
GET /pembina-asrama → Dashboard
GET /pembina-asrama/shalat → Screen 2
POST /pembina-asrama/shalat/record → batch attendance per kamar
GET /pembina-asrama/checkin → Screen 3
GET /pembina-asrama/kesehatan → Screen 4
POST /pembina-asrama/kesehatan → add sick santri
GET /pembina-asrama/kegiatan → Screen 5
GET /pembina-asrama/pelanggaran → Screen 6
GET /pembina-asrama/kamar/:kamarId → Screen 7
5. Pembimbing Hafalan Persona — 6 Screens
Page reference
- Figma page:
10. Pembimbing Hafalan(node520:2) - Palette: Royal purple
#6d28d9(reverential, Qur'anic)
Screen inventory
| # | Screen | Flutter class |
|---|---|---|
| 1 | Dashboard Hafalan | PembimbingHafalanDashboard |
| 2 | Daftar Santri (by juz) | PembimbingHafalanDaftarSantri |
| 3 | Setoran Baru (form) | PembimbingHafalanSetoranBaru |
| 4 | Riwayat Setoran | PembimbingHafalanRiwayatSetoran |
| 5 | Target Mingguan | PembimbingHafalanTargetMingguan |
| 6 | Profil Pembimbing | PembimbingHafalanProfil |
Data model — Tahfidz progression
CREATE TABLE setoran_hafalan (
id, santri_id, pembimbing_id, tanggal, jam,
surah_id, ayat_awal INT, ayat_akhir INT,
rating_kelancaran TINYINT (1-5),
rating_tajwid TINYINT (1-5),
rating_makhraj TINYINT (1-5),
rating_kefasihan TINYINT (1-5),
catatan TEXT, recorded_at
);
CREATE TABLE target_mingguan (
id, santri_id, pembimbing_id, minggu_ke, tahun,
deskripsi_target, progress_pct TINYINT, status ENUM('active','done','missed')
);
Widget spec — Quality rating
// widgets/hafalan/quality_rating_row.dart
class QualityRatingRow extends StatelessWidget {
final String aspectLabel; // "Kelancaran", "Tajwid", "Makharijul Huruf", "Kefasihan"
final int currentRating; // 0-5
final Function(int) onChange; // emits tap on star
final String descriptor; // "Sangat baik", "Baik sekali", "Cukup", "Perlu latihan", "Kurang"
}
// widgets/hafalan/juz_progress_badge.dart
class JuzProgressBadge extends StatelessWidget {
final int juzNumber;
final double pctComplete;
final Color aksenWarna; // purple when in progress, gold when complete
}
6. PPDB Flow — 7 Screens
Page reference
- Figma page:
11. PPDB (Admissions)(node521:2) - Palette: Emerald + Amber accent (welcoming, trusted)
Screen inventory
Public (no auth):
| # | Screen | Route |
|---|---|---|
| 1 | Welcome / Landing | /ppdb |
| 2 | Form Step 1 — Data Santri | /ppdb/daftar/1 |
| 3 | Form Step 2 — Data Orang Tua | /ppdb/daftar/2 |
| 4 | Form Step 3 — Review & Submit | /ppdb/daftar/3 |
| 5 | Status Pendaftaran | /ppdb/status/:noPPDB |
Admin (Admin Lembaga role):
| # | Screen | Route |
|---|---|---|
| 6 | Dashboard PPDB | /admin-lembaga/ppdb |
| 7 | Detail Calon Santri | /admin-lembaga/ppdb/:calonId |
Architecture note
Public PPDB screens cannot use the standard app shell (bottom nav, auth chips) because the user is pre-auth. These render with a minimal brochure shell (PpdbPublicScaffold) distinct from the authenticated TenantScaffold.
// scaffolds/ppdb_public_scaffold.dart
class PpdbPublicScaffold extends StatelessWidget {
final Widget body;
final String lembagaName; // injected via kode_lembaga route param
final bool showBackButton;
// No bottom nav. Static footer with call-center info.
}
7. P2 Feature Modules — 17 Screens
7.1 Kamar / Asrama Management — 6 screens
- Figma page:
12. P2 Feature Extensions, x=0–2300 band - Palette: Indigo
#6366f1 - Consumers: Pembina Asrama + Admin Lembaga (shared module)
| Screen | Route |
|---|---|
| Dashboard Kamar | /kamar |
| Daftar Kamar | /kamar/list |
| Assign Santri ke Kamar | /kamar/assign/:kamarId |
| Room Detail | /kamar/:kamarId |
| Stats & History | /kamar/stats |
| Add Kamar | /kamar/new |
7.2 Data Pelanggaran + Surat — 5 screens
- Figma page:
12. P2 Feature Extensions, x=2760–4600 band - Palette: Red
#dc2626 - Consumers: Pembina Asrama + Admin Lembaga + Kepala Yayasan
| Screen | Route |
|---|---|
| Dashboard Pelanggaran | /pelanggaran |
| Catat Pelanggaran Baru | /pelanggaran/new |
| Detail Kasus | /pelanggaran/:caseId |
| Generator Surat | /pelanggaran/surat |
| Rekap | /pelanggaran/rekap |
7.3 Payroll Management — 6 screens
- Figma page:
12. P2 Feature Extensions, x=5060–7360 band - Palette: Cyan
#0ea5e9 - Consumers: Bendahara Yayasan + Admin Lembaga
| Screen | Route |
|---|---|
| Payroll Dashboard | /payroll |
| Staff Master | /payroll/staff |
| Compute Preview | /payroll/compute |
| Slip Gaji Individual | /payroll/slip/:staffId/:periode |
| Summary Report | /payroll/report |
| Formula Settings | /payroll/settings |
8. Splash Screen Unification
Before
- Parent splash: emerald gradient, "Portal Orang Tua Santri"
- Shared splash: blue gradient, "Portal Manajemen Pesantren"
After (both)
- Emerald gradient
#43a047 → #2e7d32 → #1b5e20 - Logo Inter Bold
- Subtitle "Portal Manajemen Pesantren" (inclusive, role-agnostic)
Flutter implication
Only one splash_screen.dart needed. No conditional paths.
9. Global Design Token Additions
// design_tokens/palette_personas.dart
abstract class PersonaPalette {
// Existing
static const parent = Color(0xFF2E7D32); // emerald
static const staff = Color(0xFF2E7D32); // emerald (unified)
// New
static const bendaharaYayasan = Color(0xFF2E7D32); // same emerald, yayasan-scope
static const pembinaAsrama = Color(0xFF0F766E); // teal
static const pembimbingHafalan = Color(0xFF6D28D9); // royal purple
// Feature modules (cross-persona)
static const kamarMgmt = Color(0xFF6366F1); // indigo
static const pelanggaran = Color(0xFFDC2626); // red
static const payroll = Color(0xFF0EA5E9); // cyan
// Semantic (already in foundation)
static const success = Color(0xFF059669);
static const warning = Color(0xFFD97706);
static const danger = Color(0xFFDC2626);
static const info = Color(0xFF2563EB);
}
10. Implementation Checklist (Flutter)
- Regenerate
app_theme.dartfrom new tokens via Style Dictionary pipeline - Refactor
KepsekDashboard→KepalaYayasanDashboardKonsolidasi+ rename 13 other screens - Implement
KodeLembagaFieldwith localStorage persistence - Build
LembagaOnboardingScreen+ auto-suggest APIGET /public/lembaga/search?q= - Build
LembagaSwitcherScreen+X-Tenantheader middleware - Implement 5 new persona route trees
- Implement 3 P2 feature module route trees
- Wire Flutter accessibility labels for Arabic text in Setoran form
- QA Dark mode variants for all 51 new screens (only 1 existing Dark variant exists — Kepala Yayasan Executive Dashboard)
- Update
assets/icons/for 20+ new icon requirements
11. Tier C+ Addendum (Post-Review Revisions)
11.1 Kepala Sekolah / Kepala Yayasan Split (Revision)
The initial "Kepsek → Kepala Yayasan rename" was an interpretation error. Backend forensic revealed both roles exist independently:
| Role | Seeded? | Scope | Backend file |
|---|---|---|---|
kepala_sekolah | ✅ yes | Single lembaga | DynamicRolePermissionSeeder.php |
kepala_yayasan | ❌ code-only | Yayasan consolidation | JournalService.php:392 hardcoded |
Resolution: Split into two separate personas with clean scope boundaries.
Page 7a — Kepala Sekolah (NEW, lembaga-scope)
| # | Screen | Figma ID | Flutter class |
|---|---|---|---|
| 1 | Executive Dashboard | 90:3 | KepalaSekolahExecutiveDashboard |
| 2 | Laporan Akademik | 90:109 | KepalaSekolahLaporanAkademik |
| 3 | Laporan Keuangan | 90:201 | KepalaSekolahLaporanKeuangan |
| 4 | Monitoring Guru | 90:291 | KepalaSekolahMonitoringGuru |
| 5 | Executive Dashboard (Dark Mode) | 109:3 | KepalaSekolahExecutiveDashboardDark |
| 6 | Notifikasi Center | 109:108 | KepalaSekolahNotifikasiCenter |
| 7 | Dashboard Ringkas | 306:2 | KepalaSekolahDashboardRingkas |
| 8 | Profil | 306:215 | KepalaSekolahProfil |
Routes:
GET /kepala-sekolah → Executive Dashboard (default lembaga)
GET /kepala-sekolah/akademik → Laporan Akademik
GET /kepala-sekolah/keuangan → Laporan Keuangan
GET /kepala-sekolah/guru → Monitoring Guru
GET /kepala-sekolah/notifikasi → Notifikasi Center
GET /kepala-sekolah/profil → Profil
Page 7b — Kepala Yayasan (REDUCED to 6 screens, yayasan-scope)
| # | Screen | Figma ID | Flutter class |
|---|---|---|---|
| 1 | Laporan Analytics Konsolidasi | 306:70 | KepalaYayasanLaporanAnalyticsKonsolidasi |
| 2 | Keuangan Konsolidasi | 306:150 | KepalaYayasanKeuanganKonsolidasi |
| 3 | Dashboard Konsolidasi | 339:2 | KepalaYayasanDashboardKonsolidasi |
| 4 | Manajemen Guru Yayasan | 339:52 | KepalaYayasanManajemenGuruYayasan |
| 5 | Unit Pendidikan | 339:116 | KepalaYayasanUnitPendidikan |
| 6 | Audit Trail Yayasan | 339:170 | KepalaYayasanAuditTrailYayasan |
Routes:
GET /kepala-yayasan → Dashboard Konsolidasi
GET /kepala-yayasan/analytics → Laporan Analytics Konsolidasi
GET /kepala-yayasan/keuangan → Keuangan Konsolidasi
GET /kepala-yayasan/guru → Manajemen Guru Yayasan
GET /kepala-yayasan/lembaga → Unit Pendidikan (list lembaga anak)
GET /kepala-yayasan/audit → Audit Trail Yayasan
11.2 Vector Icon Library Replacement
Problem: Before revision, 691 text-nodes across 13 pages used Unicode emoji (✉ ⚿ 🏠 🔔 etc). Emojis render inconsistently across Android OEM skins, cannot be theme-tinted, and fail accessibility labels.
Solution: Built 🔧 Icon Library page (28 shape-composed vector icons) + ran bulk sweep.
Icon factory — 28 icons from primitive shape composition:
Field: mail · lock · eye · building
Navigation: chevron-right · chevron-left · arrow-right · arrow-left · caret-down
Status: check · close · warning · info · alert
Core: bell · home · book · money · people · settings · search · plus · calendar · phone · location · chat · download
Persona-specific: bed · pill · moon · mosque · target · trophy · star · clock · file
Sweep results — 360 emoji text-nodes replaced across 12 pages:
| Page | Replaced |
|---|---|
| 2. Parent | 42 |
| 3. Guru | 40 |
| 4. Wali Kelas | 39 |
| 5. Bendahara | 17 |
| 6. Bendahara Yayasan | 14 |
| 6. Admin Lembaga | 44 |
| 8. Kepala Sekolah | 12 |
| 9. Kepala Yayasan | 17 |
| 8. Shared | 15 |
| 9. Pembina Asrama | 32 |
| 10. Pembimbing Hafalan | 16 |
| 11. PPDB | 22 |
| 12. P2 Extensions | 36 |
| 1. Foundation & Components | 14 |
| TOTAL | 360 |
Flutter implication: Each icon maps to a 24×24 dart widget in amal_icons.dart:
abstract class AmalIcons {
static const IconData mail = IconData(0xE001, fontFamily: 'AmalIcons');
static const IconData lock = IconData(0xE002, fontFamily: 'AmalIcons');
// ... generated from icon library page via Figma export + IconFont generator
}
Alternatively use Material Symbols with semantic matching:
Icons.mail_outline → AmalIcons.mail
Icons.lock_outline → AmalIcons.lock
Icons.visibility_outlined → AmalIcons.eye
Emojis intentionally NOT replaced (keep for emotional/cultural context):
﷽(bismillah) — religious content, not icon🎉🏆⭐(celebration, achievement) — emotional signaling🤒😊— character illustrations for health/mood- Any emoji embedded inside longer string (e.g., "🌙 Waktu Isya")
11.3 Bottom Nav Standardization
Audit findings: 9 persona pages, 2 distinct architectures:
- Architecture A (flat): Guru, Kepala Sekolah — BottomNav children are alternating Frame (icon) + Text (label)
- Architecture B (grouped): Wali Kelas, Bendahara, Admin Lembaga, Kepala Yayasan, Bendahara Yayasan, Pembina Asrama, Pembimbing Hafalan — each item is a child frame
Nav-{Label}containing icon + label
Standardization applied:
- Renamed Bendahara Yayasan nav label "Dashboard" → Beranda (18 text nodes, 9 frame renames). Rationale: Indonesian-first consistency across all personas.
- Verified first-item "Beranda" consistency: 9/9 personas ✅
- Verified last-item "Profil" or equivalent: 7/9 personas (Admin Lembaga uses "Menu" as drawer-overflow, semantically correct)
Final nav map per persona (standardized):
| Persona | Beranda | 2 | 3 | 4 | 5 | 6 (Parent only) |
|---|---|---|---|---|---|---|
| Parent | Beranda | Tagihan | Tabungan | Nilai | Absensi | Profil |
| Guru | Beranda | Kehadiran | Nilai | Jadwal | Profil | — |
| Wali Kelas | Beranda | Kelas | Nilai | Absensi | Profil | — |
| Bendahara | Beranda | Bayar | Jurnal | Laporan | Profil | — |
| Admin Lembaga | Beranda | Akademik | Keuangan | SDM | Menu | — |
| Kepala Sekolah | Beranda | Laporan | Keuangan | Guru | Profil | — |
| Kepala Yayasan | Beranda | Laporan | Keuangan | Guru | Profil | — |
| Bendahara Yayasan | Beranda | Laporan | Approval | Audit | Profil | — |
| Pembina Asrama | Beranda | Shalat | Malam | Kesehatan | Profil | — |
| Pembimbing Hafalan | Beranda | Santri | Setoran | Target | Profil | — |
Flutter implementation:
// shared/bottom_nav.dart
class AmalBottomNav extends StatelessWidget {
final PersonaType persona;
final int activeIndex;
// Loads per-persona items from _personaNavs map keyed by PersonaType.
// Enforces visual uniformity: 5 items (6 for Parent), 80px height,
// icon 24px, label 10px, active = persona primary color
}
Post-deploy fix (same session, 2 iterations): Initial bottom nav standardization left 15 IconDot placeholders empty (colored rectangles, not icons). First fix attempted stroke-style outline icons, but user feedback flagged inconsistency with canonical filled-style pattern used by old personas (Guru, Wali Kelas, Bendahara, Admin Lembaga, Kepala Yayasan). Canonical spec extracted via audit: 22×22 icon frame, single FILLED vector or multi-primitive composition (solid fill + white cutouts for detail), Inter 11px labels (Medium inactive / Semi Bold active), slate-500 inactive color
#717078, persona-primary active color. Second fix applied: 115 icons rebuilt using multi-primitive filled-style composition. Icons mapped by label: Beranda→home (triangle roof+body), Laporan→file (rect with 3 text lines), Approval→check-circle (filled circle + white ✓), Audit→eye (filled oval + white iris + dark pupil), Profil→people (circle head + body), Shalat→mosque (dome+body+arch), Malam→moon (crescent), Kesehatan→pill (capsule 45°), Santri→people, Setoran→book (rect + spine + 6 text lines), Target→target (3 concentric rings + center). Professional design system lesson learned: When adding to existing design system, inherit the canonical pattern via audit — don't build parallel. This principle is now documented as guideline for Flutter handoff phase.
11.4 Updated Total Screen Count
| Previous | +Tier C+ | Total |
|---|---|---|
| 101 screens (original MVP) | +52 screens + 8 Kepsek split | 161 screens |
11.5 Updated Pricing — Rp 245.000.000
Delta from Tier C (Rp 226 jt):
- Kepala Sekolah persona separation: +Rp 12 juta (Q1=C chosen: rebuild with pure lembaga-scope content)
- Vector icon library + 360 emoji replacements: +Rp 4 juta
- Bottom nav audit + standardization: +Rp 3 juta
New total: Rp 245.000.000
12. Tier C++ Addendum (Professional Review Fixes)
Following a UX review audit, 6 specific quality fixes applied to bring new pages to canonical pattern compliance.
12.1 Icon Library Expansion (37 icons total, was 27)
10 new icons added: sunrise, sun, sun-cloud, sunset, moon-filled, briefcase, medical-kit, calendar-event, building-alt, star-filled. All follow multi-primitive filled-style composition matching canonical pattern.
12.2 Pembina Asrama Shalat Picker Fix
Previous state: 5 emoji (🌅 ☀️ 🌤️ 🌇 🌙) in waktu segmented picker. Replaced with 5 vector icons at 16×16 inside 40px segmented tiles. Active state (Isya) uses white moon on green tile, done states use persona primary color with ✓ checkmark.
12.3 Check-in Malam Progress Ring Fix
Critical fix: Previous "98%" progress indicator was a solid emerald disc (visual lie — showed 100% while label said 98%). Replaced with proper arc-based ring using ELLIPSE.arcData with innerRadius: 0.78 for donut effect. Arc spans -π/2 to -π/2 + 2π × 0.98 = 352.8°, leaving a visible 2% gap at 12 o'clock that matches the true completion state.
Flutter implementation hint:
SizedBox(
width: 64, height: 64,
child: CircularProgressIndicator(
value: 0.98,
strokeWidth: 7,
backgroundColor: Color(0xFFCCFBF1),
valueColor: AlwaysStoppedAnimation(Color(0xFF0F766E)),
),
)
12.4 Status Pill Semantic Harmonization
Previous: 5 different pill colors (pink/orange/pink/orange/purple) carried no semantic meaning.
New 3-color semantic scale:
- Red (
#fee2e2/#b91c1c) — critical/unreachable: "Belum ada info" - Amber (
#fef3c7/#92400e) — late/past-deadline: "Terlambat dari izin", "Izin pulang kemarin" - Slate (
#f1f5f9/#475569) — scheduled absence: "Izin ke rumah sakit", "Sakit di UKS"
Row borders also color-coded with severity-border variants (#fca5a5, #fcd34d, #cbd5e1).
12.5 Kegiatan Icon Semantic Disambiguation
Previous: "Kegiatan" tile used generic file icon — identical to "Laporan" in bottom nav (semantic collision). New: uses calendar-event icon (calendar with event dot indicator), visually distinct from plain file.
12.6 Per-Kamar Severity Scaling
Previous: All missing-santri kamar (-1, -2) used uniform amber color.
New progressive severity scale:
- Green (penuh): attendance = capacity (12/12)
- Amber (-1): 1 santri missing
- Red (-2, -3): 2+ santri missing — urgent attention
Left strip + count pill + number text all color-coordinated per severity level. Enables 1-second visual triage.
12.7 Professional UX Principles Documented
For future additions to the design system:
- Inherit pattern via audit — don't build parallel. Extract canonical spec from existing components before adding new ones.
- Color should carry meaning, not decoration — palette size should match data semantic dimensions.
- Progressive disclosure through visual weight — severity gradients (green → amber → red) enable subconscious triage.
- Mixed-string emojis are fine for Figma — restructuring to icon+text in Figma is high-cost; Flutter implementation handles this naturally via
Row(children: [Icon, Text]). - Use native primitives where possible —
ELLIPSE.arcDatabeats manual SVG path calculation for progress indicators.
12.8 Updated Pricing — Rp 252.000.000
Delta from Tier C+ (Rp 245 jt):
- Icon library expansion (10 new icons): +Rp 1 juta (absorbed into Design line)
- P1 fixes (Shalat + Progress Ring + standalone emoji critical): +Rp 3 juta
- P2 fixes (Status pills + Kegiatan + severity): +Rp 2 juta
- P3 fixes (Icon library docs + semantic guidelines): +Rp 1 juta
New total: Rp 252.000.000
13. Material Symbols Mapping (Flutter Implementation Reference)
Critical handoff guidance: The custom shape-composed vector icons in Figma are communication artifacts for design review. For production Flutter implementation, use Material Symbols Rounded (Google's official icon library, built into Flutter via Icons.xxx). Reasons:
- Pixel-perfect at any size — vector font format renders crisply 16px through 64px
- Battle-tested — used by Google Workspace, billions of installs
- Theme-tintable via single
colorproperty — no manual stroke/fill management - Built into Flutter —
Icons.wb_sunnyetc, no dependency to add - Solves the "16px composition limit" that custom shape icons hit in Figma
13.1 Bottom Navigation Icons
| Persona | Label | Figma Icon | Material Symbol | Flutter Code |
|---|---|---|---|---|
| All | Beranda | home (multi-primitive filled) | Icons.home | Icon(Icons.home, color: PersonaColor.primary) |
| Bendahara Yayasan | Laporan | file | Icons.description | Icon(Icons.description) |
| Bendahara Yayasan | Approval | check-circle | Icons.check_circle | Icon(Icons.check_circle) |
| Bendahara Yayasan | Audit | eye | Icons.visibility | Icon(Icons.visibility) |
| All | Profil | people | Icons.person | Icon(Icons.person) |
| Pembina Asrama | Shalat | mosque | Icons.mosque | Icon(Icons.mosque) |
| Pembina Asrama | Malam | moon-filled | Icons.bedtime | Icon(Icons.bedtime) |
| Pembina Asrama | Kesehatan | pill | Icons.medical_services | Icon(Icons.medical_services) |
| Pembimbing Hafalan | Santri | people | Icons.groups | Icon(Icons.groups) |
| Pembimbing Hafalan | Setoran | book | Icons.menu_book | Icon(Icons.menu_book) |
| Pembimbing Hafalan | Target | target | Icons.adjust | Icon(Icons.adjust) |
13.2 Pembina Asrama — Shalat Picker (5 Waktu)
The 16×16 weather icons in Figma reach pixel-density limits. Flutter implementation should use Material Symbols at 20×20 minimum:
| Waktu | Figma Icon | Material Symbol | Flutter Code |
|---|---|---|---|
| Subuh (Fajr) | crescent + star | Icons.brightness_4 (alternative: Icons.dark_mode) | Icon(Icons.brightness_4, size: 20) |
| Zuhur | full sun + rays | Icons.wb_sunny | Icon(Icons.wb_sunny, size: 20) |
| Ashar | sun + horizon | Icons.brightness_5 | Icon(Icons.brightness_5, size: 20) |
| Magrib | sun setting | Icons.brightness_6 (alternative: Icons.brightness_3) | Icon(Icons.brightness_6, size: 20) |
| Isya | filled crescent | Icons.bedtime (alternative: Icons.nights_stay) | Icon(Icons.bedtime, size: 20) |
Alternative — Phosphor Icons (more religiously authentic):
# pubspec.yaml
dependencies:
phosphor_flutter: ^2.0.0
import 'package:phosphor_flutter/phosphor_flutter.dart';
final shalatIcons = {
'Subuh': PhosphorIcons.sunHorizon(PhosphorIconsStyle.fill),
'Zuhur': PhosphorIcons.sun(PhosphorIconsStyle.fill),
'Ashar': PhosphorIcons.sunDim(PhosphorIconsStyle.fill),
'Magrib': PhosphorIcons.sunset(PhosphorIconsStyle.fill),
'Isya': PhosphorIcons.moonStars(PhosphorIconsStyle.fill),
};
Phosphor has dedicated sunrise/sunset/moonStars icons — semantically more accurate than Material Symbols' generic brightness icons.
13.3 Akses Cepat Tile Icons
| Tile | Figma Icon | Material Symbol | Phosphor Alternative |
|---|---|---|---|
| Shalat | mosque (custom) | Icons.mosque | PhosphorIcons.mosque() |
| Check-in | home | Icons.cottage | PhosphorIcons.house() |
| Kesehatan | pill capsule | Icons.medical_services | PhosphorIcons.firstAidKit() |
| Kegiatan | calendar-event | Icons.event | PhosphorIcons.calendarCheck() |
| Kamar | bed | Icons.bed | PhosphorIcons.bed() |
| Pelanggaran | warning triangle | Icons.report | PhosphorIcons.warning() |
| Profil (settings) | gear | Icons.settings | PhosphorIcons.gear() |
| Notifikasi | bell | Icons.notifications | PhosphorIcons.bell() |
13.4 Form Field Icons (Input adornments)
| Field | Figma Icon | Material Symbol | Position |
|---|---|---|---|
mail | Icons.mail_outline | prefixIcon | |
| Password | lock | Icons.lock_outline | prefixIcon |
| Show/hide password | eye | Icons.visibility_outlined / Icons.visibility_off_outlined | suffixIcon |
| Kode Lembaga | building | Icons.business_outlined | prefixIcon |
| NIK | id-card | Icons.credit_card | prefixIcon |
| Tanggal Lahir | cake | Icons.cake_outlined | prefixIcon |
| Telepon | phone | Icons.phone_outlined | prefixIcon |
| Alamat | location | Icons.location_on_outlined | prefixIcon |
13.5 Status & Severity Icons
| State | Figma Icon | Material Symbol |
|---|---|---|
| Success / Done | check (filled circle) | Icons.check_circle |
| Warning | warning (triangle) | Icons.warning |
| Error / Critical | alert | Icons.error |
| Info | info | Icons.info |
| Neutral / Help | ? badge | Icons.help_outline |
13.6 Action Icons
| Action | Figma Icon | Material Symbol |
|---|---|---|
| Add new | plus | Icons.add |
| Search | search | Icons.search |
| Filter | (chip dropdown) | Icons.filter_list |
| Sort | (chip dropdown) | Icons.sort |
| Download | download | Icons.download |
| Upload | (mirror download) | Icons.upload |
| Edit | (pencil) | Icons.edit_outlined |
| Delete | close | Icons.delete_outline |
| Share | (paper plane) | Icons.share |
| Refresh | (rotate) | Icons.refresh |
13.7 Implementation Strategy
Recommended Flutter setup — abstract icon access via shared widget:
// lib/shared/icons/amal_icons.dart
abstract class AmalIcons {
// Bottom nav
static const IconData beranda = Icons.home;
static const IconData laporan = Icons.description;
static const IconData approval = Icons.check_circle;
static const IconData audit = Icons.visibility;
static const IconData profil = Icons.person;
// Shalat (use Phosphor for semantic accuracy if desired)
static const IconData subuh = Icons.brightness_4;
static const IconData zuhur = Icons.wb_sunny;
static const IconData ashar = Icons.brightness_5;
static const IconData magrib = Icons.brightness_6;
static const IconData isya = Icons.bedtime;
// Akses Cepat (Pembina Asrama)
static const IconData shalat = Icons.mosque;
static const IconData checkIn = Icons.cottage;
static const IconData kesehatan = Icons.medical_services;
static const IconData kegiatan = Icons.event;
static const IconData kamar = Icons.bed;
static const IconData pelanggaran = Icons.report;
// Form fields
static const IconData email = Icons.mail_outline;
static const IconData password = Icons.lock_outline;
static const IconData kodeLembaga = Icons.business_outlined;
}
Usage anywhere:
Icon(AmalIcons.shalat, size: 22, color: PersonaPalette.pembinaAsrama)
This pattern means every screen uses the same icon constant. To swap from Material Symbols to Phosphor (or any other library), only update AmalIcons — all 161 screens benefit automatically.
13.7.1 Font Stack — Multi-Script Support (Indonesian + Arabic)
Critical: Inter font does NOT include Arabic glyphs (or Quranic ligatures like ﷽ U+FDFD bismillah). Using Inter for Arabic text causes:
- Wrong glyph fallback (renders via system font with incorrect metrics)
- Width calculation broken (e.g., bismillah glyph reports as 203px wide instead of 36px)
- Layout overflow (text extends outside its container)
Required font stack for Amal app:
# pubspec.yaml
fonts:
- family: Inter
fonts:
- asset: assets/fonts/Inter-Regular.ttf
- asset: assets/fonts/Inter-Medium.ttf
weight: 500
- asset: assets/fonts/Inter-SemiBold.ttf
weight: 600
- asset: assets/fonts/Inter-Bold.ttf
weight: 700
- family: Amiri
fonts:
- asset: assets/fonts/Amiri-Regular.ttf
- asset: assets/fonts/Amiri-Bold.ttf
weight: 700
Usage pattern — abstract via TextStyle constants:
// lib/shared/theme/typography.dart
abstract class AmalText {
static const TextStyle title = TextStyle(fontFamily: 'Inter', fontSize: 28, fontWeight: FontWeight.bold);
static const TextStyle body = TextStyle(fontFamily: 'Inter', fontSize: 14);
// Arabic-specific
static const TextStyle arabicHeader = TextStyle(fontFamily: 'Amiri', fontSize: 24);
static const TextStyle arabicBody = TextStyle(fontFamily: 'Amiri', fontSize: 18);
}
// Usage:
Text('﷽', style: AmalText.arabicHeader.copyWith(color: Color(0xFFE9D5FF)))
Text('الْكَهْف', style: AmalText.arabicBody)
Where Arabic text appears in app:
- Pembimbing Hafalan Dashboard hero — "بِسْمِ اللهِ" bismillah accent (compact form)
- Pembimbing Hafalan Setoran Baru — surah name in Arabic alongside transliteration
- Direction C (Warm Islamic) mockups — calligraphy headers (deferred from Tier C+)
- Any future quote/dua/ayat content
⚠️ Important — Avoid U+FDFD bismillah ligature:
The Unicode character ﷽ (U+FDFD "Arabic Ligature Sallallahou Alayhe Wasallam") is an ornamental banner glyph designed for classical Quranic typography. In Amiri Bold at 28px, it renders 321px wide — far too wide for modern UI accent placement. Always use the compact text form instead:
| ❌ Avoid | ✅ Use | Reason |
|---|---|---|
﷽ (U+FDFD) | بِسْمِ اللهِ | Compact form fits as UI accent (38px vs 321px wide) |
| Single ligature char | Multi-char text | Modern Islamic apps (Muslim Pro, Athan) all use compact form |
// ❌ Wrong - overflows in mobile UI
Text('﷽', style: TextStyle(fontFamily: 'Amiri', fontSize: 28))
// ✅ Right - compact accent
Text('بِسْمِ اللهِ', style: TextStyle(fontFamily: 'Amiri', fontSize: 16, fontWeight: FontWeight.bold))
13.8 Why This Matters
- Saves design budget — no need to perfect 37 custom icons in Figma
- Production-grade quality — Material Symbols are pixel-perfect at any size
- Accessibility built-in — Material Symbols have semantic labels for screen readers
- Theme-tintable — single
colorproperty, works with light/dark mode - Future-proof — adding new persona/feature = pick from existing library, not commission new SVG
Custom Figma icons remain valuable as design communication — they convey intent, brand feel, and structural pattern. But for production rendering, Material Symbols is the right tool.
14. Production Cleanup — Duplicate Frame Removal
During post-Tier C audit, discovered staff self-attendance screens (built in earlier sessions) had massive frame duplication:
| Screen | Original children | Duplicate frames | Cleaned children |
|---|---|---|---|
| Guru / Presensi Hari Ini | 62 | 40 | 22 |
| Guru / Riwayat Presensi | 65 | 39 | 26 |
| Guru / Pengajuan Izin | 47 | 27 | 20 |
| WK / Presensi Hari Ini | 47 | 29 | 18 |
| WK / Riwayat Presensi | 44 | 20 | 24 |
| WK / Pengajuan Izin | 32 | 15 | 17 |
| TOTAL | 297 | 170 | 127 |
Root cause: Frame duplication accident during incremental builds — likely Cmd+D (duplicate) actions that were never undone, accumulated layer-by-layer at exact same coordinates.
Detection script (Plugin API):
const seen = new Map();
const dupes = [];
for (const child of screen.children) {
const sig = `${child.name}|${Math.round(child.x)}|${Math.round(child.y)}|${Math.round(child.width)}`;
if (seen.has(sig)) dupes.push(child);
else seen.set(sig, child);
}
for (const d of dupes) d.remove();
Concurrent fix — Invisible CTA buttons: 4 button frames had #ffffff background with white text → invisible. Detected via findAll(name matches Submit|Clock In|Kirim|Simpan|Daftar) + check fill type. Applied emerald gradient + white text + shadow.
Flutter implication: Production code must enforce one-of-each-element discipline. Use widget keys (Key('clock-in-button')) so React-style reconciliation prevents accidental duplicates. Never use Stack to compose primary CTAs — use Column so a duplicate can't visually overlap and hide its sibling.
15. P5 & PAUD Module — Kurikulum Merdeka Mobile (Tier C+++++)
Backend Project Amal sudah implement penuh P5 (Proyek Penguatan Profil Pelajar Pancasila) sesuai Permendikbudristek No. 13 Tahun 2025. Mobile module dibangun sebagai cross-cutting feature yang mounted ke 3 personas (Guru formal, Wali Kelas, Parent) dengan conditional rendering by jenjang.
15.1 Multi-Jenjang Scope Matrix
Scope rules — yang menentukan UI yang ditampilkan:
| Jenjang | Curriculum | Show P5? | Mobile Screens Used |
|---|---|---|---|
| TK (Taman Kanak-Kanak) | PAUD/Kurikulum Merdeka PAUD | ❌ | PAUD STPPA screens (alternative) |
| MI (Madrasah Ibtidaiyah) | Kurikulum Merdeka | ✅ | P5 Fase A/B/C screens |
| MTs (Madrasah Tsanawiyah) | Kurikulum Merdeka | ✅ | P5 Fase D screens |
| MA (Madrasah Aliyah) | Kurikulum Merdeka | ✅ | P5 Fase E/F screens |
| Diniyah (religious-only) | Kurikulum Diniyah | ❌ | Skip P5 entirely (use Pembimbing Hafalan) |
15.2 Page Reference (RESTRUCTURED — v1.7)
Architectural decision: P5/PAUD screens awalnya di module page 15. P5 & PAUD, tapi setelah review direstrukturisasi ke persona pages mereka karena:
- Setiap persona punya distinct workflow & UI (input vs view, formal vs read-only)
- Flutter dev navigates persona-by-persona, bukan feature-by-feature
- P5 bukan "shared entity" seperti P2 modules (Kamar/Pelanggaran/Payroll) — lebih ke "persona-specific workflow yang share data"
New location of 9 screens:
- Page
2. Parent (Orang Tua): 3 screens (P5 Progress Anak [Formal], PAUD Perkembangan Anak [TK], Rapor P5 [Formal]) - Page
3. Guru (Teacher): 4 screens (P5 Project List, Quick Assessment, Photo Evidence, Progress Siswa) — semua [Formal] - Page
4. Wali Kelas: 2 screens (P5 Aggregate Kelas [Formal], PAUD Perkembangan [TK])
Page 15. P5 & PAUD deleted setelah restructure.
Naming convention: [Persona] / [Formal|TK] [Feature]
Guru / [Formal] P5 Project ListWK / [TK] PAUD PerkembanganParent / [Formal] Rapor P5
Visual jenjang badge in HeaderBand top-right per screen:
- "MI · MTs · MA" badge for [Formal] screens
- "TK (PAUD)" badge for [TK] screens
Diniyah scope note added on page 12. Pembimbing Hafalan: "Santri Diniyah-only TIDAK punya P5 — pakai hafalan/kitab tracking via persona ini."
- Palette: Amber primary
#f59e0b(Kemendikbud Merdeka official); Pink#ec4899for PAUD distinction
15.3 Screen Inventory (9 total)
| # | Screen | Figma ID | Flutter Class | Persona |
|---|---|---|---|---|
| 1 | P5 Project List | 586:3 | P5ProjectList | Guru formal |
| 2 | P5 Quick Assessment | 586:60 | P5QuickAssessment | Guru formal |
| 3 | P5 Photo Evidence | 586:138 | P5PhotoEvidence | Guru formal |
| 4 | P5 Progress Siswa (radar) | 586:164 | P5ProgressSiswa | Guru/WK |
| 5 | P5 Aggregate Kelas | 586:204 | P5AggregateKelas | Wali Kelas formal |
| 6 | PAUD Perkembangan TK | 587:2 | PAUDPerkembanganTK | Wali Kelas TK |
| 7 | Parent P5 Progress | 587:69 | P5ParentView | Orang Tua (anak MI/MTs/MA) |
| 8 | Parent PAUD STPPA | 587:140 | PAUDParentView | Orang Tua (anak TK) |
| 9 | P5 Rapor Section | 587:189 | P5RaporSection | All personas |
15.4 6 Dimensi Pancasila — Color Mapping
| Dimensi | Color | Hex |
|---|---|---|
| Beriman & Bertaqwa | Emerald | #059669 |
| Berkebinekaan Global | Blue | #2563eb |
| Bergotong Royong | Orange | #ea580c |
| Mandiri | Purple | #7c3aed |
| Bernalar Kritis | Indigo | #4f46e5 |
| Kreatif | Pink | #db2777 |
15.5 Routes
GET /p5/projects → Project List (filtered by guru.formal_assignments)
GET /p5/projects/:id/assess → Quick Assessment
POST /p5/projects/:id/assessments → Submit assessment
POST /p5/projects/:id/photos → Upload photo evidence
GET /p5/students/:id/progress → Progress Siswa with radar
GET /p5/classes/:id/aggregate → Aggregate Kelas (only formal MI/MTs/MA)
GET /paud/classes/:id/perkembangan → PAUD STPPA alternative for TK
GET /parent/p5/:childId → Parent view P5 (child age 7-18)
GET /parent/paud/:childId → Parent view PAUD (child age 4-6)
GET /rapor/:studentId/p5/:semester → Rapor P5 section
15.6 Conditional Rendering Logic (Flutter)
// In persona dashboard
if (guru.assignments.any((a) => ['MI','MTs','MA'].contains(a.jenjang))) {
showMenuItem(P5MenuItem());
}
// In parent dashboard (per-child tab)
for (final child in parent.children) {
switch (child.enrolledJenjang) {
case 'TK':
showTab(PAUDProgressTab(child));
case 'MI': case 'MTs': case 'MA':
showTab(P5ProgressTab(child));
// Diniyah-only: no curriculum tab
}
}
// Wali Kelas dashboard
if (waliKelas.kelas.jenjang == 'TK') {
return PAUDPerkembanganScreen();
} else {
return P5AggregateScreen();
}
15.7 Synergy with Pembina Asrama (Future Enhancement, Tier 3)
Activities tracked di Pembina Asrama persona (shalat jama'ah, kegiatan malam, pelanggaran) bisa otomatis menjadi bukti P5 untuk Guru formal:
| Pembina Activity | P5 Dimensi Evidence |
|---|---|
| Shalat berjama'ah hadir | Beriman & Bertaqwa |
| Kerja bakti asrama | Bergotong Royong |
| Kegiatan malam mandiri | Mandiri |
| Diskusi kitab kuning | Bernalar Kritis |
Cross-module data sharing — deferred to Tier 3 (post-MVP).
15.8 Updated Pricing — Rp 266.000.000
Delta from previous (Rp 252 jt):
- 8 P5 core screens: +Rp 12 jt
- 2 PAUD alternative screens (TK): +Rp 1.5 jt
- Cross-module integration architecture: +Rp 0.5 jt
New total: Rp 266.000.000
16. Selfie Attendance + ML Kit Liveness (Post-Tier-C Addition)
16.1 Business Justification
Original staff self-attendance design relied only on GPS radius verification. Discovered weakness: staff bisa "titip absen" — beri HP ke teman untuk clock-in dari campus while actual physical presence elsewhere.
Selfie + liveness detection eliminates this fraud vector:
- Photo evidence of physical presence (not just device location)
- ML Kit liveness check prevents photo-of-photo attacks
- Timestamp + GPS coordinates embedded in photo metadata
- Face match score recorded untuk audit trail
Industry precedent: Gadjian, Talenta, BPJS Mobile, Gojek Driver — semua implement pattern ini untuk anti-fraud.
16.2 Screen Changes
Updated screens (2):
- Guru / Presensi Hari Ini — added selfie info banner between GPS verified dan Clock In button
- WK / Presensi Hari Ini — same update (mirror)
New screens (4):
- Guru / Selfie Camera (node
622:12) - Guru / Clock In Success (node
622:34) - WK / Selfie Camera (node
622:73) - WK / Clock In Success (node
622:95)
Archived screens (2):
[DEPRECATED] Guru / QR Scanner(moved to Archive page) — legacy QR-based attendance replaced[DEPRECATED] Guru / Kehadiran QR Scanner— same, deprecated design
16.3 Selfie Camera Screen Anatomy
Dark theme (#0f172a) full-screen camera UX:
┌─────────────────────────────────┐
│ ✕ Selfie Kehadiran│ Top bar
│ │
│ ┌─────────────────────────────┐ │
│ │ ML Kit Active [chip] │ │
│ │ │ │
│ │ ╭────────────╮ ✓Terdet │ │ Viewport w/ face guide
│ │ ╱ ╲ │ │
│ │ │ │ │ │
│ │ ╲ ╱ │ │
│ │ ╰────────────╯ │ │
│ │ │ │
│ │ 🙂 Tersenyum... │ │ Liveness instruction
│ │ ━━━━━━━━━━━━━━ 60% │ │ Progress bar
│ └─────────────────────────────┘ │
│ │
│ Posisikan wajah... │ Guidance text
│ │
│ ( ◎ capture ) │ Big capture button
│ Batalkan │
└─────────────────────────────────┘
16.4 Clock In Success Screen Anatomy
Light emerald theme (#ecfdf5) celebration:
┌─────────────────────────────────┐
│ │
│ ╭─────╮ │
│ ╱ ✓ ╲ │ Big check hero
│ ╲ ╱ │
│ ╰─────╯ │
│ │
│ Clock In Berhasil! │ Heading
│ Senin, 7 Apr · 07:23 WIB │
│ [✓ Tepat Waktu] │ Status pill
│ │
│ ┌───────────────────────────┐ │
│ │ Detail Kehadiran 😊✓ │ │ Detail card
│ │ 📍 Lokasi MTs Al-Hikmah │ │ with selfie
│ │ 📷 Selfie Terverifikasi │ │ thumb +
│ │ 🕐 Shift 06:30-15:00 │ │ verified
│ └───────────────────────────┘ │
│ │
│ [Kembali ke Beranda] (primary)│ CTAs
│ Lihat Riwayat Presensi → │
│ │
└─────────────────────────────────┘
16.5 Flutter Implementation
Dependencies to add:
dependencies:
camera: ^0.10.5
google_mlkit_face_detection: ^0.10.0
image_picker: ^1.0.4
path_provider: ^2.1.1
http: ^1.1.0
geolocator: ^10.1.0
Flow code outline:
class SelfieAttendanceFlow {
Future<void> clockIn() async {
// 1. Validate GPS (radius from school)
final position = await Geolocator.getCurrentPosition();
final distance = Geolocator.distanceBetween(
position.latitude, position.longitude,
school.latitude, school.longitude,
);
if (distance > 50) throw OutOfRadiusException();
// 2. Open Selfie Camera screen
final selfie = await Navigator.push<File>(
context, MaterialPageRoute(builder: (_) => SelfieCameraScreen()),
);
if (selfie == null) return;
// 3. Run ML Kit face detection + liveness
final faceDetector = FaceDetector(
options: FaceDetectorOptions(
enableLandmarks: true,
enableClassification: true,
enableTracking: true,
enableContours: true,
performanceMode: FaceDetectorMode.accurate,
),
);
final inputImage = InputImage.fromFile(selfie);
final faces = await faceDetector.processImage(inputImage);
if (faces.isEmpty) throw NoFaceDetectedException();
if (faces.first.smilingProbability! < 0.5) throw LivenessFailException();
// 4. Upload selfie + metadata
final response = await api.post('/api/attendance/selfie-clock-in', {
'selfie_base64': base64Encode(selfie.readAsBytesSync()),
'latitude': position.latitude,
'longitude': position.longitude,
'liveness_score': faces.first.smilingProbability,
'timestamp': DateTime.now().toIso8601String(),
});
// 5. Navigate to Success screen
Navigator.pushReplacement(
context, MaterialPageRoute(builder: (_) => ClockInSuccessScreen(response)),
);
}
}
16.6 Backend Endpoint
New endpoint: POST /api/v1/attendance/selfie-clock-in
Request payload:
{
"selfie_base64": "...",
"latitude": -7.2575,
"longitude": 112.7521,
"liveness_score": 0.87,
"timestamp": "2025-04-07T07:23:00+07:00"
}
Backend validation:
- Decode base64, save to S3/object storage
- Verify GPS within school radius (reject if > threshold)
- Face match against reference photo (via compreface/face-api/AWS Rekognition)
- Liveness score threshold (reject if < 0.5)
- Record attendance with selfie_url + verification metadata
- Return status + attendance_id
Response:
{
"status": "success",
"attendance_id": "att_abc123",
"clock_in_time": "07:23:00",
"is_on_time": true,
"selfie_url": "https://cdn.amal.sch.id/selfies/2025-04-07/guru-123.jpg",
"face_match_score": 0.98,
"liveness_pass": true
}
16.7 Security & Fraud Prevention
Anti-fraud measures:
- Liveness detection — ML Kit face classification (smile/blink) prevents static photo spoofing
- GPS validation — backend re-verify radius (don't trust client-submitted lat/long)
- Face matching — server-side compare with stored reference photo (onboarding baseline)
- Rate limiting — max 5 clock-in attempts/hour to prevent brute force
- Audit log — all attendance records with selfie URL + metadata preserved
Privacy compliance:
- Selfie stored encrypted at rest (AES-256)
- Auto-purge after 12 months (retention policy)
- GDPR/UU PDP equivalent: user request delete → all selfies deleted
- Access: admin yayasan + owner student only, not public
16.8 Pricing Impact
Added to Tier C scope:
- +Rp 22 juta (Selfie Camera + ML Kit + Clock In Success screens + backend + anti-fraud logic)
- New total: Rp 342 juta (was Rp 320 jt)
16.9 Related Deferred Features
Face enrollment (initial onboarding) — Tier 3 (post-MVP):
- Admin Lembaga captures staff reference photo during hiring onboarding
- Reference photo becomes face-match baseline
- Separate Admin screen: "Staff Face Enrollment"
Multi-factor fallback:
- If selfie fails 3x → fallback ke manual review (Kepala Sekolah approve via notification)
- Avoids locking out staff due to camera/lighting issues
Both features deferred to Phase 2 of project.
17. Multi-Role Support (Post-Selfie Addition)
17.1 Business Justification
Reality pesantren: 40-60% staff punya 2-4 role concurrent (Ustadz senior = Guru Mapel + Wali Kelas, Kepala Sekolah = also Guru reduced load, etc). Proper multi-role support critical, not nice-to-have.
17.2 Architecture — 2 Patterns Based on Data Overlap
Pattern A — Unified Dashboard (roles dengan HIGH data overlap):
- Guru + Wali Kelas (same kelas context)
- Guru + Pembimbing Hafalan (same santri pool)
- Kepala Sekolah + Guru (academic + teaching same lembaga)
Pattern B — Role Switcher (roles dengan LOW/ZERO data overlap):
- Guru + Bendahara (academic vs financial)
- Admin + Bendahara (zero workflow overlap)
- Kepala Yayasan + Kepala Sekolah (different scope)
17.3 Implementation Details
Unified Dashboard (Guru+WK — node 64:3)
Rebuilt screen dengan 2 sections:
- 📚 MAPEL SAYA (top section): Jadwal mengajar, Quick Actions (Input Nilai, Absensi Mapel, Materi Ajar)
- 🎯 KELAS SAYA (bottom section): Existing WK content preserved (KPI, Kehadiran, Tidak Hadir, Ranking)
- Role Chip top-right: "Guru + Wali Kelas ▾" (tap-able for switch)
Timing-based UX: Pagi-siang = top priority (Guru context), Sore-malam = bottom priority (WK context).
Role Chip Pattern
Applied to 6 dashboards:
- Guru Dashboard: "Guru · IPA ▾"
- WK Dashboard: "Wali Kelas · VII-A ▾"
- Bendahara Dashboard: "Bendahara · Lembaga ▾"
- Admin Lembaga Dashboard: "Admin · Al-Hikmah ▾"
- Bendahara Yayasan Dashboard: "Bendahara Yayasan ▾"
- Kepala Yayasan Dashboard: "Kepala Yayasan ▾"
Visual spec:
- Size: 150×28px
- Radius: 14px (pill shape)
- Position: top-right of header (inside HeaderBand or AppBar)
- On gradient header: white translucent (22% opacity)
- On white AppBar: emerald-50 bg + emerald-300 border + emerald-700 text
- Dropdown caret (▾) signals tap-able
- Conditional display: Only visible if
user.roles.length > 1
Ganti Role Menu (7 Profile Screens)
Added prominent menu item di profile screens:
- Guru / Profil
- WK / Profil
- Bendahara / Profil
- Admin / Profil Menu
- BendYayasan / Profil
- PembinaAsrama / Profil
- PembimbingHafalan / Profil
Visual spec:
- Card 380×72px with emerald-50 bg + emerald-300 border
- Icon chip emerald 44×44 dengan swap-arrows vector (white strokes)
- Title: "Ganti Role" bold emerald-dark
- Subtitle: "Berpindah peran aktif (Guru / Wali Kelas / Bendahara)"
- Chevron right
- Placed above standard profile sections (Akun, Preferensi, Lainnya)
17.4 Flutter Implementation Pattern
// lib/core/auth/role_manager.dart
class RoleManager {
final User user;
Role _activeRole;
bool shouldShowSwitcher() {
final roles = user.roles;
if (roles.length <= 1) return false;
// Unified pattern for compatible combinations
final unifiedCombos = [
{'guru', 'wali_kelas'},
{'guru', 'pembimbing_hafalan'},
{'wali_kelas', 'pembina_asrama'},
{'kepala_sekolah', 'guru'},
];
final roleSet = Set.from(roles);
for (final combo in unifiedCombos) {
if (roleSet.containsAll(combo) && roleSet.length == combo.length) {
return false; // Unified dashboard — skip switcher
}
}
return true; // Role switcher required
}
Widget dashboardForUser(User user) {
if (shouldShowSwitcher()) {
return RoleSwitcherScreen(user.roles);
}
if (user.hasRoles(['guru', 'wali_kelas'])) {
return GuruWKUnifiedDashboard();
}
if (user.hasRoles(['kepala_sekolah', 'guru'])) {
return KepalaSekolahReducedLoadDashboard();
}
return RoleSpecificDashboard(user.primaryRole);
}
}
// widgets/role_chip.dart
class RoleChip extends StatelessWidget {
final User user;
Widget build(BuildContext context) {
if (user.roles.length <= 1) return SizedBox.shrink();
return GestureDetector(
onTap: () => Navigator.pushNamed(context, '/role-switcher'),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.emeraldTint.withOpacity(0.22),
borderRadius: BorderRadius.circular(14),
),
child: Row(children: [
Text(user.activeRoleDisplay),
Icon(Icons.arrow_drop_down, size: 12),
]),
),
);
}
}
17.5 Notification Context Filtering
Added filter tabs pattern di Notifikasi Center (Kepala Yayasan as reference):
- "Semua (24)" — all notifications
- "Konsolidasi" — yayasan-scope only
- "Lembaga" — single lembaga scope only
Backend: each notification tagged dengan role_context: 'guru'|'wali_kelas'|'bendahara'|etc. Mobile filter by role_context == activeRole OR role_context IN unifiedCompatibleRoles.
17.6 Role Switcher Icon Upgrade
Replaced emoji icons dengan proper vector icons di 3 role cards:
- Teacher icon (filled person silhouette) → emerald for Guru Mapel
- Clipboard icon → blue for Wali Kelas
- Briefcase icon → amber for Anggota Komite Keuangan
Color-coded per role = instant visual recognition.
17.7 Pricing Impact
Added to Tier C scope:
- +Rp 12 juta total:
- Unified Dashboard rebuild: Rp 4 jt
- 7 Profile screens Ganti Role menu: Rp 2 jt
- Role chip di 6 dashboards: Rp 2 jt
- Role Switcher polish (icons + logic): Rp 1 jt
- Notification context filter: Rp 2 jt
- Multi-role testing QA: Rp 1 jt
New total: Rp 354 juta (was Rp 342 jt)
18. Jenjang-Specific Page Separation (TK + Diniyah)
18.1 Context
Pesantren Indonesia umumnya multi-jenjang: TK (PAUD), MI/MTs/MA (Formal Kurikulum Merdeka), Diniyah (kitab kuning + hafalan). Content differences signifikan (40-60% unique per jenjang) sehingga separation pages di Figma justified untuk design clarity.
18.2 Page Restructure
Before:
- Page 3: "Guru (Teacher)" — generic
- Page 4: "Wali Kelas" — generic
After:
- Page 3: "Guru (Formal)" — MI/MTs/MA Kurikulum Merdeka content
- Page 4: "Wali Kelas (Formal)" — MI/MTs/MA homeroom content
- NEW Page 3a: "Guru + WK (TK / PAUD)" — 6 screens pink palette
- NEW Page 3b: "Guru + WK (Diniyah)" — 6 screens brown/sepia palette
18.3 Page 3a — Guru + WK (TK / PAUD)
Palette: Pink (#ec4899 primary) — matches PAUD branding established earlier
Screens (6):
- Beranda — KPI + aktivitas tema hari ini + akses cepat
- Input Perkembangan STPPA — 6 aspek (Nilai Agama, Fisik-Motorik, Kognitif, Bahasa, Sosial-Emosional, Seni) dengan hearts rating + level badge (BB/MB/BSH/BSB)
- Progress Anak Detail — hexagonal radar chart + 6 detail cards
- Aktivitas Hari Ini — timeline dengan status (done/active/pending)
- Komunikasi Wali — chat list casual tone + FAB compose
- Profil — stats (18 anak, 6 aspek, 3 thn, 4.9 rating)
Bottom nav: Beranda / Perkembangan / Aktivitas / Komunikasi / Profil
18.4 Page 3b — Guru + WK (Diniyah)
Palette: Brown/sepia (#78350f primary) — kitab kuning parchment, distinct dari Pembimbing Hafalan purple
Accent: Emerald (Quranic highlight) + Amiri font untuk Arabic text
Screens (6):
- Beranda — Bismillah Arabic hero + KPI + Jadwal Hari Ini dengan waktu shalat context (Ba'da Ashar/Maghrib/Isya)
- Input Hafalan — Jenis tabs (Qur'an/Hadits/Kitab) + Surah picker + 4 kualitas rating (Kelancaran/Tajwid/Makharij/Tartil)
- Murajaah Tracker — summary 18/22 santri + filter + status per santri (done/partial/pending)
- Kitab Kuning Progress — 4 kitab cards (Safinah, Taqrib, Arba'in Nawawi, Aqidah al-Awam) dengan progress bar + ETA khataman
- Setoran Hari Ini — queue list dengan "Berikutnya" callout + 14 menunggu list
- Profil — stats (22 santri, 156 Juz total, 5 thn, 4.9 rating)
Bottom nav: Beranda / Hafalan / Kitab / Setoran / Profil
18.5 Rationale — Why NOT Separate Role Switcher Pages
User originally proposed 5 pages including "RS Guru+WK" dan "RS Guru+Bendahara". Declined this split karena:
- Role Switcher adalah SAME screen di runtime (Shared
495:35) dengan content adapts berdasarkanuser.roles - Terpisahnya page = tidak reflect runtime reality
- Duplicate content (same screen, different card data = maintenance nightmare)
Scenarios tetap di Shared Role Switcher dengan note di Doc 12 menjelaskan variant behaviors.
18.6 Flutter Implementation Pattern
// lib/core/jenjang/jenjang_router.dart
class JenjangRouter {
static Widget routeGuruDashboard(User user) {
final jenjang = user.primaryAssignmentJenjang;
switch (jenjang) {
case Jenjang.TK:
return GuruWKTKBeranda(); // Pink palette, PAUD STPPA
case Jenjang.Diniyah:
return GuruDiniyahBeranda(); // Brown palette, Kitab + Hafalan
case Jenjang.MI:
case Jenjang.MTs:
case Jenjang.MA:
default:
return GuruFormalBeranda(); // Default Kurikulum Merdeka
}
}
}
18.7 Pricing Impact
Added to Tier C+ scope:
- +Rp 22 juta total:
- 6 TK screens (Pink palette, STPPA workflows): Rp 11 jt
- 6 Diniyah screens (Brown palette, kitab kuning + hafalan): Rp 11 jt
- Flutter conditional routing logic: absorbed into existing architecture
New total: Rp 376 juta (was Rp 354 jt)
Change Log
- v1.0: Tier C scope addendum. Multi-tenant auth flow, Kepsek→Kepala Yayasan refactor, 5 new personas, 3 P2 modules. 152 screens total.
- v1.1: Tier C+ revisions. Kepala Sekolah / Kepala Yayasan split. Vector icon library + 360 emoji replacements. Bottom nav standardization. 161 screens. Rp 245 juta.
- v1.2: Tier C++ professional review fixes. Icon library expanded to 37 icons. Shalat picker + hero moon + progress ring rebuilt. Status pill semantic harmonization (5 colors → 3 semantic). Per-kamar severity scaling. Kegiatan icon disambiguation. UX principles documented. Rp 252 juta.
- v1.3 (today): Akses Cepat canonical alignment to Parent pattern (88×84 tiles, white inner chip 22×22). Shalat picker icons rebuilt with celestial-position metaphor. Section 13 added: Material Symbols mapping table for all icon usage — Flutter dev should use Material Symbols (or Phosphor Icons) instead of custom shape-composed vectors. Custom Figma icons documented as communication artifacts; production should use icon font library.