Like 0
HANDOVER_FE_DAILY_ACTIVITY_REMINDER.md Raw

Handover FE — Daily Activity: TASK vs REMINDER

Status: backend selesai.

Repo Commit
module-contracts 0863240 feat(notification): add daily-activity reminder event key
modules_base 862bafd feat(daily-activities): add reminder type with division notification
modules_base 8dcaa4f chore(contracts): bump @module/contracts to 0863240
module-notification 0249405 feat(email): add daily-activity reminder template
module-notification 9bfc4b9 chore(contracts): bump @module/contracts to 0863240

Migration: prisma/migrations/20261109101500_v1_daily_activity_reminder dan prisma/migrations/20261109120000_v1_daily_activity_assignment_notified.


1. Ringkasan

Daily activity sekarang punya dua tipe:

  • TASK — perilaku lama. Punya window startTime + endTime, boleh link ke action plan. Tidak mengirim notifikasi.
  • REMINDER — satu titik waktu (reminderTime), tanpa window. Saat waktunya tiba, backend mengirim notifikasi in-app + push + email ke tim project dari division guest yang dipilih.

Keduanya muncul di kalender weekly/monthly yang sama. Bedanya hanya kolom waktu: task = rentang, reminder = satu jam.

Default type kalau tidak dikirim = TASK, jadi payload lama tetap valid apa adanya.


2. Yang berubah untuk FE

Hal Dulu Sekarang
type tidak ada "TASK" | "REMINDER", default TASK
startTime / endTime wajib wajib hanya untuk TASK; null untuk reminder
reminderTime tidak ada wajib untuk REMINDER
reminderSentAt tidak ada kapan reminder terkirim; null = masih pending
startTimeWib / endTimeWib / reminderTimeWib tidak ada string ISO dengan offset +07:00 — pakai ini untuk print jam
divisionGuestId opsional wajib untuk REMINDER (audiens notifikasi)
Grouping hari di kalender server-local WIB (+07:00)
Kontrak report PDF — tidak berubah

startTime/endTime/reminderTime (Date UTC) tetap dikirim dengan bentuk lama. Field *Wib adalah tambahan, bukan pengganti.


3. Endpoint

Tidak ada endpoint baru. Semua di bawah project path:

POST   /api/projects/:projectId/daily-activities
GET    /api/projects/:projectId/daily-activities
GET    /api/projects/:projectId/daily-activities/:dailyActivityId
PATCH  /api/projects/:projectId/daily-activities/:dailyActivityId
DELETE /api/projects/:projectId/daily-activities/:dailyActivityId

Authz: membership project dengan role MEMBER / OWNER / ADMIN.

POST / PATCH / DELETE memakai ProjectNotClosedGuard: 403 Daily activities are read-only: the project is closed kalau status project COMPLETED.

Semua response dibungkus envelope standar (statusCode, message, data, durationMs, _reference) seperti endpoint lain. Contoh di §6 dan §7 menampilkan isi data saja.


4. Create

4a. TASK

POST /api/projects/:projectId/daily-activities
Content-Type: application/json
{
  "title": "Kunjungan site Gedung A",
  "type": "TASK",
  "startTime": "2026-11-09T01:00:00.000Z",
  "endTime": "2026-11-09T03:00:00.000Z",
  "note": "Cek bekisting lantai 3",
  "visibility": "PUBLIC",
  "divisionGuestId": "uuid-division-guest",
  "actionPlans": [{ "scopeType": "TASK", "itemId": "uuid-item" }]
}

type boleh dihilangkan — hasilnya TASK.

4b. REMINDER

{
  "title": "Meeting koordinasi MEP",
  "type": "REMINDER",
  "reminderTime": "2026-11-09T01:00:00.000Z",
  "note": "Bawa dokumen shop drawing",
  "visibility": "PUBLIC",
  "divisionGuestId": "uuid-division-guest"
}

4c. Aturan validasi (400)

Kondisi Pesan
TASK tanpa startTime startTime is required
TASK tanpa endTime endTime is required
TASK dengan endTime <= startTime endTime must be after startTime
TASK mengirim reminderTime A task takes no reminderTime
REMINDER tanpa reminderTime reminderTime is required for a reminder
REMINDER mengirim startTime/endTime A reminder takes no startTime/endTime
REMINDER tanpa divisionGuestId A reminder needs a division guest to notify
divisionGuestId bukan guest di workspace project Division is not in this workspace catalog

actionPlans pada reminder tidak dilarang oleh DTO, tapi tidak berguna — reminder tidak punya progress dan report menampilkan action plan kosong. Sembunyikan field itu di form reminder.


5. Update

PATCH /api/projects/:projectId/daily-activities/:dailyActivityId

Sifatnya partial: field yang tidak dikirim tidak berubah. Yang perlu diperhatikan:

  1. Setengah window boleh. Kirim startTime saja sah; validasi final memakai gabungan nilai tersimpan + payload.
  2. Ganti type harus lengkap. TASK → REMINDER wajib menyertakan reminderTime (dan guest sudah ada di row, boleh tidak dikirim ulang). REMINDER → TASK wajib menyertakan startTime + endTime.
  3. Kolom bentuk lama dibersihkan otomatis. Switch ke REMINDER mengosongkan startTime/endTime, dan sebaliknya.
  4. divisionGuestId: null menghapus guest, tapi akan ditolak kalau hasil akhirnya REMINDER.
  5. actionPlans = full replace. Kalau dikirim, daftar lama dihapus dan diganti isi payload. Kirim [] untuk mengosongkan.
  6. Reschedule hanya saat jam berubah. Mengubah reminderTime (atau switch masuk ke REMINDER) membatalkan job lama, menjadwalkan yang baru, dan mereset reminderSentAt ke null sehingga reminder bisa terkirim lagi. Mengubah title/note saja tidak mengirim ulang reminder yang sudah terkirim.

Hanya creator atau OWNER/ADMIN workspace yang boleh edit: 403 Only the creator or a workspace admin can edit a daily activity.


6. Response shape

POST / GET / PATCH mengembalikan objek yang sama (isi data):

{
  "id": "uuid",
  "projectId": "uuid-project",
  "title": "Meeting koordinasi MEP",
  "type": "REMINDER",
  "startTime": null,
  "endTime": null,
  "reminderTime": "2026-11-09T01:00:00.000Z",
  "reminderSentAt": null,
  "startTimeWib": null,
  "endTimeWib": null,
  "reminderTimeWib": "2026-11-09T08:00:00.000+07:00",
  "note": "Bawa dokumen shop drawing",
  "visibility": "PUBLIC",
  "createdBy": "uuid-user",
  "updatedBy": "uuid-user",
  "createdAt": "2026-11-09T00:12:00.000Z",
  "updatedAt": "2026-11-09T00:12:00.000Z",
  "createdByUser": { "id": "uuid-user", "name": "Budi", "avatarUrl": null },
  "updatedByUser": { "id": "uuid-user", "name": "Budi", "avatarUrl": null },
  "divisionGuestId": "uuid-division-guest",
  "divisionGuest": { "id": "uuid-division-guest", "name": "Struktur" },
  "actionPlanLinks": []
}

Contoh task:

{
  "type": "TASK",
  "startTime": "2026-11-09T01:00:00.000Z",
  "endTime": "2026-11-09T03:00:00.000Z",
  "reminderTime": null,
  "startTimeWib": "2026-11-09T08:00:00.000+07:00",
  "endTimeWib": "2026-11-09T10:00:00.000+07:00",
  "reminderTimeWib": null
}

Aturan render:

  • Print jam dari *Wib (new Date(x).getHours() di browser lokal user bisa beda timezone — *Wib sudah membawa offset, atau ambil substring 11:00), bukan dari startTime mentah.
  • type === 'REMINDER' → tampilkan satu jam + ikon/label reminder. reminderSentAt !== null = sudah terkirim.
  • actionPlanLinks[].itemName adalah snapshot nama item saat ditulis — tetap ada walau item action plan-nya sudah dihapus.

7. Kalender

GET /api/projects/:projectId/daily-activities?view=weekly&month=2026-11&visibility=PUBLIC&search=meeting
Query Default Keterangan
view weekly weekly | monthly
month bulan berjalan (WIB) YYYY-MM
visibility semua filter PUBLIC / PRIVATE
search — contains pada title

Response (isi data):

{
  "view": "monthly",
  "start": "2026-10-31T17:00:00.000Z",
  "end": "2026-12-05T16:59:59.999Z",
  "days": [
    { "date": "2026-11-01", "activities": [] },
    { "date": "2026-11-02", "activities": [ /* ... */ ] }
  ]
}

Perilaku yang perlu diketahui:

  • date adalah hari kalender WIB (YYYY-MM-DD), bukan hari lokal server. Aktivitas jam 22:00 WIB tidak akan lompat ke hari berikutnya.
  • weekly mengembalikan hanya hari milik bulan itu (FE yang menyusun baris minggu).
  • monthly mengembalikan grid penuh ala Google Calendar: hari dari bulan sebelum/sesudah ikut, minimal 35 sel (42 untuk bulan yang butuh 6 baris).
  • Hari kosong tetap ada dengan activities: [] — FE tidak perlu mengisi gap sendiri.
  • Urutan activities dalam satu hari = urutan waktu efektif (startTime untuk task, reminderTime untuk reminder). Tidak perlu sort ulang.
  • start/end adalah window yang benar-benar di-query (WIB midnight s/d 23:59:59.999), dikirim sebagai instant UTC.

Baris PRIVATE hanya terlihat oleh creator-nya atau OWNER/ADMIN workspace; row yang tidak boleh dilihat hilang dari response (bukan null).


8. Notifikasi

Dikirim otomatis oleh backend saat reminderTime tiba. FE tidak perlu memanggil apa pun.

Aspek Perilaku
Audiens seluruh anggota project team (status active) dari semua project aktif yang division_id = division guest yang dipilih
Kanal in-app + push + email, mengikuti preferensi notifikasi masing-masing user (default in-app aktif)
Event key base.daily_activity.reminder
Cakupan satu kali per reminder
reminderSentAt diisi saat terkirim, juga saat audiensnya kosong

Payload in-app/push reminder:

{
  "type": "info",
  "title": "Daily Activities Reminder",
  "message": "[Budi]: Please send the MoM document by 5 PM at the latest.",
  "data": {
    "action": "daily_activity.reminder",
    "activityId": "uuid",
    "projectId": "uuid-project",
    "reminderTime": "2026-11-09T08:00:00.000+07:00"
  }
}

Copy mengikuti desain: [Sender Username]: <note>

  • Sender Username = display name pembuat daily activity (createdBy).
  • <note> = field note; kalau kosong dipakai title supaya tidak ada pesan kosong.
  • Kalau auth tidak bisa me-resolve nama pembuat, prefix [...]: dihilangkan (isi note tetap terkirim), dan email jatuh ke sapaan berbasis email.

8b. Assignment

Dikirim sekali, tepat saat reminder dibuat (bukan saat waktunya tiba), ke admin project — yaitu project_teams.role = project-owner atau admin — dari semua project yang division_id = division guest.

{
  "type": "info",
  "title": "Assignment Daily Activities",
  "message": "[Andi] has assign you to the daily activities. On 09 Nov 2026 08:00.",
  "data": {
    "action": "daily_activity.assigned",
    "activityId": "uuid",
    "projectId": "uuid-project",
    "reminderTime": "2026-11-09T08:00:00.000+07:00"
  }
}
  • [Date] = tanggal + jam reminderTime pada wall clock WIB, format 09 Nov 2026 08:00.
  • Nama pembuat tidak ter-resolve → [Someone].
  • Kanal: in-app + push + email, mengikuti preferensi tiap user (default in-app saja).
  • Event key terpisah: base.daily_activity.assigned (reminder: base.daily_activity.reminder), jadi user bisa mematikan salah satu tanpa mematikan yang lain.
  • Dikirim ulang hanya kalau reminderTime berubah (PATCH dengan jam baru). Dedupe per instant lewat kolom assignment_notified_for, ditulis dalam transaksi yang sama dengan outbox row — retry/clone tidak menghasilkan notifikasi kedua, dan guest berubah tanpa jam berubah tidak memicu kirim ulang.

Catatan penting:

  1. Audiens = divisi, bukan project. Kalau satu divisi punya beberapa project, anggota project lain di divisi yang sama juga menerima notifikasi.
  2. Tidak ada notifikasi kalau division guest kosong — karena itu reminder tanpa guest ditolak saat create/update.
  3. Tidak ada repeat / recurring. Satu reminder = satu kali kirim.
  4. Tidak ada endpoint batal. Menghapus reminder (DELETE) membatalkan job-nya.
  5. Kirim ulang hanya bisa dengan mengubah reminderTime (atau switch type keluar-masuk REMINDER) — itu mereset reminderSentAt.
  6. Anggota tim yang belum punya akun (user_id null) tidak menerima notifikasi in-app/push.
  7. Email memakai baris yang sama (bodyText) sebagai isi pesan, jadi teks di inbox = teks in-app.

9. Report PDF

Kontrak API report tidak berubah. Di dalam PDF, reminder muncul di tabel Daily Activities dengan:

  • kolom Time: 08:00 (reminder) (WIB)
  • kolom Action plan: —
  • reminder ikut terhitung dalam window periode berdasarkan reminderTime
  • urutan dalam satu hari mengikuti jam efektif, jadi reminder berselang-seling dengan task — bukan menumpuk di atas

10. Ringkasan error

HTTP Kasus
400 semua aturan bentuk di §4c
403 project COMPLETED (write), bukan creator/admin (update/delete)
404 daily activity tidak ditemukan di project itu

11. Checklist FE

  • Form create punya pemilih tipe; pilih TASK → field window, pilih REMINDER → field satu jam + division guest wajib.
  • Field startTime/endTime di form TASK dikirim dalam UTC ISO (atau offset apapun, backend pakai z.coerce.date).
  • Form reminder menyembunyikan pemilih action plan.
  • Kalender membaca jam dari reminderTimeWib/startTimeWib/endTimeWib.
  • Kalender merender reminder dengan penanda visual berbeda (ikon lonceng / label) dan tanpa rentang waktu.
  • Kalender tidak melakukan grouping/sort ulang — pakai days[].activities apa adanya.
  • Grid monthly memakai days dari response (jangan hitung grid sendiri).
  • Detail reminder menampilkan status: reminderSentAt === null → "Pending", selain itu → waktu terkirim.
  • Handle 403 project closed dengan state read-only (sembunyikan tombol tulis).
  • Deep link notifikasi: data.action === 'daily_activity.reminder' atau 'daily_activity.assigned' → buka project data.projectId di tanggal data.reminderTime.

12. Skenario QA

  1. Create TASK dengan window → muncul di kalender pada hari & jam WIB yang benar, tanpa penanda reminder.
  2. Create REMINDER tanpa division guest → 400.
  3. Create REMINDER jam 23:30 WIB → masuk ke hari yang sama (bukan hari berikutnya).
  4. Patch TASK → REMINDER lengkap dengan reminderTime → kolom window jadi null di response.
  5. Patch REMINDER → TASK dengan startTime+endTime → reminderTime jadi null, reminder pending lama dibatalkan.
  6. Biarkan reminder lewat waktunya → penerima di project team divisi terkait dapat in-app + push + email; reminderSentAt terisi.
  7. Replay/duplikat job tidak mengirim notifikasi kedua (reminderSentAt sudah terisi).
  8. Ubah reminderTime ke masa depan sebelum terkirim → hanya satu notifikasi, di jam baru.
  9. Kalender monthly bulan November 2026 → 35 sel atau lebih, kelipatan 7, hari dari Oktober/Desember ikut.
  10. Project COMPLETED → POST/PATCH/DELETE dibalas 403.