# BE — Kalender menampilkan pekerjaan harian (Kanban) pada tanggal laporan

Dokumen ini untuk **tim backend** mengimplementasikan perilaku yang diharapkan pengguna:

> *“Saya sudah isi tugas di Kanban Daily Report untuk tanggal X; harusnya muncul di Kalender pada tanggal itu.”*

Ringkasan konteks produk ada di: [`BE_HANDOFF_REPORT_KANBAN_TASKS_CALENDAR_TEAM_MONITORING.md`](./BE_HANDOFF_REPORT_KANBAN_TASKS_CALENDAR_TEAM_MONITORING.md).

---

## 1. Status implementasi (Opsi A MVP) — di repo BE

Sudah ada di codebase:

- **Migrasi** `BE/internal/db/migrations/000006_calendar_events_report_link.up.sql` — kolom `report_id`, `kind`, indeks unik partial.
- **Sinkron** `BE/internal/api/report_calendar_sync.go` — `syncReportCalendarEvent`: `report_date` terisi + isi bermakna (`workDescription`, legacy `blockers`/`tomorrow`, atau ≥1 `tasks[].title`) → satu baris `kind = report_summary` (all-day UTC). Parsing `report_date` memakai 10 karakter pertama (`YYYY-MM-DD`) agar aman jika driver mengembalikan timestamp.
- **Backfill** `BackfillReportCalendarEvents` + `POST /api/v1/admin/reports/calendar-sync-backfill` (superadmin) atau `go run ./cmd/calendar-backfill`.
- **Dipanggil dari** `createReport` dan **setiap** `patchReport` (akhir handler) di `BE/internal/api/rest.go`.
- **`listCalendar`** mengembalikan `reportId` dan `kind` jika ada.

FE Kalender **tidak wajib diubah** untuk menampilkan titik/agenda — field tambahan itu opsional (link ke laporan, styling).

---

## 1b. Kenapa Kalender masih kosong *setelah* BE di-deploy?

Kalender FE hanya menampilkan apa yang dikembalikan **`GET /api/v1/calendar/events`** dari baris **`calendar_events`**. Biasanya **bukan** bug tampilan FE, melainkan salah satu dari berikut:

| Penyebab | Cek / tindakan |
|----------|----------------|
| **Error `pq: column e.report_id does not exist` (atau sejenis)** | Kode BE sudah query kolom baru, tetapi **DB belum migrasi `000006`**. **Restart API saja tidak menjalankan migrasi** (kecuali startup dengan `WORKPULSE_AUTO_MIGRATE=1` sesuai kebijakan deploy). Pastikan migrasi jalan ke **database yang sama** dengan `WORKPULSE_DATABASE_DSN` proses `workpulse-api`. |
| **Migrasi 000006 belum di DB yang dipakai API** | Di DB target: `\d calendar_events` harus ada `report_id`, `kind`. Tanpa kolom ini, **SELECT** kalender error; **INSERT** sync juga gagal → simpan laporan bisa **500**. |
| **Proses API masih binary lama** | `go build` / deploy artifact baru lalu **`pm2 restart workpulse-api`** (atau unit systemd setara). Pastikan FE / reverse proxy mengarah ke host yang sudah pakai binary itu (`workpulseApiBase` / same-origin). |
| **Laporan dibuat *sebelum* fitur sync; belum ada PATCH lagi** | Sync jalan saat **POST** create dan **PATCH** report. Data lama: buka **Daily Report** untuk tanggal yang dimaksud, pastikan syarat Kanban terpenuhi, lalu **Save draft** sekali atau tunggu autosave agar **PATCH** terkirim. Alternatif: **backfill** (§6). |
| **`report_date` NULL atau body kosong** | Sync butuh `report_date` + minimal **deskripsi pekerjaan** (`workDescription`), teks legacy (`blockers`/`tomorrow`), **atau** ≥1 task Kanban berjudul. Hanya judul laporan tanpa isi → tidak insert acara. |
| **Filter tim di Kalender** | Jika chip tim aktif (bukan “Semua”), acara **`team_id` NULL** (laporan tanpa tim) **disembunyikan** oleh FE. Coba filter **Semua**. |

### Checklist singkat deploy / ops

| Langkah | Catatan |
|--------|---------|
| **Migrasi DB `000006`** | Wajib sebelum atau bersamaan dengan binary yang sudah pakai `report_id` / `kind`. |
| **`go build` → binary API** | Mis. `go build -o bin/api ./cmd/api` di folder BE. |
| **`pm2 restart workpulse-api`** | Memuat proses baru; **tidak** menggantikan langkah migrasi. `--update-env` hanya bila perlu memuat ulang env dari ecosystem. |
| **`pm2 restart workpulse-fe`** | Opsional; hanya jika ada perubahan FE atau cache yang relevan — kosongnya agenda biasanya dari data API, bukan dari restart FE. |

### SQL verifikasi cepat

**1) Pastikan kolom migrasi ada (skema):**

```sql
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'public'
  AND table_name = 'calendar_events'
  AND column_name IN ('report_id', 'kind');
```

Harus mengembalikan **dua baris** (`report_id`, `kind`). Jika kosong → migrasi `000006` belum di DB ini.

**2) Laporan vs acara turunan (ganti `:user_id`):**

```sql
SELECT id, title, report_date, updated_at
FROM reports
WHERE user_id = :user_id
ORDER BY updated_at DESC
LIMIT 5;

SELECT id, title, starts_at, ends_at, report_id, kind
FROM calendar_events
WHERE user_id = :user_id
ORDER BY starts_at DESC
LIMIT 10;
```

**3) Cek tanggal laporan tertentu (contoh 14 Mei 2026) dan user yang sama:**

```sql
SELECT r.id AS report_id, r.title, r.report_date, ce.id AS calendar_event_id, ce.kind
FROM reports r
LEFT JOIN calendar_events ce ON ce.report_id = r.id AND ce.kind = 'report_summary'
WHERE r.user_id = :user_id AND r.report_date = '2026-05-14';
```

- `calendar_event_id` **NULL** setelah user sudah **PATCH** laporan itu → cek log API saat PATCH, atau syarat Kanban / `report_date` §1b.
- Belum pernah PATCH sejak deploy → `calendar_event_id` NULL wajar; lakukan simpan sekali atau backfill.

Baris `calendar_events` kosong padahal skema sudah benar dan PATCH sudah jalan → **log API** (error INSERT) atau filter tim di FE.

---

## 2. Sumber kebenaran yang ada di BE hari ini

| Entitas | Lokasi |
|---------|--------|
| Laporan | `reports` (`id`, `user_id`, `body` TEXT, `report_date` DATE, `status`, `division`, …) |
| Acara kalender | `calendar_events` (`id`, `user_id`, `team_id` nullable, `title`, `starts_at`, `ends_at`, `all_day`, `created_at`) — migrasi `000001_init.up.sql` |

JSON body Kanban (FE): `FE/utils/report-kanban-payload.ts` — schema `workpulse.report.v1`, array `tasks[]` dengan `id`, `title`, `column` (`todo` \| `doing` \| `done`).

---

## 3. Rekomendasi implementasi (arsitektur; Opsi A MVP sudah di §1)

### Opsi A — **Sinkron ke `calendar_events`** (paling kecil dampak ke FE) — **sudah dipilih & diimplementasi (MVP)**

Kalender FE **tidak perlu diubah** jika BE mengisi `calendar_events` untuk rentang yang sama dengan `report_date` + jam.

**MVP (satu acara per laporan per hari):**

- Untuk setiap baris `reports` yang punya `report_date` **dan** `body` JSON valid dengan minimal satu `tasks[].title` (atau selalu buat jika ada `report_date`):
  - Upsert **satu** baris `calendar_events`:
    - `user_id` = `reports.user_id`
    - `team_id` = `reports.team_id` jika ada; else `NULL`
    - `all_day` = `true` **atau** blok waktu dari JSON `workStart`/`workEnd` + `report_date` (lihat §4 zona waktu)
    - `title` = mis. `Laporan harian · N tugas` atau judul singkat + counter (produk tentukan)
    - `starts_at` / `ends_at` = rentang hari tanggal `report_date` (MVP all-day: 00:00–23:59:59.999 di TZ yang disepakati, atau UTC full-day)
- **Idempotensi:** tambah kolom di `calendar_events`, mis.:
  - `report_id BIGINT NOT NULL REFERENCES reports(id) ON DELETE CASCADE`
  - `kind VARCHAR(32) NOT NULL DEFAULT 'report_summary'` — unik `(report_id, kind)` untuk baris ringkasan
- **Kapan sync:** setelah `POST /api/v1/reports` (create) dan `PATCH` yang mengubah `body` atau `report_date` atau `status` (hapus acara jika laporan dihapus / `report_date` dikosongkan — kebijakan produk).
- **Hapus:** `ON DELETE CASCADE` dari `reports` → hapus acara turunan; atau saat `body` tidak lagi punya task, hapus acara.

**Versi lanjutan (satu acara per task):**

- Banyak baris `calendar_events` per laporan; identitas stabil per task: kolom `task_client_id VARCHAR(64)` (UUID dari JSON) + `report_id`; **UNIQUE (`report_id`, `task_client_id`)**.
- `title` = `task.title`; waktu = slot dalam hari (mis. bagi rata `workStart`–`workEnd`) atau all-day dengan suffix kolom.

**Titik kode BE yang disentuh:**

- Handler create/update report (sama file area `patchReport` / create report di `BE/internal/api/rest.go` atau modul terpisah).
- Ekstraksi JSON: parse aman; skip jika `body` bukan JSON / schema tidak cocok.
- **Migrasi SQL baru** (jangan edit migrasi lama yang sudah deploy): `ALTER TABLE calendar_events ADD COLUMN …`.

---

### Opsi B — **Gabungkan di query `listCalendar`** (tanpa insert)

- `listCalendar` mengembalikan union:
  - acara asli dari `calendar_events`, **plus**
  - baris sintetis dari `reports` (parse `body` di aplikasi atau `jsonb` jika kolom dipindah).
- Perlu kontrak respons yang jelas (mis. field `source: "calendar" | "report"` + `reportId`) — **FE harus diubah** untuk styling/klik ke laporan.

Opsi ini lebih berat di query dan caching; opsi A biasanya lebih rapi untuk FE yang sudah ada.

---

## 4. Zona waktu & tanggal

- `report_date` adalah **DATE** (tanpa jam).
- `calendar_events` memakai **TIMESTAMPTZ**.
- Sepakat satu kebijakan: mis. **UTC** hari kalender (`report_date` → `starts_at = report_date 00:00 UTC`, `ends_at = report_date+1 00:00 UTC` untuk all-day), atau **offset organisasi** (kolom/tabel TZ user — belum ada = default UTC).

Dokumentasikan pilihan di respons API / README deploy agar konsisten dengan tampilan FE “hari” lokal.

---

## 5. Draft vs submitted

**Perilaku di repo BE saat ini:** **Draft dan submitted** sama-sama memicu sync (`syncReportCalendarEvent` tidak memfilter `status`). Acara `kind = report_summary` ikut ter-update setiap **PATCH** yang mengubah isi relevan.

Jika produk ingin hanya submitted: perlu perubahan BE (filter status) dan update dokumen ini.

---

## 6. Checklist implementasi BE (status repo)

**Implementasi kode (selesai di repo):**

- [x] Migrasi `000006`: `report_id`, `kind`, indeks unik partial (`report_summary` satu baris per laporan).
- [x] `syncReportCalendarEvent` + panggilan dari `createReport` / `patchReport`.
- [x] Parser `body` minimal selaras FE (`workpulse.report.v1` + ≥1 `tasks[].title` non-kosong).
- [x] `listCalendar` / `listCalendarOrg`: field `reportId`, `kind` di JSON saat ada.

**Operasi / produk (di luar merge kode):**

- [ ] **Migrasi `000006` diterapkan** ke setiap lingkungan (staging/prod) pada DB yang dipakai `workpulse-api` — verifikasi dengan SQL §1b (query `information_schema`).
- [ ] **Binary API** di server sesuai commit yang berisi sync + migrasi; **`pm2 restart workpulse-api`** (atau setara) setelah deploy.
- [ ] **Data lama:** minimal satu **PATCH** per laporan yang ingin muncul di kalender, atau **backfill** sekali (SQL `INSERT … SELECT` dari `reports` + parse body di app — belum ada endpoint bawaan; lihat §1b).
- [ ] (Opsional) `calendar_org` / superadmin: kebijakan tampilan acara `report_summary` lintas user.

---

## 7. Referensi cepat

| File | Peran |
|------|--------|
| `BE/internal/api/rest.go` | `listCalendar`, `patchReport`, `createReport` |
| `BE/internal/api/report_calendar_sync.go` | `syncReportCalendarEvent` |
| `BE/internal/db/migrations/000006_calendar_events_report_link.up.sql` | Kolom `report_id` / `kind` |
| `BE/internal/db/migrations/000001_init.up.sql` | Skema awal `calendar_events`, `reports` |
| `FE/pages/calendar.vue` | Konsumsi `GET /calendar/events` |
| `FE/utils/report-kanban-payload.ts` | Bentuk JSON task |

---

*Setelah deploy: **migrasi DB** (wajib sebelum binary baru yang query `report_id`) + **restart API** + minimal satu **PATCH** laporan (atau backfill). Error `pq: column … report_id` → §1b. Kalender masih kosong → §1b + SQL verifikasi.*
