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
canEditsudah 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: <mime>.
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:
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
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
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
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
- Tampilkan tombol edit hanya bila
entry.canEdit === truedanentry.status === 'PENDING'. - Form: persentase/volume, deskripsi, tanggal, kelola gambar (pilih yang dihapus per thumbnail), ganti PDF.
- Tidak perlu input alasan, tidak perlu menampilkan sisa kuota.
- Setelah sukses, entry kembali
PENDINGdengan nilai baru — tidak ada baris riwayat baru.
5.2 Admin
- Tampilkan tombol edit bila
entry.canEdit === true. - Satu form: status, persentase/volume, deskripsi, tanggal, gambar, PDF, alasan (wajib).
- Tampilkan sisa kuota dari
entry.editCount/entry.maxEdits("Revisi 2 dari 3"). - Bila
canEdit === falsekarena kuota habis, disable tombol dan jelaskan batasnya. - 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
{
"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
[
{
"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: <mime> |
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
- Selalu
multipart/form-data, walaupun hanya mengirimprogress. Body JSON tidak dibaca interceptor upload. removeDocumentdandocumentSourcebukan boolean —removeDocumentharus string'true'/'false'.removeImageIdsper id. Kalau thumbnail dihapus semua, kirim semua id-nya, jangan kirim array kosong berharap server menghapus semua.- Jangan kirim field yang tidak berubah.
descriptionyang dikirim ulang dengan nilai lama tetap dianggap perubahan nilai (dan untuk admin, memakan satu kuota). - Kuota dihitung per request, bukan per field. Gabungkan semua koreksi dalam satu submit.
- Status berubah =
approvedByditulis ulang ke admin yang menyimpan. Ini disengaja. canEdittidak menggantikan role check. Untuk member, syarat tombol edit =canEdit && status === 'PENDING' && createdBy === currentUserId.- Entry lama tidak bisa diedit. Kalau list menampilkan riwayat panjang, sembunyikan aksi edit
pada semua entry kecuali
canEdit === true. - Koreksi member tidak tercatat. Jangan mengharapkan riwayat untuk entry
PENDING; panel riwayat akan kosong atau berisi revisi admin sebelumnya. editCountbukan 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) |