# PRD — Visit Plan Module

> ## ⛔ ATURAN ABSOLUT #0 — LEGACY FILES ARE **NEVER** COMMITTED TO GIT
>
> **User directive 2026-07-02 (verbatim intent): the legacy Visit Plan PHP files must NOT be pushed into git, NO MATTER WHAT.**
>
> - The user pushes the legacy PHP reference screens from their Mac into `Pages/MenuVisitPlans/Just An Example, Do not Link/` (first batch `Create Visit Plan/` **arrived 2026-07-02**, verified git-ignored) — mirroring the Sample Order convention, but **unlike Sample Order, whose legacy folder ended up tracked in git**, this one must stay **LOCAL ONLY, FOREVER**.
> - **NEVER `git add`** anything under that folder — not via `git add .`, not via `git add -A`, not via any bulk stage. Before **every** commit touching this module, run `git status` and verify **zero** legacy `.php` files are staged.
> - This applies to ANY legacy reference file for this module, wherever it lands in the repo, in every current and future session.
> - **Files, not knowledge:** summarize a legacy rule in your own words and cite its filename in the README/PRD, but NEVER paste verbatim legacy PHP (whole functions, SQL as-written) into those committed docs — that commits exactly what must stay local-only.
> - **Hard guard — APPLIED & VERIFIED 2026-07-02:** `ColorindoChemtraInertia/.gitignore` carries `/resources/js/Pages/MenuVisitPlans/Just An Example, Do not Link/` (`git check-ignore` OK — see README §3). Do not weaken or remove it.
> - If a legacy file is ever found staged or committed: STOP, tell the user immediately, and wait for instructions.

> ## ⚠️ ATURAN ABSOLUT #0b — PARALLEL-SESSION EDIT GUARD (user directive 2026-07-02)
>
> The user runs several Claude sessions on this repo at the same time. **Before editing any EXISTING file**, check via Bash when it was last modified and by whom it might still be "in edit":
>
> 1. Run the check first, e.g. `date && stat -c '%y  %n' <target files>` — or sweep with `find app routes database resources/js tests -type f -mmin -30` (cover every path the pass will touch) — and **list to the user every target file modified less than 30 minutes ago** (it is likely still being edited by another session). The check is **per edit**, not once per session — a start-of-session sweep goes stale.
> 2. Last modification **≥ 30 minutes ago → OK to edit** without asking.
> 3. Last modification **< 30 minutes ago → STOP and ASK the user first**; edit only after approval.
> 4. Exemptions: brand-new files this session is creating, and files whose **latest** change is this session's own edit. mtime says *when*, not *who* — the exemption holds only while YOU were the last editor; if another session may have touched the file after you, it is hot again. Shared hotspots (`routes/web.php`, `SampleOrderController.php`, `Sidebar.jsx`, seeders) deserve the check the most — see the git-status collision note in §3.2.
> 5. **New-file carve-out to exemption 4:** brand-new-file exempt is NOT "write blind" — before creating an owned artifact, `ls`/`git status` the exact target path *immediately before writing* (a parallel session may have just created it, same shared tree). A new **migration** is exempt from the edit-check but NOT from ordering: `ls database/migrations/*visitplan*` first — Laravel runs migrations in filename order across the whole repo against the shared dev DB, so two same-minute visit-plan migrations can apply out of the order each author assumed (the family already has 134940 + 134941 in one minute). The same authorship rule binds at APPLY time: `migrate` only the file(s) you authored this session, via `--path=database/migrations/<your_file>.php` — a pending `*visitplan*` migration you did not write is another session's WIP: STOP and coordinate (README rule 5). Handoff exception: apply a prior session's migration (still via `--path`) only when the user explicitly confirms it as approved handoff work AND you have read the file in full.

---

## 1. Document Metadata

| Field | Value |
|---|---|
| **Title** | PRD — Visit Plan Module |
| **Module** | Visit Plan (sales customer-visit workflow: plan a visit → conduct it → file the visit report; legacy menu name "Visit Report") |
| **Status** | **Phases 1–7 SHIPPED and committed (last: 2026-07-21).** The module is live: routed, in the sidebar, `VisitPlanController` + `VisitPlanPolicy` (11 abilities) + `VisitPlanAccess`, 157 passing Pest tests, real DB writes, built bundle. Shipped surface = own/tier lists, create (incl. on-behalf), report filing + tier comments, Visit Report Action lines + per-line tier comments + the PM approval queue, participants, Link-With (VP side), Company Context, 6 calendars, XLS export, per-sales graph, 3 queued emails, 2 Pengelolaan lookup CRUDs. ⚠️ §3.1/§3.2/§3.3 and parts of §5 below still describe the ORIGINAL Phase-1 snapshot — read §8 for the current state. |
| **Owner** | IT — PT Colorindo Chemtra |
| **Date** | 2026-07-02 (data model verified); status refreshed 2026-07-21 |
| **Stack** | Laravel 13.8 / Inertia.js v3 / React 19.2 / Tailwind v4 / MySQL 8.4 |
| **Document Type** | Hybrid PRD — documents the shipped Phase-1 copy verbatim, the **verified** data-model facts (live dev DB, 2026-07-02), and the phased roadmap. Legacy business rules are **OPEN** (§9) pending the legacy PHP files. Nothing beyond user-approved decisions is implied. |

### Locked decisions (user, 2026-07-02)

1. **Folder/naming:** group `Pages/MenuVisitPlans/`, sub-module `VisitPlans/`, files `List.jsx` + `Create.jsx`, components `VisitPlansList` / `VisitPlanCreate` (mirrors `MenuSampleOrders/SampleOrders/`). Module is called **Visit Plan** (matches the `visitplan*` tables) even though the legacy menu root says "Visit Report" — final display naming is an open item (§9).
2. **Phase-1 scope:** files + PRD **only**. No routes, no sidebar, no DB.
3. **PRD basis:** proto pages + verified DB schema now; legacy rules incorporated later from the pushed legacy files (see Aturan Absolut #0).
4. **DB verification:** read-only queries were approved and run 2026-07-02 (`SHOW CREATE TABLE`, `SELECT` counts/lookups via `docker exec cci-mysql`, see [[php-via-docker-cci]] / [[dev-db-fk-drift]] memories).

---

## 2. Overview & Purpose

**Visit Plan** is the workflow sales users use to **plan visits to their customers** — to build the relationship or to market products. A plan records who visits (sales + optional colleague "visit with", optionally joined by a principal), which company and contact person is visited, when (schedule date + time range), and why (purpose + remark). After the visit, the same record receives the **visit report** (report date, discussion, optional file upload) — the legacy module's "Create Visit Report" step.

**Why it exists:** Visit Plan is the next transactional workflow (after Quotations, LWR, and Sample Orders) being ported from the legacy PHP app to the Laravel/Inertia/React stack. Legacy production data is substantial: **11,799 active plans** since the module's inception.

**Construction lineage (deliberate):**
- **List / Create** pages are verbatim copies of the **NewUI prototype** (`Pages/Proto/VisitReport/{Index,CreateVisitPlan}.jsx`, already utility-themed) — the same "prototype-first, integrate-later" path the Sample Order Create page took.
- The future backend mirrors the **Sample Order / Quotation module patterns**: presenter-style read controller, `FormRequest` validation, rolemenu-driven Policy, `<entity>assignment` history writes, Pest feature tests. `Pages/MenuSampleOrders/` + `SampleOrder PRD.md` is the reference (user instruction: *reference only* — do not modify that module).

### 2.1 What the whole menu is for (screen-by-screen)

> Menu map approved by the user 2026-07-03. The **Create** and **Create-for-Another-Sales** rows are verified against the legacy `createvisitplan.php` + `createvisitplananothersales.php` (in `Just An Example, Do not Link/`); the remaining rows describe intent from the legacy menu tree (§7) and the proto — exact rules for the not-yet-built screens stay OPEN in §9.

**In one line:** the sales team's customer-visit workflow — salespeople **plan** visits to customers, **file a report** after each visit, and managers **monitor and comment on** their team's visits through list and calendar views. (Legacy names the menu root "Visit Report"; data lives in the `visitplan*` tables.)

- **Create Visit Plan** (menu 80, `createvisitplan.php`) — a salesperson schedules a visit **for himself**: division, schedule date + time range, company, contact person(s), optional colleague ("visit with") and/or principal, purpose, remark. The Company list is limited to **his own** companies (company whose assigned sales = the logged-in user), and he is recorded as **both** inputter (`UserIDInput`) and sales (`UserIDSales`). Saved as status **1 Created**.
- **Create Visit Plan – Join Visit Another Sales / Sales Admin** (menu 100, `createvisitplananothersales.php`) — someone creates a plan **on behalf of a different salesperson**. Identical to the normal create **except** it adds a **Sales dropdown**; only after a sales is picked does the **Company** field appear, listing **that chosen sales'** companies. The creator is the inputter (`UserIDInput`); the picked salesperson is the sales (`UserIDSales`) — equivalently the `UserIDSales` of the chosen company (the safer way to derive it in the port).
- **Create Visit Report** (menu 82, `listvisitplanreport.php?StatusID=1`) — after the visit, the salesperson opens a "Created" plan and records what happened (report date, discussion, optional file upload), moving it toward **3 Report Submitted**.
- **View Visit Report** (menu 83, `listvisitplanreport.php`) — the salesperson's own list/history of their plans and filed reports.
- **View All – Visit Plan** + role variants **Head Dept / SM / PM / MM** (menus 84 / 429 / 85 / 95·388 / 121) — manager/monitoring views of the team's visits, each scoped to what that tier may see; also where SM/PM/Head leave comments (assignment events 5 SM Comment / 6 PM Comment / 7 SM Read / 11 Head Comment). Per-tier row-scoping is still OPEN (§9.6).
- **View Calendar** + variants **All / Head Dept / SM / PM / MM** (menus 88 / 89 / 428 / 92 / 94 / 124) — the same visits as a date × salesperson matrix (the `ViewCalendar` page), so a manager sees who is visiting whom on which day, with per-sales totals.
- **Action Plan** *(legacy, retired — menu 81 soft-deleted)* — follow-up action line-items on a visit (e.g. Promote / Follow Up) with PM input; tables empty, likely out of scope unless revived.
- **Pengelolaan → Visit Report Action** (menu 96) — master-data list of the action types (Promote, Follow Up, …).
- **Pengelolaan → Visit Report Link With** (menu 319) — links a visit plan to a related deal in another module (LWR / Quotation / Sample Order) so the visit references the transaction it is about.

**Verified create-insert shape** (from the two legacy create files — for the Phase-3 store pass; substantially answers §9.3/§9.4/§9.5): a create writes **five tables** — `visitplan` (header) + `visitplanassignment` (1 row, status 1, `Comment` = the Remark) **always**, plus `visitplancompanycpdetail`, `visitplanfrienddetail`, `visitplanprincipalvisitdetail` (**0..n each**, only the filled ones). The **header stores only** `CompanyID` + who/when/purpose/remark — contact-person, colleague and principal are **not** written to the header (those columns are commented out in legacy) and live **entirely in the three detail tables**. Initial status **1 Created**; the legacy email step is commented out (nothing sent); Purpose/Remark are required in the UI but **not** server-side in legacy. ⚠️ This **corrects** the §5.3/§5.4 hypothesis that the header holds a "primary" CP — it does not; the current create path is fully pivot-based (§5.3/§5.4 updated to match — 2026-07-03).

---

## 3. Scope

### 3.1 In Scope — Phase 1 (SHIPPED 2026-07-02)

1. `resources/js/Pages/MenuVisitPlans/VisitPlans/List.jsx` — copy of `Proto/VisitReport/Index.jsx`. Only deltas vs the proto source: provenance header comment, component renamed `VisitReportIndex` → `VisitPlansList` (incl. the `.layout` statement), and a `TODO(wiring pass)` comment on the Create button. Mock `PLANS` array, `proto.visit-report.*` route refs, and all styling intact.
2. `resources/js/Pages/MenuVisitPlans/VisitPlans/Create.jsx` — copy of `Proto/VisitReport/CreateVisitPlan.jsx`. Only deltas: provenance header comment, component renamed `CreateVisitPlan` → `VisitPlanCreate` (incl. `.layout`). Mock option lists, `@/Proto/visitPlanData` import, `window.alert/confirm` prototype submit, and `proto.visit-report.*` route refs intact.
3. This PRD.

### 3.2 Explicitly NOT in Phase 1

- **No routes** — the pages are not renderable (`/visit-plans` does not exist; the originals remain reachable at `/proto/visit-report`). Route/`Inertia::render` strings for these pages must use the group prefix: `MenuVisitPlans/VisitPlans/List` / `MenuVisitPlans/VisitPlans/Create`.
- **No sidebar / menu / rolemenu rows, no `VisitPlanPolicy`, no controller, no FormRequests, no tests.**
- **No `npm run build`** — no route references the new files yet; the build is REQUIRED in the wiring pass or the SPA will hang resolving the pages ([[frontend-build]]). ⚠️ Coupling hazard: Inertia v3's zero-arg page glob bundles everything under `Pages/**` — from the **next** build onward (whoever runs it, for whatever reason) these copies compile in, making their imports load-bearing even without routes: `@/Proto/visitPlanData` (mock), the `Components/Proto/UI/*` set, and the Quotation module's `HistoryPopover`. A Proto cleanup or Quotation refactor that removes those breaks the whole build — coordinate per [[shared-function-convention]] before deleting them.
- **No changes** to `routes/web.php`, `PrototypeController`, `Sidebar.jsx`, any model, or any Sample Order file (parallel sessions own several of those files right now — coordinate before touching `routes/web.php`).

### 3.3 Out of Scope until explicitly scheduled (legacy features, see §8 menu tree)

Report filing (Create/View Visit Report), View All role variants (Head Dept / SM / PM / MM), the 6 calendar views, "Join Visit Another Sales" create variant, Action Plan line items, Link-With (visitplanlink), lookup CRUDs (Visit Report Action, Visit Report Link With), file upload, email, edit/delete/restore.

---

## 4. Users & Permissions (future — pattern locked by precedent, not yet built)

- All routes go inside the `auth` middleware group.
- **`VisitPlanPolicy`** will mirror `SampleOrderPolicy`/`QuotationPolicy`: every ability funnels through `User::canAccessMenuLink('<LinkLaravel>')` — an active `rolemenu` row for the user's RoleID joined to an active, `IsShow=1` menu row whose `LinkLaravel` matches exactly. No admin bypass exists in this app; Administrator works only via real grant rows.
- New menu leaves (created via the Pengelolaan UI at wiring time, like SO menus 1299/1300/1556–1560) will carry `LinkLaravel` values such as `/visit-plans`, `/visit-plans/create`. **Menu IDs are not reserved yet** — the wiring pass must ask the user which views get menus and which roles get grants (LANGKAH 0 of the `new-feature` skill: "Menu ID berapa untuk akses fitur ini?").
- Row-level scoping (who sees whose plans) is legacy-defined per view variant (own / Head Dept / SM / PM / MM / All) — **rules unknown until the legacy files arrive** (§9).

---

## 5. Data Model — VERIFIED against dev DB `colorindochemtrainertia`, 2026-07-02

All 10 tables already have Eloquent models (attribute-driven: `#[Table]`, `#[Fillable]`, `#[ScopedBy(NotDeletedScope::class)]`, `timestamps: false`, PK `ID`), generated and relationship-verified by the `generate-models` / `verify-model-relationships` workflows. All tables are `ENGINE=InnoDB DEFAULT CHARSET=latin1` (⚠️ non-latin1 characters — e.g. emoji in Purpose/Remark — cannot be stored; validate or transliterate at the write pass).

### 5.1 `visitplan` (header) — model `App\Models\VisitPlan`

| Column | Type | Null | Notes |
|---|---|---|---|
| `ID` | int | NO | PK — **`auto_increment`** since the 2026-07-02 migration (§5.6 trap #1, RESOLVED — re-verified 2026-07-21). Max ID at snapshot: **11832** |
| `IsDeleted` | int | NO | 0/1; `creating()` defaults 0 |
| `Tanggal` | datetime | NO | Created date-time |
| `VisitPlanStatusID` | int | YES | FK → `visitplanstatus` (**11 seeded rows** since 2026-07-02 — §5.6 trap #2, RESOLVED; see §5.5) |
| `DivisionID` | int | YES | FK → `division` |
| `UserIDInput` / `UserIDSales` | int | YES | FK → `users` (creator / sales) — snapshot semantics TBD (§9, [[userid-sales-snapshot]]: **ASK the user** before applying) |
| `CompanyID` / `CompanyCPID` | int | YES | FK → `company` / `companycp` |
| `ProjectID` | int | YES | FK → `companyproject` — the proto Create has **no** project field; legacy usage TBD (§9) |
| `CompanyCPOthers` | varchar(500) | NO | Free-text CP when not in master |
| `UserIDVisitWith` | int | YES | FK → `users` (colleague joining the visit) |
| `UserVisitWithOthers` | varchar(500) | NO | Free-text companion |
| `VisitWithPrincipal` | int | NO | Flag (0 in sampled rows) |
| `VisitingPrincipalID` | int | YES | FK → `principal` |
| `VisitingPrincipalName` | varchar(255) | NO | Denormalized principal name |
| `VisitPrincipalCPID` | int | YES | FK → `principalcp` |
| `ScheduleDate` | date | NO | Visit date |
| `ScheduleTime1` / `ScheduleTime2` | time | NO | From / To (stored `08:00:00`; the proto UI uses `08.00` dot-format strings — mapping needed at integration) |
| `Purpose` / `Remark` | varchar(500) | NO | |
| `ReportDate` | datetime | NO | ⚠️ `0000-00-00 00:00:00` until the report is filed (zero-date, §5.6 trap #4) |
| `Discussion` | text | NO | Report body (empty string until reported) |
| `UploadName` | varchar(200) | NO | Report attachment (empty until uploaded) |
| `UploadType` | varchar(300) | NO | |
| `UploadSize` | int | NO | 0 when none |
| `UploadContent` | longblob | NO | ⚠️ Legacy BLOB. **Superseded 2026-07-10**: the `file_uploads` disk convention is locked (CLAUDE.md "File Upload") and Phase 4 shipped it — new rows write `UploadContent=''` (the "bytes are on disk" discriminator) with the file at `file_uploads/visitreport/report{id}.{ext}`. The BLOB is read only as a fallback for legacy rows |

10 FK constraints, all `ON DELETE/UPDATE RESTRICT`: CompanyCPID→companycp, CompanyID→company, DivisionID→division, ProjectID→companyproject, UserIDInput→users, UserIDSales→users, UserIDVisitWith→users, VisitingPrincipalID→principal, VisitPlanStatusID→visitplanstatus, VisitPrincipalCPID→principalcp.

**Live data:** 11,801 rows (11,799 active). Header status distribution (active): **1 Created → 5,062 · 3 Report Submitted → 6,131 · 4 Cancel → 606** (names per §5.5).

### 5.2 `visitplanassignment` (history) — model `App\Models\VisitPlanAssignment`

`ID` int **auto_increment** (⚠️ trap #1 RESOLVED 2026-07-02, re-verified 2026-07-21; max at snapshot **22631**), `IsDeleted`, `Tanggal` datetime, `VisitPlanID` FK, `UserID` FK→users, `VisitPlanStatusID` FK→visitplanstatus, `Comment` text NOT NULL. The `<entity>assignment` audit pattern (one row per status event).

**Live data:** 22,601 rows (~1.9 per plan). Status distribution: **1 Created → 11,843 · 3 Report Submitted → 6,969 · 4 Cancel → 612 · 5 SM Comment → 1,808 · 6 PM Comment → 62 · 7 SM Read → 1,305 · 11 Head Comment → 2**. Statuses 5/6/7/11 appear ONLY in assignments, never on current headers — with the seeded names it's evident the assignment table is a combined status-transition **and** comment/read event log (akin to the SM/PM comment flows on Quotation/SO). Exact flows TBD (§9).

### 5.3 `visitplancompanycpdetail` (multi-CP pivot) — model `App\Models\VisitPlanCompanyCpDetail` — **ACTIVELY USED**

`ID` int **auto_increment** (next 4179), `IsDeleted`, `VisitPlanID` FK, `CompanyCPID` FK→companycp, `CompanyCPOthers` varchar(255) NOT NULL.

**Live data:** 4,149 rows (4,148 active) — only a minority of the 11,799 plans have rows here, while the header also carries a single `CompanyCPID`/`CompanyCPOthers`. **RESOLVED 2026-07-03** (from the legacy create files — §2.1): the **current** create writes contact persons **only to this pivot** and leaves the header `CompanyCPID`/`CompanyCPOthers` **unset**. So the header CP columns are an **older-era** field — an earlier create path (not in the provided files) most likely populated them, which explains why many old plans carry a header CP but no pivot row (and newer ones the reverse). **Port:** write CPs to this pivot; do **not** treat the header CP as authoritative.

### 5.4 Empty-in-dev tables (**as of the 2026-07-02 snapshot** — several now carry rows, see the note below)

> ⚠️ **Correction 2026-07-03 (verified from the legacy create — §2.1):** "0 rows in dev" ≠ "unused by design." The current create **does write** `visitplanfrienddetail` (each *Visit With*) and `visitplanprincipalvisitdetail` (each *Visit With Principal*) whenever those fields are filled — they are empty here only because the dev snapshot has no such data. Genuinely retired = `visitplandetail` / `visitplandetailassignment` (the Action Plan line-items, menu 81 soft-deleted). `visitplanlink` / `visitplanlinkwith` remain the cross-module link tables (deferred).

| Table | Model | Shape (all `ID` auto_increment) |
|---|---|---|
| `visitplandetail` | `VisitPlanDetail` | `Tanggal`, `VisitPlanID`, `ActionID` FK→`action`, `ProjectID` FK→**companyprojectdetailcc** (repointed 2026-08-06, resolves GH #296 — was wrongly `companyproject`; the column always held a CC-line id, matching the sibling quotationdetails/sampleorderdetail tables), `PrincipalID` FK→principal, `BarangID` FK→barang, `Remark` text, `PMRemark` text, `VisitActionStatusID` FK→**visitplanstatus** — the legacy "Action Plan" line items (menu 81, now soft-deleted; PM involvement implied by `PMRemark`) |
| `visitplandetailassignment` | `VisitPlanDetailAssignment` | per-line history: `Tanggal`, `VisitPlanID`, `VisitPlanDetailID`, `UserID`, `VisitPlanStatusID`, `VisitActionStatusID`, `Comment` text |
| `visitplanfrienddetail` | `VisitPlanFriendDetail` | multi-companion pivot: `VisitPlanID`, `UserIDVisitWith` FK→users, `UserVisitWithOthers` varchar(255) |
| `visitplanprincipalvisitdetail` | `VisitPlanPrincipalVisitDetail` | multi-principal pivot: `VisitPlanID`, `VisitWithPrincipal` int, `VisitingPrincipalID` FK, `VisitingPrincipalName`, `VisitingPrincipalCPID` FK→principalcp, `VisitingPrincipalCPName` |
| `visitplanlink` | `VisitPlanLink` | `InsertDate` timestamp (DEFAULT CURRENT_TIMESTAMP **ON UPDATE** CURRENT_TIMESTAMP — the family's only timestamps), `VisitPlanID` FK, `VisitLinkWith` int, `VisitLinkWithID` int. AUTO_INCREMENT=11 → rows existed once, hard-deleted. Cross-module link codes **LWR=1, Quotation=2, SampleOrder=3, VisitReport=4** come from the SO legacy pass — UNVERIFIABLE in this DB (both link tables empty); re-confirm from the legacy files |
| `visitplanlinkwith` | `VisitPlanLinkWith` | link-type lookup: `InsertDate` timestamp, `VisitLinkWithName`, `VisitLinkWithURL` (managed by legacy Pengelolaan menu 319). ⚠️ Its generated `#[Fillable]` includes `IsDeleted` — deviates from the project rule (ID/IsDeleted are never fillable). ✅ **FIXED** at the Phase-6 lookup-CRUD pass — `#[Fillable('VisitLinkWithName','VisitLinkWithURL')]` |

### 5.5 Lookups

- **`visitplanstatus`** — `ID` auto_increment, `IsDeleted`, `VisitPlanStatusName` varchar(50). Was **empty** in dev until the user seeded it on **2026-07-02** (11 rows, mirroring production): **1 Created · 2 InProgress (IsDeleted=1) · 3 Report Submitted · 4 Cancel · 5 SM Comment · 6 PM Comment · 7 SM Read · 8 Pending Action · 9 Submited Action (sic) · 10 Confirm Action · 11 Head Comment**. This grounds the proto badges: Planned ≈ 1 Created, Done ≈ 3 Report Submitted, Cancelled ≈ 4 Cancel (the UI tone mapping itself is still a wiring-pass decision).
- **`action`** — `ID`, `IsDeleted`, `ActionName` varchar(200). Active rows: **1 Promote, 2 Follow Up, 4 "a"** (junk); 3 deleted. Referenced by `visitplandetail.ActionID`; legacy CRUD = Pengelolaan menu 96 (`listvraction1.php`).

### 5.6 ⚠️ Schema traps (verified 2026-07-02 — every future write pass MUST handle these)

1. **`visitplan.ID` and `visitplanassignment.ID` had NO `auto_increment` — ✅ RESOLVED 2026-07-02** (plain `int NOT NULL` PKs; legacy generated IDs manually). Fixed via migration `2026_07_02_134941_add_auto_increment_to_visitplan_ids.php` (guarded, reversible; wraps the `ALTER` in `Schema::withoutForeignKeyConstraints()` because MySQL error 1833 refuses to MODIFY an FK-referenced column even when the type is unchanged). **Applied to dev 2026-07-02, verified**: both columns show `Extra=auto_increment`, all row counts/MAX IDs unchanged (11,801/11832 · 22,601/22631); InnoDB resumes the counter at MAX+1. The models' `incrementing: true` is now correct as-is.
2. **`visitplanstatus` lookup — ✅ RESOLVED 2026-07-02** — `visitplan`, `visitplanassignment`, `visitplandetail(+assignment)` all carry real dev-DB RESTRICT FKs to `visitplanstatus`, which was **empty** (any insert with a status would have failed). The **user seeded the 11 production rows on 2026-07-02** (§5.5), and the seed is captured in migration `2026_07_02_134940_seed_visitplanstatus_lookup.php` (explicit IDs 1–11, no-op where rows already exist, no-op `down()` because live rows FK-reference the IDs). Remaining care: ID 2 "InProgress" is soft-deleted (exclude from option lists), and decide at the write pass whether code hardcodes status IDs (SO precedent) or reads the lookup.
3. **Legacy `0`-sentinels vs NULL — still live (re-verified 2026-07-02)** — existing rows use `0` for "none" in nullable FK columns: `ProjectID`=0 in **11,799** rows, `VisitingPrincipalID`=0 in **11,795**, `UserIDVisitWith`=0 in **7,435**, `CompanyCPID`=0 in **1,501**, zero NULLs anywhere — all violating the RESTRICT FKs (constraints were added with FK checks off). Note: `database/sql/update_database_colorindochemtra.sql.txt` (run ~2026-05-20, `_run_stderr.log` beside it) **contains** `UPDATE visitplan SET <col>=NULL WHERE <col>=0` statements for this family, but today's data does not reflect them (DB likely re-seeded from the dump afterwards) — whether to re-run that normalization is a **user decision**. Until then: new writes must use **NULL**, never 0 (BR-11), and UPDATEs touching old rows can trip FK errors on their untouched 0-columns.
4. **Zero-date `ReportDate`** — `0000-00-00 00:00:00` until reported. Laravel's connection already runs the relaxed sql modes (strict minus `NO_ZERO_DATE`/`NO_ZERO_IN_DATE`, user-approved 2026-06-11), so writing the zero-date at create works — but raw clients in strict mode error even *comparing* it (observed live).
5. **NOT NULL, no-default columns every insert must set explicitly:** header — `CompanyCPOthers`, `UserVisitWithOthers`, `VisitWithPrincipal` (int), `VisitingPrincipalName`, `Purpose`, `Remark`, `ReportDate` (zero-date), `Discussion`, `UploadName`, `UploadType`, `UploadSize` (0), `UploadContent` (empty — longblob); assignment — `Comment`.
6. **latin1 charset** on all family tables — reject/normalize non-latin1 input.

---

## 6. Phase 1 Page Inventory (what shipped, verbatim behavior)

### 6.1 `VisitPlans/List.jsx` (component `VisitPlansList`)

- Header "Visit Report" + subtitle "Daftar rencana & laporan kunjungan ke pelanggan." and a **Create Visit Plan** button → still `route('proto.visit-report.create')` (TODO comment marks the repoint; SO's FR-2.6 precedent).
- Client-side table over the mock `PLANS` array (8 rows): resizable columns ID / Division / Company / Schedule (date + from–to time) / Meet With / Purpose / Sales / Status (`StatusBadge` tones: Planned→primary, Done→success, Cancelled→danger).
- Client-side search (company/purpose/sales/meet/id), Division filter, Status filter, count pill, shared `ListFooter` pagination (10/25/50).
- ⚠️ *(Phase-1 snapshot — SUPERSEDED)* Rows had **no click action** and the proto had no Detail/Show page. **Since Phase 4/5 a row click opens the Visit Report** (`visit-plans.report`), which is the module's de-facto detail screen; tier lists open a subordinate's report the same way. Closes §9.14.
- Imports all resolve to real shared modules (`StatusBadge`, `ListFooter`, `useResizableColumns`, `AppLayout`); layout = array form `VisitPlansList.layout = [AppLayout]`.

### 6.2 `VisitPlans/Create.jsx` (component `VisitPlanCreate`)

- Breadcrumb + Back to List (→ proto index route).
- **Visit Plan Information card:** Division*, Schedule Date*, Schedule Time* (From/To selects, 06.00–20.30 half-hour dot-format strings), Company* (`SearchableSelect`); company info panel (address/phone from mock `COMPANY_INFO`); Meet With* (company CP), Visit With (friends), Visit With Principal, Principal CP; Purpose* (⚠️ **no client-side length cap** — the server must enforce the varchar(500)), Remark* (maxLength 500; both DB columns are varchar(500)).
- Prototype submit: required-field check → `window.confirm` → `window.alert` ("not saved to server") → redirect to proto index. **To be replaced** with `useForm` + toast + loading-spinner pattern per the `new-feature` skill at integration.
- **Company Context card:** 6 read-only reference tabs (Company Production / Company Product / Company Visit Report / Details Sample Order / Details Quotation / Details LWR) rendered from mock `@/Proto/visitPlanData` via a generic `DataTable` with `HistoryPopover` cells. Real data sources at integration time likely overlap the Company module's tab panels (`Components/MenuCompanies/CompanyTabPanels.jsx`) and the SO/Quotation/LWR read endpoints — **owned by other sessions**; per [[shared-function-convention]], consume shared endpoints or ask the owners, do not fork their code.
- Form gaps vs schema (proto simplification, resolve at integration): no Project picker, single CP despite the multi-CP pivot, single companion despite `visitplanfrienddetail`, no link-with, no action-plan lines, mock strings as option values (integration must post **IDs**, `useForm` FK fields init `null` — `inertia-v3-stack` rules).

---

## 7. Legacy Menu Tree (dev DB `menu` table, verified 2026-07-02)

Root **79 "Visit Report"** (ParentID NULL, Sort 12, IsShow 1) — children (active unless noted):

| ID | ShowName | Legacy Link | Sort |
|---|---|---|---|
| 80 | Create Visit Plan | `createvisitplan.php` | 0 |
| 100 | Create Visit Plan Join Visit Another Sales | `createvisitplananothersales.php` | 1 |
| 82 | Create Visit Report | `listvisitplanreport.php?StatusID=1` | 2 |
| 83 | View Visit Report | `listvisitplanreport.php` | 3 |
| 84 | View All - Visit Plan | `listvisitplanreportall.php` | 4 |
| 429 | View All Visit Plan Head Dept | `listvisitplanreportallheaddept.php` | 5 |
| 85 | View All Visit Plan SM | `listvisitplanreportallsm.php` | 6 |
| 95 | View All PM - Visit Report | `listvisitplanreportallpm.php` | 7 |
| 388 | View All Visit Plan PM | `Untitled-1.php` (sic) | 8 |
| 121 | View All Visit Plan MM | `listvisitplanreportallmm.php` | 9 |
| 89 | View Calendar Visit Report All | `listvisitplancalendarall.php` | 10 |
| 88 | View Calendar Visit Report | `listvisitplancalendar.php` | 11 |
| 428 | View Calendar Visit Report Head Dept | `listvisitplancalendarheaddept.php` | 12 |
| 92 | View Calendar Visit Report SM | `listvisitplancalendarsm.php` | 13 |
| 124 | View Calendar Visit Report MM | `listvisitplanbydatemm.php` | 14 |
| 94 | View Calendar Visit Report PM | `listvisitplanbydate.php` | 15 |

Soft-deleted children: 81 "Action Plan" (`listvisitplanactionplan.php`), 427 "View All Visit Plan Head Dept (Approval (blm ditentukan penggunaannya))".

Under **Pengelolaan** (root 11) → **131 "Visit Report"**: 96 "Visit Report Action" (`listvraction1.php`, Sort 1), 319 "Visit Report Link With" (`listvisitplanlinkwith.php`, Sort 2).

`Create Visit Report` filters `StatusID=1` — consistent with the now-seeded names (§5.5): **1 Created** = awaiting report, **3 Report Submitted**, **4 Cancel**. Transition rules still come from the legacy files (§9).

**Legacy files — RECEIVED 2026-07-02** (`Just An Example, Do not Link/Create Visit Plan/`): `createvisitplan.php` + 8 companions — `listcompanyproduction.php`, `listcompanyproductionhistorypopup.php`, `companyProductionAction.php`, `listcompanyproduct.php`, `listvisitreporthistoryview.php`, `listsocompany.php`, `listquotationcompany.php`, `listlwrcompany.php` (the Create page's six Company-Context tab sources + history views).

**Legacy files — FULL REMAINDER RECEIVED 2026-07-13** (~30 files; `createvisitplananothersales.php` (already used by the shipped Join-Another-Sales slice), `listvisitplanreport.php`, all five `listvisitplanreportall*{,view}*.php` tier variants + their `createvisitplanreport{sm,headdept,mm,all}.php` write-handlers, the PM queue, the Action screens, the `vrlinkwith*` link-create handlers, the full `All Calendar/` set, `createvisiplanreportsa.php`, XLS exports, `listvisitreportgraph.php`, `getpmapproval.php`). All under `Just An Example, Do not Link/`, **never committed** (Aturan Absolut #0).

**Still absent after the batch (reconciled by the 2026-07-13 gap audit — none block Bucket A):** `listvisitplanbydateview{,mm}.php` (PM/MM calendar render panes) · `listvisitreportgraph{sm,mm}.php` (graph tier siblings) · `createcompanyprojectnonheader.php` + `getcompanyprojectdetailid.php` (To-Project cascade — CompanyProject module, out of scope) · `listcompanycp1.php` (CP form UI) + `listcompanyproductedit.php` · `suggest_barang.php`, `getbarangfromprincipal.php` (typeahead helpers → re-implement as Laravel endpoints) · the `*viewexport*.php` XLS set · `listvraction1.php` (menu 96) + `listvisitplanlinkwith.php` (menu 319) lookup CRUDs. **Non-issues:** `Untitled-1.php` (menu 388 broken link; real PM read = `listvisitplanreportallviewpm.php`) · `listvisitplanactionplan.php` (deleted menu 81).

---

## 8. Roadmap (proposed — each phase re-confirms scope with the user first)

| Phase | Content | Gate |
|---|---|---|
| **1 ✓** | Files-only copy + PRD (this document) | shipped 2026-07-02 |
| **2** | Wiring: real routes + `VisitPlanController` (read), `VisitPlanPolicy`, menu/rolemenu rows, sidebar, rebuild via `bash nodebuild.sh` from the outer git root `/home/itccj/server/LaravelInertia/` (README rule 7 — NOT `npm run build`), Pest read tests. List integration decisions: server pagination/search/sort (SO List is the template), which columns, status-name mapping | status names known (§5.5); needs the view-screen legacy files for row scoping + user decisions on menus/roles; coordinate `routes/web.php` with parallel sessions |
| **3** | Create/store: `StoreVisitPlanRequest`, transactional insert (header + assignment + CP pivot?), option endpoints (companies scoped how? friends? principals?), Create page → `useForm`. Groundwork done 2026-07-02: status seed + AUTO_INCREMENT migrations (§5.6 #1–2) | `createvisitplan.php` **received 2026-07-02** — needs the rules-extraction pass (§9) first |
| **4 ✓** | Visit Report filing: `report()`/`updateReport()`/`reportFile()` — in-place status 3/4 + `ReportDate` + `Discussion` append + one `visitplanassignment` row; upload → `file_uploads/visitreport/report{id}.{ext}` (gated download + legacy-BLOB fallback); `Report.jsx` port (filing slice wired, rest inert stubs); List row-click; OWNER-only Policy; gate-reject 422. Create-form: Division derived from the company (read-only). | ✅ **SHIPPED 2026-07-13** — #102 closed; commits `4c6321c`+`c8a02fa`; follow-ups #127/#128; #106 item 2 done |
| **5+** | View All role variants (Head Dept/SM/PM/MM), calendar views, Action Plan lines, Link-With, lookup CRUDs, email hooks. **Join-Another-Sales ✅ SHIPPED 2026-07-13.** | per-user scheduling |
| **6 ✓** | Bucket B (Action items + PM approval queue) + Pengelolaan lookup CRUDs + 3 emails + #128 sanitize + XLS export + per-sales graph + #106 | ✅ **SHIPPED 2026-07-17** |
| **7 ✓** | ACTION-line **tier comments** (head/sm/mm → one `visitplandetailassignment` row: status 5/6/11, `VisitActionStatusID` NULL; comment-only; plan status 1/3; readable by the PM) + **author-permission fix** (`manageActions` = creator **OR** sales; `report()` untouched) + per-line history rendered on the report page AND the PM queue | ✅ **SHIPPED 2026-07-21** — 149 VP+VisitReport tests green (696 assertions) |

Per CLAUDE.md issue-tracking rule: at the end of each phase, file GitHub issues for all remaining follow-ups before moving on.

**Tracking issues (filed 2026-07-03):** Phase 2 → [#100](https://github.com/ColorindoChemtra/ColorindoChemtraInertia63/issues/100) · Phase 3 → [#101](https://github.com/ColorindoChemtra/ColorindoChemtraInertia63/issues/101) · Phase 4 → [#102](https://github.com/ColorindoChemtra/ColorindoChemtraInertia63/issues/102) · Phase 5+ → [#103](https://github.com/ColorindoChemtra/ColorindoChemtraInertia63/issues/103). (§9.3/§9.4/§9.5 answered by §2.1; the rest map to these issues.)

**Phase-4 status (2026-07-13):** Phase 4 **SHIPPED** — [#102](https://github.com/ColorindoChemtra/ColorindoChemtraInertia63/issues/102) CLOSED (commits `4c6321c` + `c8a02fa`). Follow-ups filed: [#127](https://github.com/ColorindoChemtra/ColorindoChemtraInertia63/issues/127) (file-upload extensionless-ext bug, LWR+VP) · [#128](https://github.com/ColorindoChemtra/ColorindoChemtraInertia63/issues/128) (report follow-ups: Discussion display+sanitize, sales-admin gate, stub sections). [#106](https://github.com/ColorindoChemtra/ColorindoChemtraInertia63/issues/106) item 2 (Division derived-from-company) RESOLVED (`c8a02fa`).

**Phase-5 progress (2026-07-13):** first slice — **Create Visit Plan "Join Another Sales"** (on-behalf, legacy menu 100) — SHIPPED: a HIDDEN capability `/visit-plans/create-for-others` + a "Join Another Sales" button on the View list (Sample Order create-sales-admin parity); `createForOthers`/`salesCompanies` + scope-aware `store()` (UserIDInput=actor, UserIDSales + DivisionID DERIVED from the chosen company), searchable Sales→company cascade. 2-subagent adversarial verify → SHIP. Resolves §9 Q9. Tracked under [#103](https://github.com/ColorindoChemtra/ColorindoChemtraInertia63/issues/103).

**Phase-5 remaining-legacy batch + gap audit (2026-07-13):** the user pushed the full remainder of the legacy tree (~30 files — see §7), which **file-unblocks buckets A (View-All tiers + comment/read writes), D (Link-With create), and E (calendar grid)**. An 8-subagent read-only gap audit reconciled it against the built app. Net: the bottleneck moved from *missing files* → *unresolved decisions + build work* (`VisitPlanController` is still own-list only; `Report.jsx`'s 4 sections are still inert stubs). **Audit corrections to prior assumptions:** MM tier scopes on `userdivision.IsReadReport=1` (company division), NOT `userprincipal.IsMM`; **PM is a WRITE tier** (batch `btn-submit` → `visitplandetail.PMRemark` + status-6 rows); status 6 is dual-written by the MM header page and the PM line batch; `createvisiplanreportsa.php` is the fully-wired blueprint for all 4 `Report.jsx` stubs. **Recommended next slice = Bucket A read-only tier lists** (`all`+`head`+`sm`+`mm`) — fully file-unblocked, `SampleOrderController` is the template, and its scope layer is the blocking dependency for calendars (E) and the comment/PM writes. Decisions to lock before coding it: HEAD scope column, MM `IsReadReport` rule, list status-filter source, per-tier rolemenu grants (menus 84/429/85/95/388/121 — NOT the create role set), and confirming PM-line/graph/exports are deferred. Residual still-absent files (§7) do not block it.

**Phase-5 build session (2026-07-16) — Bucket A + D1 + D2 shipped (uncommitted; #103):** `VisitPlanController` now serves the read-only **View-All tiers** (all/head/sm/mm — scopes VERIFIED from the legacy row-query files: head=`UserIDSales`-only + `HeadID` subtree, sm=`IsHeadDiv` on `vp.DivisionID`, mm=`IsReadReport` on `company.DivisionID`; tier leaves seeded IDs 3582–3585 to the real legacy audiences); **D1** report-time Visit Participants editing (`updateParticipants`, 3 pivots, owner-gated, no status gate); **D2** Link-With **VP-side only** (writes `visitplanlink` only — the reciprocal on SampleOrder/Quotation/LWR is deferred = #36). **64 VisitPlan tests green**; each slice adversarially reviewed (5 real bugs caught + fixed pre-ship). Bucket A + D1 BUILT & published; D2 build pending (quiet-tree watcher). Answers §9 **Q6** (tier row scoping) + **Q11** (Link-With, VP side). ⚠️ schema note: `userdivision` has `IsHeadDiv`/`IsReadReport` — **no `IsHeadDept` column**. Still TODO in Phase 5: tier comment/approval WRITES (statuses 5/6/7/11), calendars (E), D3 (multi-participant on the CREATE form), Company Context (C). 🔧 cci-mysql crash fixed (log-file permission).

**Phase-5 completion session (2026-07-17) — Bucket A tier WRITES + C + E + D3 + "Create Visit Report" SHIPPED & committed (#103):** `96 VisitPlan tests green (442 assertions)`, built & published, two read-only adversarial reviews (all findings fixed). New shared service `App\Services\VisitPlanAccess` (per-plan tier capabilities `{owner,all,head,sm,mm}` + `subtree`). **Bucket A completed** — the report page is now ONE capability-gated screen reachable by in-scope tier managers from the tier lists: SM Comment (5) / SM Read (7) / MM (6) / Head (11) each append a coloured block to `Discussion` + one `visitplanassignment` row and do **not** mutate the header status (faithful to `createvisitplanreport{sm,headdept,mm}.php`); view-all gets submit/cancel (`updateReport`, +ReportDate) + arbitrary change-status (`statusChange`, no ReportDate, legacy btn-change); participants-edit + attachment-download widened to in-scope managers. Email DEFERRED (per Phase 4). **Bucket C — Company Context**: 6 lazy per-tab read-only reference tabs for the plan's company (Production/Product/Visit-History/SO/Quotation/LWR), **essential columns** (user decision), `viewReport`-gated, schema-verified. **Bucket E — Calendars**: month-grid `Calendar.jsx` (proto ViewCalendar2), own + 4 tier variants reusing `gateView`/`applyViewScope`, reached via a **list-header "Calendar" button** ⚠️ *(SUPERSEDED 2026-07-21: there are SIX calendars — a `pm` variant also shipped — they gate on their own `gateCalendar()`, and menu rows 3966–3971 DO put them in the sidebar)*. **Bucket D3**: multi-participant (0..n) Meet-With/Visit-With/Principal on the CREATE form → the 3 pivots (backward-compatible with the single fields). **"Create Visit Report"** (legacy menu 82): a `view=createReport` = own list defaulted to status Created, new visible sidebar leaf `/visit-plans/create-report` (menu 3586). Answers §9 **Q1** (status 5/6/7/11 transitions built) + **Q8** (calendars, grid via month window). ⚠️ latent (unreachable) note: the legacy single-field participant inserts still run alongside the D3 array loops — a crafted request posting both could duplicate a pivot; kept because the single-field create/on-behalf tests use them.

**Phase-6 session (2026-07-17) — Bucket B + lookup CRUDs + emails + #128 SHIPPED (uncommitted; #103):** full VP+VisitReport suite **125 tests green (568 assertions)**, built & published. ⚠️ **NOT committed** — `routes/web.php` still carries another session's company-projects/netsuite hunks (#0 = user chose "wait"). **Bucket B** (Action items + PM queue): owner-managed `visitplandetail` lines (ProjectID NULL — To-Project cascade OUT of VP scope; `actionStore`/`actionUpdate`/`actionBarang` on the Report page) + the **PM approval queue** (`/visit-plans/approval-pm`, page `ApprovalPm/Index.jsx`, per-line `userprincipal.IsHeadDiv` scope, Pending/Submited/Confirm → `VisitActionStatusID` 8/9/10 + `PMRemark` + status-6 header & line rows). **Lookup CRUDs** (Pengelolaan menu 96/319, parent 131): Visit Report Action master (`action`) + Link-With lookup (`visitplanlinkwith`, model fillable fixed — dropped `IsDeleted`/`InsertDate`). **Emails** ⚠️ *("log driver" is STALE — `.env` is now `MAIL_MAILER=smtp` via Brevo, and the sends are QUEUED `Send*Emails` jobs needing a running worker)*: 3 wired Mailables (ToHeadDivision/ToPM/ToUser) + shared `mail/visit-plan-document.blade.php`, post-commit fail-soft, subjects/From verbatim; `PMToUser` deferred (no clear trigger — orphan avoided). **#128** server-side Discussion sanitize (`GeneralFunction::scrubHtml`); #127 VP-side already guarded. **XLS tier-list export** (shared ExcelJS + Web Worker foundation — `visitPlanExport` spec + `exportData()` sharing the extracted `scopedListQuery()` with `index()`; 50k cap, 1000/page) + **per-sales stacked-bar graph** (`graph()`, legacy `listvisitreportgraph.php`, via **Recharts** added to package.json) — routes `visit-plans.export`/`.graph`, list-header buttons, `VisitPlanExportTest`; **130 VP+VisitReport tests green (598 assertions)**. Answers §9 **Q10** (Action Plan resurrected: render + sales-add + PM queue; To-Project cascade stays OUT = CompanyProject module) + §9 **Q13** (emails — answered "log driver" at the time; the driver is now live SMTP). **Cleanup pass (same day, "clean it up"):** wired the SM/MM/Head tier-comment email (`sendEmailVisitPlanToUser`, was missing from `commentStore`); **#106** create-form Company Context now shows REAL data (shared `CompanyContextTabs` keyed by `CompanyID` via `visit-plans.context-company`; mock removed); the single-field participant double-insert is **GUARDED** (single-field branch runs only when its D3 array is absent). `VisitPlanCleanupTest`; **133 tests green (605 assertions)**. **Email delivery fix + single-field retirement (same day, "fix everything"):** VP Mailables now send From `config('mail.from.address')` (Brevo-verified sender; hardcoded `do-not-reply@` was silently rejected by the relay); the single-field participant path DELETED (branches + request rules removed; tests migrated to arrays). **Not VP work / user decisions:** Company Context full-column parity (chose essential); Link-With reciprocal #36 (the target modules' own side); commit/#0 (wait).

**Phase-7 session (2026-07-21) — ACTION-line tier comments + author-permission fix SHIPPED:** full VP+VisitReport suite **149 tests green (696 assertions)**, Pint clean, built & published. **Not a legacy port** — verified 2026-07-20 that legacy has NO tier concept on action lines (the SM/MM/Head report pages merely `<iframe>` the owner's `listvraction.php`, which hardcodes `VisitPlanStatusID=1` and has no owner gate; deliberately not replicated). **(A) Per-line tier comment** — `actionComment()` + `StoreVisitPlanActionCommentRequest` + `POST visit-plans/{visitPlan}/actions/{detail}/comment`: writes ONE `visitplandetailassignment` row (`VisitPlanStatusID` sm **5** / mm **6** / head **11**, `VisitActionStatusID` **NULL**, scrubbed `Comment`) and mutates **nothing** on `visitplandetail` — the line's `VisitActionStatusID` + `PMRemark` remain the PM's alone. Gates in order: `authorize('viewReport')` → line-belongs-to-plan + `IsDeleted=0` → plan status ∈ {1,3} → the requested tier re-checked against `VisitPlanAccess::capabilities` **server-side** → `scrubHtml(str_replace("\r\n",''))` (#128). COMMENT-ONLY: a tier may never create/edit/delete a line. **No email** (user decision — the spec was silent, and inventing behaviour is forbidden). **(B) Author permission** — `VisitPlanPolicy::manageActions()` now has its OWN rule (`allowed && (ID === UserIDInput || ID === UserIDSales)`), so on an on-behalf plan the SALES can finally work their own action lines; `report()` was deliberately left untouched because it also gates Submit Report / Cancel Visit / attachment download (widening it would have silently handed submit+cancel to the sales). **UI** (user decision 2026-07-21): the per-line `history` payload shipped in both the report and PM-queue props but **no JSX rendered it** — so a to-spec comment would have been invisible in the running app, including to the PM that the storage decision requires to read it. New shared `Components/MenuVisitPlans/ActionLineHistory.jsx` now renders it in `Report.jsx`'s `ActionSection` (expandable per row, auto-open for a commenting tier, with the comment box) **and** in `ApprovalPm/Index.jsx`; the box appears only for the tier you arrived as, via a new server-derived `capabilities.commentAction` (arriving tier ∩ grant ∩ status window). **MM↔PM label collision ACCEPTED** (user, 2026-07-20): `visitplanstatus` has no MM row and status 6 is shared with the PM queue, so an MM line comment reads back as **"PM Comment"** — no status row added, no override, no renumbering. `VisitPlanActionCommentTest` (12), including a **regression guard that the PM can read tier comments** in the approval queue (without it, a later status filter in `actionHistory()` would hide them with nothing failing).

**Module RECHECK + fixes (2026-07-21, straight after Phase 7):** three read-only audits (authorization / frontend / docs-vs-code), every finding hand-verified before acting. **157 tests green (725 assertions).** Four fixes: (1) **`viewReport` now admits the plan's SALES** — Phase 7 had widened the action-line WRITE to creator-OR-sales but left the READ at creator-or-tier, so an on-behalf sales could POST a line yet got 403 on the page that posts it; `report()`/`fileReport()` untouched so Submit/Cancel/Link-With stay out of their reach. (2) **Cross-company leak closed** — `companyContextForCompany()`/`companyCps()` accepted any `{company}` behind only the plain `/visit-plans` grant, exposing other salespeople's quotation/SO/LWR rows and unit prices; now scoped to `company.UserIDSales` unless the actor holds `/visit-plans/create-for-others`. (3) **PM calendar crash** — `Calendar.jsx` derived a non-existent `visit-plans.view-pm` route name and Ziggy hard-throws, killing the page for the 6 granted roles; replaced with an explicit route map. (4) **PM queue batch** no longer acts on checked-but-filtered-out lines. Filed rather than fixed: **#153** (tier line comments notify nobody), **#154** (`actionHistory()` process-lifetime static — stale under Octane), **#155** (**no queue worker → all business email silently never sends**; 2 VP jobs stranded, and `MAIL_MAILER` is live Brevo SMTP so starting a worker delivers them for real).

---

## 9. Open Questions (resolve from legacy files / user before the relevant pass)

1. **Status lifecycle** — names are seeded (§5.5) but transitions are not: when does a plan move 1→3/4? Which screens write assignment rows 5 SM Comment / 6 PM Comment / 7 SM Read / 11 Head Comment? What uses 8/9/10 (Pending/Submited/Confirm **Action** — presumably the Action Plan flow, whose menu is deleted)? Why is 2 InProgress soft-deleted? → **UPDATE 2026-07-13:** the tier write-handlers arrived — each of statuses 5/6/7/11 appends to `visitplan.Discussion` + inserts ONE `visitplanassignment` row and does **not** mutate the header `VisitPlanStatusID`; 8/9/10 are the Action-line lifecycle (`visitplandetail.VisitActionStatusID`, Bucket B) written by the PM-queue batch. Transitions now decodable; **decisions** remain (status-6 is dual-written by the MM header page + the PM line batch — keep the "MM Comment" label or normalize).
2. **ID generation** — how does legacy mint `visitplan.ID` / `visitplanassignment.ID` (MAX+1? where locked?), and do we seed AUTO_INCREMENT instead (structural change → user approval)?
3. **Create insert set** — which tables does `createvisitplan.php` write (header + assignment + companycpdetail? friend/principal details ever?), which fields are derived (Division from company? sales from company like SO?), initial status + assignment comment?
4. **`UserIDSales` semantics** — snapshot of `company.UserIDSales` at create (like SO/Quotation) or the actor? Display the snapshot or the current rep? Per [[userid-sales-snapshot]] this must be explicitly re-confirmed with the user for this module even though snapshot is the default.
5. **Header CP vs multi-CP pivot** — write both? Header = primary? When did legacy start writing the pivot?
6. **Row scoping per view variant** — own/HeadDept/SM/PM/MM/All rules (SO used `users.HeadID` subtree, `userdivision.IsHeadDiv`, `userprincipal` — does Visit Plan reuse these?). → **UPDATE 2026-07-13:** tier files arrived. Audit scopes: own=`vp.UserIDInput`; all=unscoped; head=`vp.UserIDSales` (self + recursive `users.HeadID` subtree — **differs from SO's `UserIDInput OR UserIDSales`**); sm=`userdivision` (IsHeadDept/IsReadReport) on `vp.DivisionID`; **mm=`userdivision.IsReadReport=1` on `company.DivisionID` (NOT `userprincipal.IsMM`)**; pm=line-grain `userprincipal.IsHeadDiv=1`. Buildable now; the scope-column choices are **decisions** to confirm.
7. **Report pass rules** — ✅ **RESOLVED (Phase 4, 2026-07-13):** OWNER-only (`visitplan.UserIDInput == actor` + `/visit-plans` grant); a report can be RE-filed at status 1 or 3; submit → status 3, cancel → 4; upload pdf/doc(x)/xls(x)/ppt(x)/png/jpg ≤ 2 MB. Remaining follow-ups (Discussion display+sanitize, sales-admin-on-behalf gate) in #128.
8. **Calendar views** — required for the port, or dropped? → **UPDATE 2026-07-13:** calendar legacy arrived — grid (own/all/head/sm via `listvisitplancalendar*.php` + shared renderer `listvisitplancalendarview.php`) is complete; **PM/MM render panes `listvisitplanbydateview{,mm}.php` are still absent**. Grid keys on `ScheduleDate`, columns per creator (`UserIDInput`). Still a **scope decision**; if in, it depends on Bucket A's scope layer (`ViewCalendar.jsx` is currently a mock stub, not wired to any controller).
9. **"Join Visit Another Sales"** (menu 100) — ✅ **RESOLVED (2026-07-13):** identical to the normal create EXCEPT `UserIDSales` = the chosen company's sales (creator stays `UserIDInput`); Division is also derived from the company. A HIDDEN capability (`/visit-plans/create-for-others`, IsShow=0) reached via a "Join Another Sales" button on the View list, granted to menu-100's roles. Shipped (Sample Order create-sales-admin parity).
10. **Action Plan** (deleted menu 81, empty tables, `PMRemark`) — resurrect or ignore? → **UPDATE 2026-07-13:** Action legacy present & analyzed (`listvraction{,edit1,historypopup}.php` + the PM queue `listvisitplanreportall{,view}pm.php`). Lifecycle 8 Pending → 9 Submited → 10 Confirm on `visitplandetail.VisitActionStatusID`; **PM is a WRITE tier** (per-line `userprincipal.IsHeadDiv=1`, batch buttons write `PMRemark` + status-6). Decision still open: resurrect vs ignore, and which slice — read-only render / thin sales-add (`ProjectID=NULL`) / PM queue / To-Project cascade. **To-Project = CompanyProject module (out of Visit-Plan scope; its `createcompanyprojectnonheader.php` is not present).** **UPDATE 2026-07-17/21:** RESURRECTED and shipped (Phase 6 render + sales-add + PM queue; Phase 7 per-line head/sm/mm comments). To-Project stays out of scope. ⚠️ The tables are **no longer empty** in dev — tests must seed their own rows AND must never assert global row counts.
11. **Link-With** — visit-plan side of the cross-module links (SO deferred its side as issue #36) — coordinate so both sides use the same codes (LWR=1, Quotation=2, SampleOrder=3, VisitReport=4). → **UPDATE 2026-07-13:** `vrlinkwith{sampleorder,lwr,quotation}.php` CREATE handlers arrived (`linkfrom=4`; reciprocal write decoded); DISPLAY (join `visitplanlink`⋈`visitplanlinkwith`) + UNLINK (`IsDeleted=1` on `visitplanlink` only, one-sided) also specified. `visitplanlinkwith` is empty in dev → **hardcode** the code→name map (SO precedent), don't rely on `VisitLinkWithURL`. Buildable now; decisions on multi-row + report-time status-gate.
12. **Display naming** — sidebar/menu label "Visit Plan" vs legacy "Visit Report"; page H1s currently say "Visit Report" (copied verbatim).
13. **Email hooks** — does legacy send mail on create/report? ✅ **ANSWERED** (Phase 6): create → head-division, tier comment + PM batch → creator/sales, action line → principal PMs. ⚠️ **The "dev default = log driver" premise is DEAD** — `.env` is `MAIL_MAILER=smtp` (Brevo), sends are queued jobs, and no worker currently runs. Re-check `.env` every time; never assume.
14. **Per-plan Detail/Show UX** — the proto has no Detail page and List rows have no click action; does the port get an SO-style Detail (row click → show) or does the legacy "View Visit Report" screen define the shape?

---

## 10. Binding Conventions for Every Future Pass

- **Invoke the `new-feature` skill** (TDD-first, toast + inline errors, loading spinner, edge-case table) and **`inertia-v3-stack`** before writing code — CLAUDE.md hard rule.
- `SHOW CREATE TABLE` before touching any model; never trust generated config blindly (§5.6 #1 proves why).
- FormRequest validation only (no inline `validate()`), `Rule::exists(...)->where('IsDeleted', 0)` on every FK, Policy + `authorize()` on every action, `NotDeletedScope` semantics, no axios, PascalCase columns as-is.
- Shared code goes to `app/Support/GeneralFunction.php` / `app/Services/` / `resources/js/lib/` on second use — and only within modules this user owns ([[shared-function-convention]]).
- Ask-first protocol ([[ask-first-protocol]]): shell commands announced with standing approvals; unclear scope → ask, never guess.
- Explain every edit and recommend the next step (user rule 2026-07-02, README rule 9): announce intended edits beforehand; report **file → what changed → why** after each chunk; end every report with the recommended next step.
- Frontend freeze (user rule 2026-07-02, README rule 10): existing FE files' **design is frozen** — minimal wiring edits only (mock→props, `useForm`, route repoints), and only when the file is not hot per Aturan Absolut #0b; visual changes need user approval; new FE files are modeled on the equal-function sibling (SO/Quotation), never a new design.
- Ultracode / multi-agent discipline (user rule 2026-07-02, README rule 11): the main agent writes ALL code/file edits; subagents are read-only verifiers and run only after the code exists.
- `npm run build` (`bash nodebuild.sh`) after any JSX change once the pages are routed; hard-refresh after rebuild.
- Tests: Pest + `DatabaseTransactions` against the dev DB, in-memory `User::factory()->makeOne(['id' => 1])` for `actingAs`, unique `TEST*` data prefixes, **never** `migrate:fresh`.
- **Shared-state discipline (README rules 5/7/12/13):** ONE git tree / ONE dev DB / ONE build bundle for all parallel sessions — never destructive git (`reset --hard`/`stash`/`clean`/`checkout -- .`), never bulk-mutate the 11,799 existing rows, plain `migrate` is repo-global (runs others' pending + auto-commits over Pest), and `bash nodebuild.sh` (from the outer git root `/home/itccj/server/LaravelInertia/`) is a global singleton publish — don't build while another session may be, and sweep the WHOLE build input — `resources/js` AND `resources/css`, vite's two declared inputs — for hot/foreign WIP immediately before building (README rule 7). `migrate` only migrations YOU authored this session, via `--path` (README rule 5 — the positive list is ownership, not authorship; sole exception: a prior session's migration the user explicitly confirms as approved handoff, read in full first). Commit only by explicit pathspec, never a bare `git commit` on the shared index; stage only what you created/edited this session. ⚠️ A pathspec commit records WORKING-TREE content and ignores the index (`git add -p` does not survive it — and is interactive, unusable in this harness): before committing, `git diff HEAD -- <file>` each file and confirm every hunk is yours; a shared file with any foreign hunk is EXCLUDED → STOP and ask (README rule 12 + §5 On-end). Backlog exception: prior-session files may be staged when the user explicitly directs the commit, after per-file review + user confirmation (README §5 On-end).
- **Recovery, never a destructive reset:** build fails on a file you don't own → STOP (rule 8); migration errors → no `migrate:fresh`/blind rollback (seed `down()` is a no-op), reconcile manually via `migrate:status` + `SHOW CREATE TABLE`; crashed Pest → clean `TEST*` rows child-before-parent; a legacy file that `include`s a missing one → request it by name, never guess.
- **Handoff:** re-read + MERGE the README/PRD before the end-of-session status write (don't overwrite a parallel session's update); record user decisions into "Locked decisions" (dated) the same session; `gh issue list` before filing, then write issue numbers back into §8/§9; commit tag `feat(visit-plan):`.
- **Two gates + advisor (README §1, rule 14):** (1) READ all of §2 — this README, the PRD, present legacy files, the SO reference, the proto, **CLAUDE.md in full**, and the **memory files** — before any edit/command/recommendation. (2) SCOPE in three tiers: edit Visit Plan's own files freely; edit its **shared integration points** (`routes/web.php`, `Sidebar.jsx`, `.gitignore`, menu/rolemenu rows) only coordinated (rule-2 + announce first — they're hot, but they ARE VP's wiring, not foreign); **never edit** other modules or Proto originals, but always **read + recommend** them (e.g. "pull X from the proto menu"). Reading and recommending anything is always fine. And be a smart advisor — tell the user what can/can't, recommend, push back on mistakes with reasons, ask when unsure, don't just agree. (Carry-over: kept Visit-Plan-scoped for now, no global memory — user's call 2026-07-03; if this ruleset is cloned for another menu, copy rule 14's verbatim user line over too.)

## 11. References

- Reference module: `Pages/MenuSampleOrders/` + `SampleOrder PRD.md` (structure/patterns only — read-only for this module's work).
- Proto originals: `Pages/Proto/VisitReport/{Index,CreateVisitPlan,ViewCalendar}.jsx`, mock data `resources/js/Proto/visitPlanData.js`, routes `proto.visit-report.{index,create}` (`PrototypeController::visitReportIndex/Create`).
- Models: `app/Models/VisitPlan*.php` (10 files), `app/Models/Action.php`.
- Dev-DB normalization script (source of the family's FKs + the not-yet-applied 0→NULL updates): `database/sql/update_database_colorindochemtra.sql.txt` + `_run_stderr.log`.
- Legacy link helper already in-repo: `Pages/MenuSampleOrders/Just An Example, Do not Link/Global Function/listlinkwithvisit.php` (SO-side view of visit links).
- Memories that bind here: `dev-db-fk-drift`, `userid-sales-snapshot`, `shared-function-convention`, `ask-first-protocol`, `php-via-docker-cci`, `frontend-build`, `mail-log-driver-dev`.
