# Handover FE — Transmittal Project Settings Dua setting baru di level project (`Projects.settings`) yang memengaruhi perilaku transmittal: 1. **Blokir download form transmittal** selama transmittal masih dalam proses review. 2. **Reminder deadline** (dalam hari) ke checker/approver yang belum merespons, dikirim email oleh cron 08:00 WIB. Backend sudah jalan di staging. FE perlu menambah 2 kontrol di form pengaturan project dan menangani satu error baru saat download. --- ## 1. Endpoint yang dipakai | Kebutuhan | Endpoint | Permission | |---|---|---| | Baca settings | `GET /api/projects/:id` | member project | | Ubah settings | `PATCH /api/projects/:projectId` | `base.project.update` (owner/admin) | | Request download form transmittal | `POST /api/projects/:projectId/reports/transmittal-form/:transmittalId/request-download` | `transmittal.download.create` | | Download form PDF langsung | `GET /api/projects/:projectId/reports/transmittal-form/:transmittalId/download` | `transmittal.report.read` | | Lampiran transmittal (byte statis) | `{transmittal}/public/transmittals/...` (URL yang sudah dipakai sekarang) | tidak ada guard terpisah | Tidak ada endpoint baru. Settings tetap dikirim lewat `PATCH` project yang sudah ada. Versi API dikirim lewat header (custom versioning), bukan di path. Pakai base URL + header yang sudah dipakai web saat ini. --- ## 2. Bentuk data di `settings` Settings adalah **satu kolom JSON**, bukan kolom per-setting. Grup baru: `transmittal_settings`. ```json { "transmittal_settings": { "disable_download_when_open": true, "deadline_reminder_days": 3 } } ``` Aturan: - `disable_download_when_open` — `boolean`, default `false`. - `deadline_reminder_days` — `integer 0..365`, default `0` (= mati). `null` diterima, artinya unset. - Dua-duanya opsional. Grup `transmittal_settings` opsional. - Key lain di dalam `settings` **wajib dipertahankan**. Backend memakai `catchall`, jadi key yang tidak dikenal tidak ditolak dan tidak dibuang. ### Bentuk data produksi (penting) Row lama menyimpan nilai yang sama dua kali: di dalam grup dan di top-level. Contoh nyata: ```json { "approver_revoke_time": 2, "approver_revoke_time_unit": "days", "transmittal_settings": { "approver_revoke_time": 2, "approver_revoke_time_unit": "days" } } ``` Backend membaca **nested dulu**, lalu fallback ke key dengan nama sama di top-level. Jadi penulisan baru cukup ke dalam `transmittal_settings`. Duplikat flat lama boleh dibiarkan. --- ## 3. Jebakan: `PATCH` mengganti seluruh kolom `settings` `settings` adalah JSON column dan di-update **replace, bukan merge**. Kalau FE mengirim: ```json { "settings": { "transmittal_settings": { "disable_download_when_open": true } } } ``` maka seluruh key lain (`basis`, `category`, `milestone_id`, `partner_company_id`, `closing_approver_id`, `priority`, `approver_revoke_time`, dst.) **hilang**. Kontrak untuk FE: - Ambil `settings` dari `GET /projects/:id`. - Gabungkan perubahan secara lokal: `settings.transmittal_settings = { ...settings.transmittal_settings, ...perubahan }`. - Kirim objek `settings` hasil gabungan secara utuh di `PATCH`. - Jangan mengirim `transmittal_settings` saja. Contoh payload benar: ```json { "settings": { "basis": "Berdasarkan arahan PT Danantara Asset Management", "category": "CONSULTANT", "priority": "MEDIUM", "milestone_id": "bf083b10-53f8-492c-bc02-c731608d7a62", "partner_company_id": "12219e63-8762-4a7b-a8f4-70cb9705ffd8", "closing_approver_id": "33b51ecb-62c2-4ef7-af2a-fc55a00e75bb", "basis_of_implementation": "Berdasarkan arahan PT Danantara Asset Management", "transmittal_settings": { "disable_download_when_open": true, "deadline_reminder_days": 3 } } } ``` --- ## 4. Setting A — `disable_download_when_open` **Label yang disarankan:** "Blokir download transmittal selama masih diproses" **Tipe kontrol:** toggle/switch **Tersedia di:** pengaturan project, hanya untuk owner/admin (`base.project.update`) Perilaku backend saat `true` — berlaku untuk form transmittal dan dokumen yang terikat padanya: - Diblokir hanya saat transmittal berstatus `OPEN`, `IN_CHECKING`, `IN_APPROVAL` ("masih dalam review"). - Semua status lain tetap bisa diunduh: `DRAFT`, `SCHEDULED`, `SENT`, `REJECTED`, `REJECTED_WITH_NOTES`, `REJECTED_RESUBMISSION`, `APPROVED`, `APPROVED_WITH_NOTES`, `ARCHIVE`. - Transmittal yang sudah closed (`is_closed` / `effective_is_closed`) tetap bisa diunduh, apa pun statusnya. - Status efektif menang atas status dasar (`effective_status` / `effective_is_closed`), sama seperti logika status lain di aplikasi. Permukaan download yang ikut diblokir (hanya aksi yang punya tombol download khusus): | Permukaan | Endpoint | |---|---| | Minta download form (antrean) | `POST /api/projects/:projectId/reports/transmittal-form/:transmittalId/request-download` | | Download form PDF langsung | `GET /api/projects/:projectId/reports/transmittal-form/:transmittalId/download` | | Ambil hasil antrean form | `GET /api/projects/:projectId/document-downloads/:downloadId/file` (hanya bila `record_resource = transmittal`) | | Download dokumen beranotasi (langsung) | `POST /api/export/room/:roomId/download` | | Minta download dokumen beranotasi (antrean) | `POST /api/export/room/:roomId/request-download` | | Ambil hasil antrean dokumen | `GET /api/projects/:projectId/document-downloads/:downloadId/file` (annotation, `record_resource = room`) | | Unduh ulang hasil export | `GET /api/export/record/:exportId/download` | | Byte file dokumen asli (mpp/autodesk, open-in-new-tab) | `GET {document}/uploads/projects/{projectId}/documents/{documentId}/{file}` | Gate hanya aktif untuk request yang membawa `transmittal_id` (lihat bagian berikut). Unduhan dokumen yang sama dari Drive tidak berubah. Transmittal yang dievaluasi = transmittal yang id-nya dikirim FE. Kalau satu revisi dipakai di beberapa transmittal, transmittal lain tidak memengaruhi hasil. ### Wajib dari FE: kirim `transmittal_id` Drive dan transmittal memakai URL/endpoint yang sama, jadi FE harus mengirim id transmittal yang sedang dibuka. Nilainya sudah tersedia di preview: `document.transmittal.id` (`preview-document.types.ts:58-62`). Tanpa parameter itu, backend mengizinkan (perilaku lama). Tambahkan `?transmittal_id=` pada: 1. `POST /api/export/room/:roomId/download` — download dokumen beranotasi (langsung). 2. `POST /api/export/room/:roomId/request-download` — minta download beranotasi (antrean). 3. `GET /api/export/record/:exportId/download` — unduh ulang hasil export. 4. `GET /api/projects/:projectId/document-downloads/:downloadId/file` — ambil hasil antrean, hanya untuk unduhan yang dimulai dari layar transmittal. 5. `GET {document}/uploads/projects/...` — tombol download mpp/autodesk dan open-in-new-tab dari preview transmittal. Endpoint form transmittal (`request-download`, `transmittal-form/:transmittalId/download`, dan file antreannya) tidak perlu parameter tambahan: route itu memang hanya dipakai transmittal. Tidak terpengaruh (memang begitu desainnya): - Preview lampiran approval/komentar (`ApprovalPanel`, `PreviewCommentsPanel`, `ClosingTransmitter`, halaman inbox) — semuanya `window.open` ke file statis, bukan tombol download. Klik kanan → Save as tetap bisa; itu diterima. - File tanda tangan. - Artefak turunan dokumen: halaman hasil ekstraksi dan preview (`.../{timestamp}_{hash}/...`), jadi viewer tetap jalan. - Export laporan/report transmittal (`/reports/transmittal-report/export`). - Dokumen di luar transmittal dan aset project lain. ### Error baru yang harus ditangani Endpoint API mengembalikan 409: ``` HTTP 409 { "statusCode": 409, "error": "TRANSMITTAL_DOWNLOAD_LOCKED", "message": "Download is disabled while this transmittal is still open." } ``` Byte file statis (baris terakhir tabel) mengembalikan **403** dengan isi yang sama: ``` HTTP 403 { "statusCode": 403, "error": "TRANSMITTAL_DOWNLOAD_LOCKED", "message": "Download is disabled while this transmittal is still open." } ``` Penanganan FE: - Branch pada `error === 'TRANSMITTAL_DOWNLOAD_LOCKED'`, jangan cek status code saja — 409 untuk endpoint API, 403 untuk byte file statis. - Tampilkan pesan yang jelas, bukan toast generik "gagal download". Sarankan: "Download dinonaktifkan selama transmittal masih diproses. Hubungi admin project untuk mengubah pengaturan." - Jangan retry otomatis. Ini keputusan konfigurasi, bukan error sementara. - Unduhan mpp/autodesk memakai `fetch` langsung ke URL statis; cek `response.ok` dan baca JSON error di atas sebelum menampilkan toast. - "Open in new tab" untuk file dokumen ikut 403 (URL sama dengan tombol download). Ini disengaja. - Preview lampiran tidak berubah, jadi tidak perlu penanganan `onerror` baru. Catatan desain: gate ini **bukan** batas keamanan, hanya penguncian kenyamanan. Kalau settings atau RPC antar-service gagal, backend memilih mengizinkan download (fail-open). Jangan andalkan ini untuk kontrol akses. (`/public` dan `/uploads` memang belum punya auth — pekerjaan terpisah.) ### Latensi efek Keputusan gate di-cache 60 detik per transmittal (form) dan per revisi (dokumen). Setelah toggle diubah, efeknya bisa terasa hingga ~1 menit. Kalau QA menguji "matikan lalu langsung download", tunggu atau ulangi setelah 60 detik. --- ## 5. Setting B — `deadline_reminder_days` **Label yang disarankan:** "Kirim pengingat H-… sebelum deadline" **Tipe kontrol:** number input (integer), boleh 0 **Tersedia di:** pengaturan project, hanya untuk owner/admin Semantik: - Nilai `0` = fitur mati (default). - Nilai `N > 0` = kirim email pengingat ketika sisa waktu menuju deadline ≤ N hari. N juga mencakup transmittal yang sudah lewat deadline. - Cron berjalan **setiap hari 08:00 WIB**, hanya untuk transmittal berstatus `OPEN` / `IN_CHECKING` / `IN_APPROVAL` dan belum closed. - Satu email per pasangan (**approval node, user, due date**). Tidak ada email berulang tiap hari untuk deadline yang sama. - Penerima: semua checker dan approver pada node tersebut yang belum merespons. Kalau user sudah merespons sebelum email dikirim, email dibatalkan saat itu juga. - Kalau transmittal selesai/ditutup sebelum email dikirim, email dibatalkan. Tidak ada UI tambahan selain input angka. Tidak ada endpoint untuk melihat riwayat reminder. Saran validasi FE: integer, `0 <= N <= 365`. Backend menolak di luar itu dengan 400. --- ## 6. Validasi & pesan error `PATCH` project memvalidasi `settings`: | Field | Aturan | Contoh tidak valid | |---|---|---| | `disable_download_when_open` | boolean | `"true"` (string) → 400 | | `deadline_reminder_days` | integer, 0..365, atau `null` | `-1`, `366`, `1.5` → 400 | | `transmittal_settings` | object | `[]`, `"on"` → 400 | Balasan 400 memakai format validasi Zod yang sudah dipakai endpoint project lain. Tidak ada kode error khusus. Hindari menulis nilai di luar rentang lewat jalur lain (script, migrasi). Nilai rusak bisa membuat pembacaan settings gagal di service lain.