Konsep Aplikasi

 # Schoolmantic — Konsep Aplikasi (Revisi)


Aplikasi absensi sekolah berbasis SaaS multi-tenant. Target awal: 50 sekolah, 300–1000 siswa per sekolah. Versi ini sudah memasukkan resolusi konflik dan skema baru hasil review.

---

## 1. Tech Stack

- **Backend**: Hono + Bun + Drizzle ORM (TypeScript)
- **Web**: Nuxt 4 + Vue 3 (TypeScript)
- **Mobile**: Flutter — **satu app** untuk semua role (UI berubah sesuai role setelah login)
- **Database**: PostgreSQL — **single database, single set of tables**, isolasi tenant via kolom `school_id` + middleware guard
- **File storage**: S3 (di dev pakai MinIO)
- **Push notification**: Firebase Cloud Messaging (FCM)
- **Bahasa komunikasi**: Indonesia
- **OS dev**: Windows 11

Semua data diambil dari database. Tidak ada data tiruan di kode.

---

## 2. Domain & Aplikasi

| Aplikasi | URL / Channel |
|---|---|
| Landing page | `www.schoolmantic.com` |
| Dashboard web (semua role) | `app.schoolmantic.com` (tidak ada subdomain per sekolah) |
| Mobile | 1 Flutter app, role-aware |

API: prefix `/api/v1` sejak awal.

---

## 3. Model Identitas (Unified User)

**Resolusi konflik**: konsep awal menyebut pelanggan login pakai email dan pegawai pakai HP, tapi juga membolehkan pelanggan sekaligus jadi pegawai. Itu kontradiktif. Sekarang dipakai **satu tabel `users` untuk semua orang**.

- 1 baris `users` per orang. Field `email` (nullable) + `phone_e164` (nullable) — minimal salah satu wajib.
- User boleh login pakai email **atau** HP.
- Role/peran ditentukan dari **`school_memberships`** (lihat §6), bukan dari kolom di `users`.
- Email & HP **unik global** (lintas sekolah).
- Format HP disimpan E.164 (`+62…`); helper konversi `08…``+628…` di service layer.

### Tingkatan user (peran)
1. **SaaS owner** — flag `users.is_saas_owner = true`. Lihat semua pelanggan, approve pembayaran, aktifkan/nonaktifkan.
2. **Pelanggan (OWNER)** — punya minimal 1 sekolah. Bisa punya banyak sekolah tanpa batas.
3. **Pegawai (EMPLOYEE)** — staf atau guru. Bisa di-flag sebagai school admin, wali kelas, guru BK, bendahara.
4. **Wali siswa (GUARDIAN)** — bisa punya banyak siswa di sekolah berbeda.
5. **Siswa (STUDENT)** — bisa punya banyak wali.

Satu user bisa punya **beberapa peran sekaligus** (mis. pelanggan yang juga jadi guru di sekolahnya sendiri). Setelah login, user dengan banyak membership melihat **tenant selector**, kecuali wali (tidak punya selector — lihat §6.2).

---

## 4. Auth & Sesi

- Hash password: **argon2id**.
- JWT akses 15 menit + refresh token 7 hari (rotation, disimpan di `refresh_tokens` dengan `revoked_at`).
- Setelah login, response berisi `tenant_options[]` untuk user multi-membership. Frontend kirim `X-School-Id` di setiap request → middleware verifikasi membership aktif.
- **Tidak ada verifikasi** email/HP saat registrasi pelanggan (langsung aktif).
- Pegawai/siswa/wali siswa tidak register sendiri — di-input oleh school admin atau pelanggan. Password awal di-set admin; user di-paksa ganti di first login (flag `must_change_password`).
- Rate limit: per-IP untuk login, per-user untuk API.

---

## 5. Alur Registrasi

Hanya **pelanggan** yang register sendiri:
1. Input email, HP, password.
2. Sekaligus input nama sekolah, tingkat sekolah, alamat.
3. Sistem otomatis: bikin `users` (status aktif), `schools`, `school_memberships(member_type=OWNER)`, dan `school_subscriptions(status=TRIAL, trial_ends_at = now + 30 hari)`.

Pelanggan bisa tambah sekolah lain kapan saja dari dashboard, sekaligus pilih paketnya.

---

## 6. Multi-Tenant: School Memberships

### 6.1 Tabel inti
- `school_memberships(id, school_id, user_id, member_type ENUM[OWNER, EMPLOYEE, GUARDIAN, STUDENT], started_at, ended_at)`
- `employee_profiles(membership_id, employee_type ENUM[STAFF, TEACHER], nip, is_school_admin, is_homeroom_teacher, is_bk, is_treasurer, …)` — bendahara hanya skema, fitur belum.
- `student_profiles(membership_id, nis, gender, birth_date, photo_key, …)` — kelas tidak di sini, lihat §7.
- `guardian_student(guardian_membership_id, student_membership_id, relation, is_primary)` — m:n, primary kontak untuk notifikasi.

Catatan: pelanggan bukan tabel terpisah — pelanggan = user yang punya membership `OWNER`.

### 6.2 Wali multi-sekolah
Wali bisa punya anak di beberapa sekolah dalam 1 database.
- Dashboard wali **tidak punya `current_school_id`** — menampilkan agregat semua anak.
- Query untuk wali selalu lewat `student_membership_id` yang dimiliki.
- Izin/dokumen tetap di-scope ke sekolah anak yang relevan.

### 6.3 Tenant guard (Hono middleware)
- Helper `requireSchoolContext(c)`: ambil `X-School-Id` → cek membership aktif → set `c.set('schoolId', …)`.
- Repository wrapper: setiap query Drizzle ke tabel tenant-scoped wajib lewat helper yang inject `WHERE school_id = ?`.
- Tabel non-tenant (whitelist eksplisit): `users`, `subscription_plans`, `audit_logs` (school_id nullable untuk aksi SaaS owner), `refresh_tokens`.
- Tidak pakai PostgreSQL RLS.

---

## 7. Sekolah

Tabel `schools` minimal punya:
- `id`, `name`, `level ENUM[SD, SMP, SMA, SMK, BIMBEL, OTHER]`, `address`
- `timezone TEXT NOT NULL DEFAULT 'Asia/Jakarta'`**per-sekolah** karena Indonesia ada WIB/WITA/WIT
- `gps_lat`, `gps_lng`, `gps_radius_meters` — untuk verifikasi selfie pegawai
- `default_late_threshold_minutes`
- `notifications_enabled BOOLEAN`
- `notification_default_channels TEXT[]` — kombinasi `PUSH`, `WHATSAPP`, `TELEGRAM`, atau kosong (tanpa notifikasi)
- `created_at`, `updated_at`, `deleted_at`

**Boundary "hari"** untuk laporan & batas terlambat dihitung di TZ sekolah, bukan UTC.

---

## 8. Tahun Akademik, Semester, Kelas

Berbeda per sekolah:
- `academic_years(id, school_id, name, start_date, end_date, is_active)`
- `semesters(id, academic_year_id, name, start_date, end_date)`
- `classes(id, school_id, name, level, …)`
- `student_class_enrollments(id, student_membership_id, class_id, academic_year_id, semester_id?, started_at, ended_at)` — supaya siswa bisa pindah kelas antar tahun/semester tanpa kehilangan history.

Wali kelas (homeroom) ditandai di `employee_profiles.is_homeroom_teacher = true` dan dihubungkan ke kelas via assignment (atau kolom `homeroom_class_id` di `employee_profiles` — pilih saat implementasi).

---

## 9. Mata Pelajaran & Penugasan Mengajar

- `subjects(id, school_id, name, code)`
- `teaching_assignments(id, teacher_membership_id, class_id, subject_id, semester_id, …)`

---

## 10. Jadwal (Template + Override)

### 10.1 Template
Konsep grup-jadwal: jadwal A untuk kelas 1–3, jadwal B untuk kelas 4–6, dst.
- `schedule_templates(id, school_id, name, …)`
- `schedule_template_slots(id, template_id, day_of_week, time_in, time_out, late_threshold_minutes)` — late threshold bisa di-override per slot.
- `class_schedule_assignments(id, class_id, template_id, effective_from, effective_to)`
- `employee_schedule_assignments(id, employee_membership_id, template_id, effective_from, effective_to)`

### 10.2 Override (resolusi konflik)
Konsep awal menyebut "jadwal bisa menimpa jadwal lain" tapi tidak ada tabel.
- `schedule_overrides(id, school_id, scope ENUM[SCHOOL, CLASS, EMPLOYEE], target_id?, date_from, date_to, type ENUM[LIBUR, UJIAN, JAM_KHUSUS], replacement_template_id?, note, priority)`
- Engine resolusi jadwal: cari override aktif → kalau tidak ada → pakai template default kelas/pegawai.

### 10.3 Hari libur
- `holidays(id, school_id, date, name, is_recurring)` untuk libur nasional/sekolah.

---

## 11. Absensi

### 11.1 Status (revisi)
Status disimpan di `attendance_daily_*`:
- `HADIR`, `TERLAMBAT`, `IZIN`, `SAKIT`, `ALPHA` (status awal dari konsep)
- `LIBUR` — supaya hari libur tidak terhitung alpha di laporan **(baru)**
- `DISPENSASI` — pulang lebih awal yang sah **(baru)**

Status `IZIN` di-set otomatis ketika `leave_requests` di-approve.

### 11.2 Tabel
- `attendance_daily_students(id, school_id, student_membership_id, class_id, date, status, time_in?, time_out?, late_minutes, schedule_template_id, source ENUM[BIOFINGER, MANUAL, FACE], recorded_by_user_id?, …)`
- `attendance_daily_employees(id, school_id, employee_membership_id, date, status, time_in?, time_out?, …, source ENUM[BIOFINGER, SELFIE, MANUAL, FACE], selfie_photo_key?, gps_lat?, gps_lng?, gps_accuracy_m?)`
- `attendance_events(id, school_id, subject_type ENUM[STUDENT, EMPLOYEE], subject_membership_id, event_at, event_type ENUM[IN, OUT], source, raw_payload JSONB)`**append-only** raw event dari biofinger/selfie. Aturan "1 baris/hari" tetap berlaku untuk `attendance_daily_*`; `attendance_events` adalah jejak audit.
- `attendance_devices(id, school_id, name, vendor, serial_number, location, last_seen_at)` — daftar mesin biofinger.
- `face_embeddings(id, school_id, subject_type, subject_membership_id, embedding BYTEA, model_version, created_at)` — siapkan walau belum dipakai.

### 11.3 Sumber absensi
- **Siswa**: mesin biofinger, manual oleh wali kelas, atau face recognition (skema disiapkan).
- **Pegawai**: mesin biofinger, atau **selfie pakai HP Android pribadi**. Selfie **wajib di radius GPS sekolah** (`gps_radius_meters`); ditolak kalau di luar.
- Selfie disimpan di S3 dengan key bertenant. Koordinat & accuracy disimpan di kolom record absensi (bukan exif).

### 11.4 Hak input absensi
- **Wali kelas**: hanya kelas yang dia ampu.
- **School admin**: semua kelas di sekolahnya.
- **Pegawai biasa**: tidak bisa input absensi orang lain.

---

## 12. Izin (Perizinan)

- `leave_requests(id, school_id, subject_type ENUM[STUDENT, EMPLOYEE], subject_membership_id, type ENUM[IZIN, SAKIT, DISPENSASI], date_from, date_to, reason, document_key?, status ENUM[PENDING, APPROVED, REJECTED, CANCELLED], submitted_by_user_id, approved_by_user_id?, approved_at?, rejection_reason?)`
- Wali siswa input izin untuk anaknya. Approval oleh **wali kelas atau school admin**.
- Upload dokumen/gambar opsional (`document_key` di S3).
- Saat status APPROVED → service sinkronisasi ke `attendance_daily_*` di rentang tanggal terkait.

---

## 13. Notifikasi (Outbox Pattern)

Channel: `PUSH` (FCM), `WHATSAPP`, `TELEGRAM`, atau **tanpa notifikasi**. Bisa kombinasi (multi-channel).

- `notification_templates(id, school_id, code, name, channels TEXT[], subject?, body_template, variables JSONB)` — code contoh: `STUDENT_PRESENT`, `STUDENT_LEFT`, `STUDENT_ABSENT`, `LEAVE_APPROVED`, dll.
- `notifications_outbox(id, school_id, template_code, channel, recipient_user_id?, recipient_phone?, payload JSONB, status ENUM[PENDING, SENT, FAILED, CANCELLED], attempts, last_error, scheduled_at, sent_at)`
- `device_push_tokens(id, user_id, token, platform ENUM[ANDROID, IOS, WEB], last_seen_at)`

Aplikasi WA/Telegram di luar project ini akan polling `notifications_outbox WHERE status='PENDING' AND channel IN (...)` lalu update status.

---

## 14. Berlangganan & Billing

Paket per-sekolah (pelanggan dengan banyak sekolah bisa pilih paket berbeda per sekolah).

| Code | Fitur | Harga |
|---|---|---|
| `WHATSAPP_ONLY` | Web + Notifikasi WhatsApp | Rp 3.000 / siswa / bulan |
| `SMS_WHATSAPP` | Web + SMS + WhatsApp | Rp 5.000 / siswa / bulan |
| `BIMBEL_FLAT` | Web + WhatsApp (berapapun siswa) | Rp 300.000 / bulan |

### Tabel
- `subscription_plans(id, code, name, price_per_student_monthly?, flat_price_monthly?, features JSONB, is_active)`
- `school_subscriptions(id, school_id, plan_id, status ENUM[TRIAL, ACTIVE, GRACE, SUSPENDED, CANCELLED], trial_ends_at, current_period_start, current_period_end, billing_student_count?)`
- `payments(id, school_id, subscription_id, amount, currency, paid_at?, proof_file_key?, status ENUM[SUBMITTED, APPROVED, REJECTED], reviewed_by_user_id?, reviewed_at?)`

### Aturan
- Trial **30 hari** otomatis saat sekolah baru dibuat.
- Setelah trial habis: TRIAL → GRACE 7 hari → SUSPENDED (read-only).
- SaaS owner approve/reject `payments`.
- Billing siswa: pakai `school_active_student_count` (view/materialized) per bulan.

---

## 15. File Storage S3

Konvensi key dengan prefix tenant:
```
schools/{school_id}/students/{membership_id}/photo.jpg
schools/{school_id}/employees/{membership_id}/selfie/{date}/{uuid}.jpg
schools/{school_id}/leave-docs/{leave_id}/{filename}
schools/{school_id}/payments/{payment_id}/proof.{ext}
```

- Upload via **presigned URL** (backend yang generate setelah cek otorisasi); Browser/Flutter PUT langsung ke S3.
- Validasi server-side: max size & mime-type whitelist (`image/jpeg`, `image/png`, `application/pdf` untuk dokumen izin).
- Selfie pegawai: lifecycle policy S3 hapus otomatis setelah 1 tahun.

---

## 16. Audit Log

Satu tabel generik:
`audit_logs(id, school_id?, actor_user_id?, action, entity_type, entity_id, before JSONB?, after JSONB?, ip, user_agent, created_at)`

Yang **wajib** di-audit:
- `users` (create, password change, role change, deactivate)
- `school_memberships` (create, role/flag change, terminate)
- `employee_profiles.is_school_admin / is_homeroom_teacher / is_bk / is_treasurer` change
- Edit manual `attendance_daily_*` (koreksi)
- `leave_requests` approve/reject
- `school_subscriptions` & `payments` (semua perubahan status)
- `schools` settings (notifications_enabled, GPS, late threshold default, dll.)
- Soft delete master data (siswa, pegawai, kelas)

---

## 17. Soft Delete & Unique Constraints

Semua master data pakai `deleted_at TIMESTAMPTZ NULL` (soft delete). Untuk hindari konflik unique:

```sql
-- contoh
CREATE UNIQUE INDEX ON users (lower(email)) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX ON users (phone_e164) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX ON student_profiles (school_id, nis) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX ON employee_profiles (school_id, nip) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX ON attendance_daily_students (school_id, student_membership_id, date) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX ON attendance_daily_employees (school_id, employee_membership_id, date) WHERE deleted_at IS NULL;
```

---

## 18. Index Krusial

- `attendance_daily_students(school_id, class_id, date)` — laporan kelas/hari
- `attendance_events(school_id, subject_type, subject_membership_id, event_at)`
- `notifications_outbox(status, scheduled_at) WHERE status='PENDING'` — partial, supaya worker poll cepat
- `school_memberships(user_id) WHERE ended_at IS NULL`
- `school_memberships(school_id, member_type) WHERE ended_at IS NULL`
- `audit_logs(school_id, entity_type, entity_id, created_at DESC)`

---

## 19. Dashboard per Role

### SaaS owner
Daftar pelanggan + aktifkan/nonaktifkan, daftar pembayaran + approve/reject, statistik global.

### Pelanggan
- Tabel sekolah (jumlah pegawai, jumlah siswa, hadir hari ini per kategori)
- Tambah sekolah + pilih paket
- Tambah school admin (flag pegawai)

### Pegawai biasa
Lihat absensi dirinya, jadwal mengajar (kalau guru).

### School admin
CRUD penuh: pegawai, kelas, siswa+wali, mata pelajaran, jadwal pelajaran, jadwal mengajar, jadwal absensi (template+override), hari libur, assignment template ke kelas/pegawai, input absensi semua kelas, perizinan, template notifikasi, setting jadwal.

### Wali kelas
Input absensi hanya kelas yang dia ampu, approve izin siswa kelasnya.

### Wali siswa
- Lihat rekap absensi anak-anaknya (bisa lintas sekolah)
- Input izin anak (upload dokumen opsional)

### Siswa
Read-only: rekap absensi & izin dirinya.

---

## 20. Laporan & Export

- Rekap absensi per kelas per bulan — export PDF & Excel
- Rekap per siswa (bahan raport)
- Statistik dashboard sekolah (grafik bulanan, perbandingan kelas)

---

## 21. Privasi & Retensi (UU PDP)

- Endpoint export data per user
- Endpoint hapus data per user (soft delete + redaksi PII di audit log)
- Selfie pegawai retention 1 tahun
- Backup harian pg_dump, retention 30 hari (di-handle infra)

---

## 22. Landing Page

- URL: `www.schoolmantic.com`
- Style: responsif penuh, animasi, card warna-warni futuristik, background terang
- Kontak WhatsApp: 62811945222
- Alamat:
  ```
  CV POLMANTIC MEDIA CITRA
  JL FLAMBOYAN NO 28, PERUM DUTA KRANJI, BINTARA — BEKASI BARAT
  KOTA BEKASI
  ```
- Pricing card sesuai §14.

---

## 23. Seed Data (1 sekolah contoh)

Sesuai permintaan, seed mencakup:
- 1 sekolah (TZ `Asia/Jakarta`, GPS area Bekasi, paket `WHATSAPP_ONLY` status TRIAL)
- 1 pelanggan (OWNER) + 1 school_admin (pegawai dgn flag `is_school_admin`)
- 10 guru (3 wali kelas, 1 BK, 1 bendahara — bendahara skema saja)
- 2 kelas (mis. "Kelas 1A", "Kelas 1B"), masing-masing punya wali kelas
- 50 siswa (25 per kelas) + masing-masing 1 wali siswa
- 10 mata pelajaran
- 1 academic_year aktif + 2 semesters
- 1 schedule_template (Senin–Jumat 07:00–14:00, late threshold 15 menit) di-assign ke kedua kelas
- 5 holidays (libur nasional contoh)
- 3 notification_templates: `STUDENT_PRESENT`, `STUDENT_LEFT`, `STUDENT_ABSENT` (channel `WHATSAPP` + `PUSH`)

---

## 24. Verifikasi End-to-End (Definition of Done)

1. **Multi-tenant isolation** — admin sekolah A request data sekolah B → 403.
2. **Multi-role user** — 1 user owner di sekolah X & guru di sekolah Y, tenant selector menampilkan dua-duanya.
3. **Wali multi-school** — dashboard wali agregat anak dari 2 sekolah tanpa switch tenant.
4. **Soft delete + reuse NIS** — hapus siswa NIS=001, buat baru NIS=001 → sukses.
5. **TZ per sekolah** — sekolah TZ `Asia/Makassar`, `attendance_daily.date` ikut tanggal lokal sekolah.
6. **Schedule override** — kelas 1A libur tanggal X → status LIBUR (bukan ALPHA), kelas 1B normal.
7. **Selfie GPS** — pegawai selfie di luar radius → ditolak.
8. **Trial expiry** — TRIAL → GRACE 7 hari → SUSPENDED.
9. **Notification outbox** — leave_request APPROVED → baris di `notifications_outbox` sesuai channel sekolah.
10. **Audit log** — flag `is_school_admin` berubah → muncul di `audit_logs` dengan before/after.
11. **Login dual identity** — login pakai email atau HP, hash sama.
12. **Bio-finger raw event** — 3 event di hari sama → 3 baris `attendance_events`, 1 baris `attendance_daily` (time_in event pertama, time_out event terakhir).

---

## 25. Ringkasan Keputusan

| # | Pertanyaan | Keputusan |
|---|---|---|
| 1 | Pelanggan login di mobile | Ya |
| 2 | Face recognition | Siswa & pegawai (skema disiapkan, fitur menyusul) |
| 3 | Storage foto/file | S3 |
| 4 | Notifikasi multi-channel | Ya (PUSH + WA + Telegram, kombinasi bebas) |
| 5 | Registrasi wali siswa & siswa | Oleh school admin |
| 6 | Perizinan perlu approval | Ya (wali kelas / school admin) |
| 7 | Export laporan PDF/Excel | Ya |
| 8 | Offline mode Flutter | Tidak |
| 9 | Subdomain per sekolah | Tidak |
| 10 | Tahun akademik ada semester | Ya |
| 11 | Identitas user | Unified (1 user multi-role) |
| 12 | Time zone | Per-sekolah (default `Asia/Jakarta`) |
| 13 | Paket berlangganan | Per-sekolah |
| 14 | Mobile app | 1 Flutter app role-aware |
| 15 | Tenant guard | Middleware Hono (bukan RLS) |
| 16 | Verifikasi register pelanggan | Tidak ada |
| 17 | Trial period | 30 hari |
| 18 | Selfie pegawai GPS | Wajib dalam radius sekolah |

Comments

Popular posts from this blog

Roadmap