# Handover FE — Edit Progress Action Plan > Backend `modules_base`, feature/version/1/action-plans. Dokumen ini untuk tim frontend. > Semua perubahan backend sudah selesai dan teruji; tidak ada yang perlu ditunggu. --- ## 1. Yang berubah, singkat Dulu progress yang salah harus dihapus lalu submit ulang. Sekarang ada **satu endpoint revisi** yang bisa mengubah nilai, lampiran, dan (untuk admin) status. Ada **dua form berbeda** di satu endpoint, dibedakan oleh status entry: | Siapa | Kapan | Form | Kuota | Tercatat | | ----- | ----- | ---- | ----- | -------- | | Member (submitter) | entry `PENDING` | nilai + lampiran | tidak ada | tidak | | OWNER / ADMIN | entry sudah direspons | nilai + lampiran + **status** | 3x | ya | Poin penting untuk FE: - Tombol edit **hanya** untuk submission terakhir. Field `canEdit` sudah dihitung server. - Member boleh revisi berkali-kali **tanpa** isi alasan. - Admin wajib isi alasan; satu submit = satu unit dari kuota 3x, walaupun nilainya banyak. - Selalu kirim `multipart/form-data`, walaupun tidak ada file. --- ## 2. Endpoint Base: `/v1/projects/:projectId/action-plans` ### 2.1 Revisi (satu endpoint, tiga varian path) ``` PATCH :activityId/progress/:progressId PATCH :activityId/tasks/:taskId/progress/:progressId PATCH :activityId/tasks/:taskId/subtasks/:subtaskId/progress/:progressId ``` `Content-Type: multipart/form-data` ### 2.2 Riwayat revisi ``` GET :activityId/progress/:progressId/edits GET :activityId/tasks/:taskId/progress/:progressId/edits GET :activityId/tasks/:taskId/subtasks/:subtaskId/progress/:progressId/edits ``` Hanya berisi revisi admin. Koreksi member sebelum approval tidak pernah muncul di sini. --- ## 3. Field yang dikirim | Field | Tipe | Boleh dipakai | Catatan | | ----- | ---- | ------------- | ------- | | `progress` | number 0–100 | member & admin | kirim `workVolume` saja → persentase dihitung server | | `workVolume` | number | member & admin | | | `description` | string ≤ 5000 | member & admin | | | `createdAt` | date | member & admin | boleh backdate, tidak boleh masa depan | | `attachments[]` | file | member & admin | bisa lebih dari satu, diulang per file | | `removeImageIds` | string \| diulang | member & admin | **pilih per id**, bukan ganti semua | | `document` | file PDF | member & admin | ganti PDF dengan upload | | `documentSource` | `upload` \| `document` | member & admin | sumber PDF | | `documentRef` | JSON string | member & admin | `{"documentId","versionId","revisionId"}` | | `companyId` | uuid | member & admin | **wajib** kalau `documentSource=document` | | `removeDocument` | `'true'`/`'false'` | member & admin | string, bukan boolean | | `status` | enum | **admin saja** | `APPROVED` / `APPROVED_WITH_NOTES` / `REJECTED` | | `reason` | string ≤ 500 | admin wajib, member opsional | | Minimal satu aksi harus dikirim. Request kosong → `400 Nothing to update`. Tipe file yang diterima: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/svg+xml`, `application/pdf`. Selain itu → `400 File type not allowed: `. ### 3.1 PDF: upload atau ambil dari modules_document Payload-nya **sama** dengan form submit progress (`POST .../progress`). Kalau dokumen diambil dari modul dokumen, jangan kirim file — kirim referensinya: ```ts form.append('documentSource', 'document'); form.append( 'documentRef', JSON.stringify({ documentId, versionId, revisionId }) ); form.append('companyId', currentUser.companyId); ``` Server memvalidasi tiga hal: pemanggil memang anggota company itu di project ini, dokumen itu punya company, dan company-nya sama. Dokumen tanpa company ditolak (fail closed). Kalau mau ganti PDF kembali ke upload biasa, kirim `document` dan **jangan** kirim `documentSource`/`documentRef` — server menimpa baris dokumen yang ada. --- ## 4. Contoh request ### 4.1 Member membetulkan persentase + salah foto ```bash curl -X PATCH "$BASE/:activityId/progress/:progressId" \ -H "Authorization: Bearer $TOKEN" \ -F 'progress=55' \ -F 'removeImageIds=img_123' \ -F 'attachments=@sesudah.jpg' ``` Tanpa `reason`. Tidak dihitung kuota, tidak masuk riwayat. ### 4.2 Admin mengubah status + nilai + lampiran sekaligus ```bash curl -X PATCH "$BASE/:activityId/progress/:progressId" \ -H "Authorization: Bearer $TOKEN" \ -F 'status=APPROVED_WITH_NOTES' \ -F 'progress=60' \ -F 'removeImageIds=img_456' \ -F 'attachments=@revisi.jpg' \ -F 'reason=koreksi hasil survey lapangan' ``` Satu request = satu unit kuota. ### 4.3 Admin mengubah status saja ```bash curl -X PATCH "$BASE/:activityId/progress/:progressId" \ -H "Authorization: Bearer $TOKEN" \ -F 'status=REJECTED' \ -F 'reason=salah input volume' ``` --- ## 5. Alur UI yang diharapkan ### 5.1 Member 1. Tampilkan tombol edit hanya bila `entry.canEdit === true` **dan** `entry.status === 'PENDING'`. 2. Form: persentase/volume, deskripsi, tanggal, kelola gambar (pilih yang dihapus per thumbnail), ganti PDF. 3. Tidak perlu input alasan, tidak perlu menampilkan sisa kuota. 4. Setelah sukses, entry kembali `PENDING` dengan nilai baru — tidak ada baris riwayat baru. ### 5.2 Admin 1. Tampilkan tombol edit bila `entry.canEdit === true`. 2. Satu form: **status**, persentase/volume, deskripsi, tanggal, gambar, PDF, alasan (wajib). 3. Tampilkan sisa kuota dari `entry.editCount` / `entry.maxEdits` (`"Revisi 2 dari 3"`). 4. Bila `canEdit === false` karena kuota habis, disable tombol dan jelaskan batasnya. 5. Status yang boleh dipilih: `APPROVED`, `APPROVED_WITH_NOTES`, `REJECTED` — termasuk membatalkan penolakan (`REJECTED` → `APPROVED`). ### 5.3 Riwayat Tampilkan `GET .../edits` di panel "Riwayat revisi": siapa, kapan, alasan, dan diff sebelum/sesudah dari `oldData`/`newData` (`status`, `values`, `attachments`). --- ## 6. Response ### 6.1 Field tambahan pada entry progress ```json { "id": "…", "status": "PENDING", "editCount": 1, "maxEdits": 3, "canEdit": true, "canEditAttachments": true } ``` | Field | Arti | | ----- | ---- | | `editCount` | Revisi admin yang sudah terpakai (koreksi member tidak dihitung) | | `maxEdits` | Batas revisi admin (3) | | `canEdit` | `true` hanya bila entry adalah submission terakhir **dan** (`PENDING` atau `editCount < 3`) | | `canEditAttachments` | Alias `canEdit`, dipertahankan untuk kompatibilitas | Catatan: `canEdit` sudah memperhitungkan "entry terakhir", jadi entry lama selalu `false`. Server tetap menolak edit pada entry lama sebagai lapis kedua. ### 6.2 Riwayat revisi ```json [ { "id": "…", "revisionNo": 2, "kind": "MIXED", "oldData": { "status": "APPROVED", "values": { "progress": 40, "workVolume": null, "description": "lama", "createdAt": "2026-02-01T00:00:00.000Z" }, "attachments": { "images": [{ "id": "…", "originalName": "a.jpg" }], "document": { "originalName": "laporan.pdf" } } }, "newData": { "status": "APPROVED_WITH_NOTES", "values": { "progress": 55, "workVolume": null, "description": "baru", "createdAt": "2026-02-01T00:00:00.000Z" }, "attachments": { "images": [{ "id": "…", "originalName": "b.jpg" }], "document": null } }, "reason": "koreksi hasil survey lapangan", "editedBy": "uuid", "editedByName": "Budi", "editedAt": "2026-11-02T03:00:00.000Z" } ] ``` `kind`: | Nilai | Arti | | ----- | ---- | | `VALUES` | nilai berubah (dengan atau tanpa perubahan status) | | `STATUS` | hanya status | | `ATTACHMENTS` | hanya lampiran | | `MIXED` | dua kelompok atau lebih dalam satu request | --- ## 7. Error yang akan ditemui | HTTP | Message | Kapan | Saran di UI | | ---- | ------- | ----- | ----------- | | 400 | `Progress entry not found` | id tidak ada / bukan project ini | tutup form | | 403 | `Only the submitter can edit the progress while it is pending` | admin buka form member | sembunyikan form | | 403 | `Only an owner or admin can change the progress status` | member kirim `status` | jangan kirim field itu | | 403 | `You do not have required role perform action` | member mencoba edit entry pasca-approval | sembunyikan tombol | | 400 | `Progress can only be edited 3 times` | kuota admin habis | tampilkan batas | | 400 | `reason is required when an owner or admin edits a progress` | admin tanpa alasan | validasi di form | | 400 | `Nothing to update: provide values, attachments, status, or a removal` | submit kosong | disable tombol submit | | 400 | `Only the latest progress entry can be edited` | entry sudah disusul submission baru | refresh list | | 400 | `Progress must be >= current progress (X%). New: Y%` | nilai di bawah progress yang sudah disetujui | tampilkan floor X | | 400 | `Progress must be between 0 and 100` | angka di luar rentang | clamp input | | 400 | `createdAt cannot be in the future` | tanggal > hari ini | batasi datepicker | | 400 | `createdAt must be >= previous progress date (…)` | tanggal lebih awal dari entry sebelumnya | tampilkan tanggal minimal | | 400 | `Activity is on hold: waiting for its prerequisite to be approved and closed` | node sedang hold (task/subtask juga bisa) | disable form | | 400 | `companyId is required when attaching a document-source document` | `documentSource=document` tanpa `companyId` | selalu kirim | | 400 | `Invalid documentRef: must be valid JSON` | `documentRef` bukan JSON | stringify objek | | 400 | `documentRef must contain documentId, versionId, and revisionId` | field kurang | cek selektor dokumen | | 400 | `Referenced document not found` | dokumen/revisi tidak ada | muat ulang daftar dokumen | | 400 | `Referenced document has no company, so it cannot be verified` | dokumen tanpa company | blokir pemilihan dokumen itu | | 403 | `Referenced document does not belong to your company` | dokumen milik company lain | filter daftar dokumen per company | | 400 | `File type not allowed: ` | tipe file tidak didukung | batasi `accept` | | 400 | `Cannot add progress: previous progress is still pending approval` | submit baru saat masih ada `PENDING` | nonaktifkan tombol tambah | --- ## 8. Jebakan yang mudah terlewat 1. **Selalu `multipart/form-data`**, walaupun hanya mengirim `progress`. Body JSON tidak dibaca interceptor upload. 2. **`removeDocument` dan `documentSource` bukan boolean** — `removeDocument` harus string `'true'`/`'false'`. 3. **`removeImageIds` per id.** Kalau thumbnail dihapus semua, kirim semua id-nya, jangan kirim array kosong berharap server menghapus semua. 4. **Jangan kirim field yang tidak berubah.** `description` yang dikirim ulang dengan nilai lama tetap dianggap perubahan nilai (dan untuk admin, memakan satu kuota). 5. **Kuota dihitung per request, bukan per field.** Gabungkan semua koreksi dalam satu submit. 6. **Status berubah = `approvedBy` ditulis ulang** ke admin yang menyimpan. Ini disengaja. 7. **`canEdit` tidak menggantikan role check.** Untuk member, syarat tombol edit = `canEdit && status === 'PENDING' && createdBy === currentUserId`. 8. **Entry lama tidak bisa diedit.** Kalau list menampilkan riwayat panjang, sembunyikan aksi edit pada semua entry kecuali `canEdit === true`. 9. **Koreksi member tidak tercatat.** Jangan mengharapkan riwayat untuk entry `PENDING`; panel riwayat akan kosong atau berisi revisi admin sebelumnya. 10. **`editCount` bukan total revisi.** Revisi admin pada baris yang sama bisa berlanjut setelah approval berikutnya me-reset kuota ke 0. --- ## 9. Migrasi & data lama Dua migrasi Prisma harus sudah jalan di environment target (`prisma migrate deploy`): ``` 20261102100000_v1_action_plan_progress_edits → kolom editCount, soft-remove images, tabel revisi 20261102110000_v1_action_plan_progress_amended_created_by → backfill createdBy row amended ``` Tidak ada perubahan bentuk response lama selain field tambahan di §6.1. Field lama tetap ada. --- ## 10. Belum ada (jangan diharapkan) - Notifikasi saat progress/lampiran direvisi. - Riwayat revisi untuk koreksi member (memang tidak disimpan). - Endpoint bulk edit; revisi tetap satu per entry. --- ## 11. File backend terkait | File | Isi | | ---- | --- | | `progress.service.ts` | `updateProgress` (semua aturan), `listProgressEdits`, `latestEditableId`, `resolveDocumentRef` | | `action-plans.controller.ts` | 3 route `PATCH .../progress/:progressId`, `parseProgressEditBody` | | `action-plans.dto.ts` | `UpdateProgressSchema` / `UpdateProgressDto` | | `.markdown/PROGRESS_EDIT.md` | Dokumen desain lengkap (termasuk aturan internal) |