# DESIGN: Board Workstation Groups

## Status
Partially implemented — extrafield `pl_group` approach chosen, managed groups list deferred.

---

## Inventory: All Places Using WS Group Logic

### 1. `planning/board.php` — lines 20–28 + 41 + 491
**Method**: `pl_section_from_ws()` — takes first word of label as section slug.
```php
$parts     = preg_split('/\s+/', trim($wsRef), 2);
$firstWord = !empty($parts[0]) ? $parts[0] : 'Other';
$slug      = strtolower(preg_replace('/[^a-zA-Z0-9]+/', '', $firstWord));
```
**Breaks when**: label is blank / equals ref (e.g. `"FM7"` → section = `"Fm7"`).
**Fix needed**: replace with `$workstationGroupMap[$wsId]` lookup from extrafield.

---

### 2. `planning/ajax/mo_gantt_data.php` — lines 70–74 + 358–373
**Method**: hardcoded prefix matching on ref.
```php
function moGanttClassifyWs($r) {
    if (strncmp($r, 'FM', 2) === 0)    return 'forming';
    if (strncmp($r, 'LASER', 5) === 0) return 'trimming';
    if (strncmp($r, 'CNC', 3) === 0)   return 'trimming';
    return 'other';
}
```
Hardcoded sort order: `['forming' => 0, 'trimming' => 1, 'other' => 2]`.
**Breaks when**: new WS ref doesn't start with FM/LASER/CNC.
**Fix needed**: read group from extrafield, sort by `sort_order` from managed groups table.

---

### 3. `planning/class/PlanningTimelineService.class.php` — lines 88–92 + 260–265
**Method**: group whitelist (`'forming'` or `'trimming'` only) + `LIKE '%form%'` / `LIKE '%trim%'` SQL filter on label/ref.
```php
if (!in_array($groupCode, array('forming', 'trimming'))) {
    return array('error' => 'Invalid group_code. Must be forming or trimming.');
}
$keyword = ($groupCode === 'trimming') ? 'trim' : 'form';
WHERE (LOWER(label) LIKE '%$keyword%' OR LOWER(ref) LIKE '%$keyword%')
```
**Breaks when**: adding a third group, or renaming WS labels.
**Fix needed**: remove whitelist, query `llx_planning_ws_group` for valid groups, filter by extrafield value instead of LIKE.

---

### 4. `productionrules/class/productionrules.class.php` — lines 960–983
**Method**: splits WS label on spaces and tries longest-first match against a service map.
```php
$wsWords = preg_split('/\s+/', trim($obj->label));
for ($i = count($wsWords); $i >= 1; $i--) {
    $candidate = strtolower(implode(' ', array_slice($wsWords, 0, $i)));
    // match against service code map
}
```
Maps first word(s) of label to a BOM service line operation code (e.g. `"forming"` → forming service).
**Breaks when**: labels change or don't contain the expected keyword.
**Fix needed**: read group from extrafield, map `pl_group` value directly to service code in config.

---

## Summary Table

| File | Method | Breaks When | Priority |
|---|---|---|---|
| `board.php` | `preg_split` first word of label | label ≠ "Group Ref" format | **High** |
| `ajax/mo_gantt_data.php` | hardcoded ref prefix (`FM`, `LASER`, `CNC`) | new WS added | **High** |
| `PlanningTimelineService.class.php` | `LIKE '%form%'` + whitelist | new group added | **High** |
| `productionrules` | `preg_split` label → service map | label changes | **Medium** |

---## Problem

`board.php` derives section headers (Forming / Trimming / …) from the first word of
the workstation **label** (`pl_section_from_ws()`).

```
"Forming FM7"  → section = "Forming",  column = "Forming FM7"
"FM7"          → section = "Fm7"       ← BROKEN (no label set)
```

When workstation labels are blank or equal to ref (`FM7`, `LASER1`), sections collapse
to per-WS headings instead of process groups.

The Dolibarr `llx_workstation_workstation` table has no group/category column.

---

## Chosen Fix: Extrafield `pl_group`

**Setup → Other → Extrafields → Workstations**
(URL: `/admin/extrafields.php?attrname=workstation_workstation`)

| Field | Value |
|---|---|
| Attribute code | `pl_group` |
| Label | `Board Group` |
| Type | `varchar` |
| Size | `64` |
| Enabled | Yes |
| Visible | 1 |

Each workstation gets `Board Group` = `Forming` / `Trimming` / etc. set manually.

### Code change (`board.php`)

1. Extend the workstation query to JOIN `llx_workstation_workstation_extrafields`:

```php
$sqlws = "SELECT w.rowid, w.ref, w.label, ef.pl_group"
       . " FROM ".$wst." w"
       . " LEFT JOIN ".MAIN_DB_PREFIX."workstation_workstation_extrafields ef"
       .         " ON ef.fk_object = w.rowid";
```

2. Store group in a new map:

```php
$workstationGroupMap[$id] = trim((string)($ws->pl_group ?? ''));
```

3. Update `pl_section_from_ws()` or replace call site with group-map lookup:

```php
// In pl_render_grid_columns() and section-building loop:
$section = !empty($workstationGroupMap[$wsId])
    ? $workstationGroupMap[$wsId]
    : pl_section_from_ws($wsRef)['label']; // fallback to label-word
```

4. Column header shows **ref only** (not label-ref combo):

```php
$workstationMap[$id] = ($ref !== '') ? $ref : $label;
```

---

## Deferred: Managed Groups List (v2)

Instead of a free-text varchar extrafield, implement a proper group registry:

### Architecture

```
llx_planning_ws_group
  rowid     INT PK
  name      VARCHAR(64)   — "Forming", "Trimming", …
  sort_order INT DEFAULT 0
  entity    INT DEFAULT 1
```

- Managed in **Planning → Admin → Setup** (new section "Board Groups")
- CRUD: add / rename / reorder / delete groups
- `pl_group` extrafield type changed from `varchar` to **`select`** with options
  populated dynamically from `llx_planning_ws_group`
- Section order on the board follows `sort_order` instead of alphabetical

### UI (admin section)

```
Board Groups
  [+ Add group]
  ☰ Forming       [rename] [↑ ↓] [delete]
  ☰ Trimming      [rename] [↑ ↓] [delete]
  ☰ Assembly      [rename] [↑ ↓] [delete]
```

Draggable rows (jQuery UI sortable) → auto-saves `sort_order` via AJAX.

### Implementation files

| File | Change |
|---|---|
| `sql/llx_planning_ws_group.sql` | New table |
| `sql/migrate_add_ws_group.sql` | ALTER extrafield type → select (or new column) |
| `admin/planning_setup.php` | New "Board Groups" CRUD section |
| `ajax/ws_groups.php` | AJAX endpoint: list / create / rename / reorder / delete |
| `board.php` | Read `sort_order` for section ordering |
| `js/board.js` | (none — section order driven server-side) |

### Extrafield approach for dropdown

Dolibarr extrafields of type `select` store choices as a serialised string.
Dynamically syncing them from `llx_planning_ws_group` requires either:
- (a) Re-saving the extrafield definition on every group CRUD (fragile), or
- (b) Storing group as a FK (`INT`) column in
  `llx_workstation_workstation_extrafields` and rendering the dropdown in our own
  admin/edit page rather than relying on Dolibarr's extrafield UI.

Option (b) is cleaner — we own the column, the UI, and the validation.

---

## Open Questions

- Should `sort_order` also control column order **within** a section, or only section order?
- Should ungrouped workstations fall into an "Other" section or be hidden?
- Do we need per-entity groups or global?
