# CLAUDE.md

Guidance for **Claude Code (claude.ai/code)** when working in this repository.

---

## 1) Project Overview

This repository contains a **custom Dolibarr ERP module** named **Planning** (`/custom/planning/`).  
It provides manufacturing planning views for **MRP Manufacturing Orders (MO)** grouped by **Working Station**, including a **Planning Board** with auto-refresh.

### Key Pages
- `planning.php` — table view grouped by working station (live MOs)
- `board.php` — **Planning Board** (primary interactive view)
- `completed.php` — completed/finished view
- `board_data.php` — AJAX endpoint used by `board.php` auto-refresh

### Current Implementation Style (IMPORTANT)
The current board implementation is **server-rendered HTML**:
- `board_data.php` returns JSON: `{ "clock": "...", "html": "<div>...</div>" }`
- JavaScript replaces the board DOM with the returned `html` and re-initializes tooltips.

**Do not refactor to a full JSON/card renderer unless explicitly requested.**  
Enhancements (like *late alert icon*) should be added in the current architecture first.

---

## 2) Technology & Constraints

- PHP 7.3+ (Dolibarr conventions)
- Vanilla JavaScript (ES5; keep compatibility; avoid ES6+)
- CSS3
- MySQL/MariaDB through Dolibarr `$db` abstraction

No npm, no Composer, no bundlers.

---

## 3) Repository Structure (actual)

```
planning/
  board.php
  board_data.php
  completed.php
  planning.php
  core/modules/modPlanning.class.php
  css/
    board.css
    planning.css
  js/
    board.js
```

---

## 4) Permissions / Access Control

This module uses `rights_class = 'planning'` and requires:

- `$user->rights->planning->read`

All pages and endpoints must enforce access checks:
- `accessforbidden()` in pages
- `403` JSON error in AJAX endpoint

---

## 5) Data Model Used by This Module

### Primary Dolibarr tables
- `llx_mrp_mo` (Manufacturing Orders)
- `llx_mrp_mo_extrafields` (custom extrafields used by this module)
  - `job_number`
  - `working_station`
- `llx_product` (product ref/label for MO product)
- `llx_stock_mouvement` (used to compute produced quantity)
- `llx_bom_bom` (used for BOM note tooltip)

### Working Station source (IMPORTANT)
**Working station is stored on MO extrafields**:
- `llx_mrp_mo_extrafields.working_station`

It may contain:
- a numeric ID (rowid) pointing to workstation tables, OR
- a free-text label/code

Workstation lookup (label/ref) currently comes from:
- `llx_workstation_workstation` (preferred)
Other files also probe fallback tables:
- `llx_workstation`
- `llx_mrp_workstation`

**Do NOT assume `workstation_id` / `workstation_label` are part of the MO card.**
They are a *lane/column context* resolved from extrafields + lookup table.

---

## 6) Board Rendering & Auto-Refresh

### `board.php`
- Renders the initial board HTML server-side.
- Includes `css/board.css` and `js/board.js`.
- Provides controls (station filter, show draft, refresh interval).
- Uses tooltip helpers to avoid broken tooltips after DOM replacement.

### `board_data.php`
- Returns JSON with `clock` and `html`.
- Builds board HTML grid (`.pl-board-grid`) with columns (`.pl-col`) and cards (`.pl-card`).

### `js/board.js`
- Handles fullscreen toggling and board theme switching:
  - `pl-board-dark` when fullscreen
  - `pl-board-light` otherwise
- Handles auto-refresh timer and AJAX fetch of `board_data.php`
- Re-initializes tooltips after DOM replacement

**When modifying the board UI, you must ensure the auto-refresh still works.**

---

## 7) Tooltip System (do not break)

This module intentionally avoids relying only on Dolibarr AJAX tooltips after refresh.

Current approach:
- Prefer TipTip (`.classfortooltip`, `.pl-job-tooltip`) where available
- Re-init Dolibarr tooltip JS after refresh as a fallback
- Extra CSRF-token injection helpers exist to keep `/core/ajax/ajaxtooltip.php` working after refresh

Rules:
- Do not remove tooltip re-init logic in `board.js`
- If adding new icons/links, ensure they have correct tooltip binding (TipTip or Dolibarr)

---

## 8) Theme Rules (Light vs Fullscreen Dark)

- Normal (non-fullscreen): must respect Dolibarr theme colors (light)
- Fullscreen: intentionally uses a **dark** board theme

Implementation:
- JS toggles `.pl-board-dark` / `.pl-board-light`
- CSS must be written so fullscreen does not leak into normal Dolibarr UI

Avoid:
- hard global overrides on `body` that affect other Dolibarr pages
- excessive `!important`

---

## 9) CSS Rules (STRICT)

- Avoid `!important` unless absolutely necessary
- Keep selectors scoped to the module:
  - prefer `.planning-page`, `.pl-board`, `.pl-card`, `.pl-col`, etc.
- Do not change Dolibarr global typography/colors

---

## 10) Late Alert Icon (Planned Feature)

Goal: show a **small late/overdue icon** on MO cards, similar to Dolibarr MO UI.

### Where to implement (current architecture)
Since cards are server-rendered:
- compute `is_late` in PHP while building each card in `board.php` and `board_data.php`
- render a small icon inside `.pl-card-top` (or near MO ref) when late

### Default late definition (match Dolibarr intent)
A card is late when:
- MO is not finished/cancelled (status not in produced/cancelled)
- `date_end_planned` exists
- `date_end_planned` < current server time

### UI requirements
- Icon must be subtle (small)
- Icon must have a tooltip showing planned end date and “Late”
- Reuse Dolibarr pictos where possible (do not add new icon libraries)

### Stability requirement
Late icon MUST survive:
- auto-refresh DOM replacement
- fullscreen mode
- tooltips after refresh

---

## 11) Performance Rules

- Do not introduce N+1 queries per card
- Any extra data needed for late icon must come from already available fields (`date_end_planned`, `status`)
- Keep board_data response lightweight; avoid expensive per-card object fetch if not required

---

## 12) Development Workflow

- Deploy to: `dolibarr/htdocs/custom/planning/`
- Enable module in Dolibarr admin
- Test in a dev instance (not production)

No build steps.

---

## 13) Language Note

You MAY write instructions and comments in **Polish or English**.

Recommended:
- High-level notes / TODO: English OK
- Code (variables, functions): English
- Keep consistency within a file

---

## 14) TG2.1 - Auto End Date (qty_per_hour + buffer)

### Założenia (Assumptions)

**Product Extrafields:**
- `qty_per_hour_form` — production rate for forming jobs (pieces/hour)
- `qty_per_hour_trim` — production rate for trimming jobs (pieces/hour)

**Job Data:**
- `qty` — quantity (pieces)
- `fk_product` — product reference
- `date_start` — job start timestamp
- `date_end` — job end timestamp (NULL = auto-calculate, non-NULL = manual override)
- `group_code` — 'forming' or 'trimming'

### Logic

**1. Pobranie Rate (loadProductRate)**
- For `group_code = 'forming'` → load `qty_per_hour_form`
- For `group_code = 'trimming'` → load `qty_per_hour_trim`
- If missing or rate <= 0 → fallback to `estimated_hours` (or 1.0h default)

**2. Liczenie Duration (calculateComputedEndDate)**
- If `date_end` is set → return `date_end` (manual override wins)
- If `date_start` missing → return null
- Calculate: `production_hours = qty / rate`
- Convert: `production_minutes = round(production_hours × 60)`
- Add buffer:
  - forming: +60 minutes
  - trimming: +30 minutes
- Result: `total_minutes = production_minutes + buffer_minutes`
- Computed end: `date_start + (total_minutes × 60 seconds)`

**3. Fallback (no rate available)**
- Use `job.estimated_hours` if available
- Else use 1.0 hour default
- Add buffer based on group_code
- Compute same as above

### Backend (class/PlanningTimelineService.class.php)

**New Methods:**

1. `loadProductRate($fk_product, $groupCode)` — loads qty_per_hour_form or qty_per_hour_trim
2. `getBufferForGroupCode($groupCode)` — returns 60 (forming) or 30 (trimming)
3. `calculateComputedEndDate($job)` — computes end timestamp or returns date_end if set

**Where used:**
- Line ~124: Calls `calculateComputedEndDate()` in job model building
- Line ~138: Adds `'computed_end'` to JSON output between `date_end` and `status`

### Frontend (js/timeline.js)

**Gantt Bar End Time (renderMachineRowGantt ~L341)**
Priority:
1. If `job.date_end` → use it (manual)
2. Else if `job.computed_end` → use it (auto, convert seconds→ms)
3. Else → fallback to `total_duration_minutes`

### Example

Product: Part-001
- qty_per_hour_form: 20 (pieces/hour for forming)
- Job: 100 pieces, forming, start 2024-01-15 08:00
- Calculation:
  - production_hours = 100 / 20 = 5 hours = 300 minutes
  - buffer = 60 minutes
  - total = 360 minutes (6 hours)
  - computed_end ≈ 2024-01-15 14:00 (+ planning boundary)

### Compatibility

✅ Pre-config (no qty_per_hour): Falls back to estimated_hours
✅ Post-config (qty_per_hour set): Uses production-based timing
✅ Manual override: date_end always wins if set
✅ No DB changes: computed_end calculated on-the-fly

### Files Changed

- `class/PlanningTimelineService.class.php`: +3 methods, job model building updated
- `js/timeline.js`: Already supports computed_end (no changes needed)
- No new DB fields required

---

## 15) Safe Change Policy

When implementing changes:
- Prefer **small, reversible commits**
- Avoid refactors that touch all pages at once
- Keep `board.php` and `board_data.php` output consistent (same card structure)

---

## 16) TG2.1 FINAL - Duration = qty / qty_per_hour (Production Rate Model)

### Core Logic

**Duration Formula (TG2.1 — THE PRIMARY DRIVER):**

```
priority:
  1) qty > 0 AND product exists AND qty_per_hour_* > 0
     → duration_hours = qty / qty_per_hour_*
  
  2) else (any of above missing/invalid)
     → duration_hours = estimated_hours (fallback)

date_end = date_start + (duration_hours × 3600 seconds)
```

Where:
- `qty` = quantity field on job (DECIMAL, default 1.0)
- `qty_per_hour_form` = product extrafield (pieces/hour for forming)
- `qty_per_hour_trim` = product extrafield (pieces/hour for trimming)
- `estimated_hours` = existing fallback (legacy)

### Files Changed

**Backend:**
1. **class/PlanningTimelineService.class.php**
   - New method `loadProductRate($fk_product, $groupCode)` — loads qty_per_hour_form or qty_per_hour_trim
   - Refactored `calculateComputedEndDate($job)` — implements TG2.1 priority logic
   - Job model: adds `computed_end` to JSON output
   - Lines ~124: Calculates computed_end in job model building
   - Lines ~138: Outputs computed_end in JSON

2. **timeline.php**
   - Updated product loading to include `extrafields` column (line 66)
   - Parses JSON extrafields and extracts `qty_per_hour_form` / `qty_per_hour_trim` (lines 75-85)
   - Passes rates to frontend in `config.products` JSON

3. **js/timeline.js**
   - Refactored `updateEndTime()` (lines 934-972) — implements TG2.1 formula
   - Added onChange bindings in Add Job modal (lines 1140-1153)
   - Added onChange handlers in Edit Job form (lines 706-770)
   - Gantt rendering (lines 345-360): already supports computed_end priority

### UI Behavior (Auto-Recalc)

**When user changes in Add Job or Edit Job modal:**
- Quantity input
- Product (FG) selection
- Group (Forming/Trimming)
- Start date
- Estimated hours (fallback)

**IMMEDIATELY:**
- Recalculate end date = start + calculated duration
- Preview shown in modal input field

**Priority:**
1. If qty > 0 + product selected + rate available → use qty/rate
2. Else → use estimated_hours
3. Display updated end date in real-time

### Backend Calculation

**calculateComputedEndDate($job):**
```
// Manual override takes priority
if (date_end is set) 
  return date_end;

// Primary path: qty + product rate
if (qty > 0)
  rate = loadProductRate(product, groupCode);
  if (rate > 0)
    duration = qty / rate;
    return date_start + duration (seconds);

// Fallback: estimated_hours
duration = estimated_hours or 1.0;
return date_start + duration (seconds);
```

### Gantt Rendering Priority

Gantt bar end time (renderMachineRowGantt, lines 345-360):
```
if (job.date_end exists)
  use date_end (manual override);
else if (job.computed_end exists)
  use computed_end (auto from qty/rate);
else
  fallback to duration_hours;
```

### Example

**Product Setup:**
- Product: Part-X
- qty_per_hour_form: 20 pcs/h
- qty_per_hour_trim: 15 pcs/h

**Add Job Flow:**
1. Select workstation, product (Part-X), group (forming)
2. Enter start date: 2024-01-15 10:00
3. Enter quantity: 100
4. [onChange fires → updateEndTime()]
5. **Calculated:**
   - rate = 20 pcs/h (from qty_per_hour_form)
   - duration = 100 / 20 = 5 hours
   - end date = 10:00 + 5h = 15:00
6. User sees end date filled in automatically

**Edit Job (change quantity):**
- User changes qty from 100 to 200
- [onChange fires immediately]
- **Recalculated:**
   - duration = 200 / 20 = 10 hours
   - end date preview updated to next day 00:00

### Compatibility

✅ **Backward compatible:**
- Old jobs without product/qty → use estimated_hours
- Old jobs with manual date_end → date_end takes priority
- Partial product rate (only qty_per_hour_form, no trim) → works for that group only

✅ **Database:**
- NO new columns needed
- NO migrations required
- Rates loaded from product extrafields (already exist in Dolibarr)

### Constraints Respected

✅ ETAP 2.4 FROZEN (layout unchanged)
✅ No DB migrations
✅ Minimal diff (only new methods + UI bindings)
✅ Works in both capacity and gantt modes
✅ No breaking changes to existing views/endpoints

### Known Limitations

- ⚠️ No auto-schedule (jobs stay in place)
- ⚠️ No overlap detection
- ⚠️ No workstation-specific rates (per-product only)
- ⚠️ Manual date_end always wins (computed_end for display only)
- ⚠️ No rate admin UI (edit product extrafields directly)

Where:
- **qty**: quantity field (DECIMAL, default 1.0)
- **cycle_time_minutes_per_part**: from product extrafields (cycle_time_form or cycle_time_trim)
- **buffer_minutes**: from workstation type (Laser 15, CNC 30, other 60)

### Usage

1. Configure product extrafields (cycle_time_form and cycle_time_trim in minutes)
2. Create job with date_start and qty
3. Gantt bar auto-sizes: date_start + (qty × cycle_time + buffer)
4. Or set date_end manually to override auto-calculation

### Example

Product: Part-001
- cycle_time_form: 2.5 minutes per part
- Workstation: Laser (15 min buffer)
- Job: 100 parts on Laser
- Expected computed_end: date_start + (100 × 2.5 + 15) × 60 = date_start + 15900 seconds

### Compatibility

✅ Pre-migration (no cycle_time configured):
- Gantt uses total_duration_minutes fallback
- computed_end is null

✅ Post-migration (cycle_time configured):
- Gantt uses computed_end (more accurate)
- Production-based timing

### Known Limitations

- ⚠️ No auto-scheduling (jobs don't move)
- ⚠️ No overlap detection
- ⚠️ Manual date_end takes priority (computed_end not saved to DB)
- ⚠️ Cycle time per-product only (not per-workstation override yet)

### Future Work

- TG2.1 v3: Auto-schedule with first-available-slot algorithm
- TG2.2: Full scheduling engine with conflict resolution
- Admin UI for buffer & cycle time configuration

