# Job Card Module — Task Plan
> Module: `jobcard` | Created: 2026-03-25 | Last updated: 2026-03-25
> **Standalone module — no dependencies on other custom modules**
> GitHub: https://github.com/ciachoo/jobcard (master)

---

## Purpose

Every Manufacturing Order (MO) must have a printed Job Card (Job Traveler) before it reaches
the shop floor. This module provides:

1. A **PDF Job Card** (`print_pdf.php`) for any MO — A4 landscape, TCPDF
2. A **print queue page** (`queue.php`) — lists MOs with print count, preview dialog, download button
3. A **print status tracker** — records who printed, when, how many copies (`llx_jobcard_printlog`)
4. A **print button** injected into the standard Dolibarr MO view via hook

---

## Module Directory Structure

```
custom/jobcard/
├── jobcard_task.md             ← this file
├── core/
│   ├── modules/
│   │   └── modJobCard.class.php        ✔ done
│   └── actions/
│       └── actions_jobcard.class.php   ✔ done (hook: Print button on MO card)
├── sql/
│   ├── llx_jobcard_printlog.sql        ✔ done (CREATE TABLE)
│   └── uninstall.sql                   ✔ done (DROP TABLE)
├── admin/
│   └── setup.php                       ✔ done
├── css/
│   └── jobcard.css                     ✔ done
├── js/
│   ├── jobcard.js                      ✔ done
│   └── qrcode.min.js                   ✔ done
├── ajax/
│   └── mark_printed.php                ✔ done
├── print_pdf.php               ✔ done (A4 landscape TCPDF, action=frame/popup/download/inline)
└── queue.php                   ✔ done
```

---

## Database

### `llx_jobcard_printlog`

```sql
CREATE TABLE llx_jobcard_printlog (
    rowid       INT AUTO_INCREMENT PRIMARY KEY,
    fk_mo       INT NOT NULL,
    fk_user     INT NOT NULL,
    date_print  DATETIME NOT NULL,
    copies      TINYINT DEFAULT 1,
    format      VARCHAR(10) DEFAULT 'A4',
    note        VARCHAR(255),
    entity      INT NOT NULL DEFAULT 1
) ENGINE=InnoDB;
```

---

## print_pdf.php — PDF generator

**URL modes:**
| URL | Behaviour |
|---|---|
| `print_pdf.php?mo=X` | Inline PDF (for iframe preview) |
| `print_pdf.php?mo=X&action=download` | Force-download, logs to printlog immediately |
| `print_pdf.php?mo=X&action=frame` | HTML wrapper with `<embed>` + `beforeprint` postMessage |
| `print_pdf.php?mo=X&action=popup` | HTML popup wrapper (not currently used) |

**Layout:** A4 landscape, TCPDF, navy header (#00003c), company logo, QR code, BOM materials in Special Instructions section.

**BOM query:** `llx_mrp_production` where `role='toconsume'` and `fk_product_type=0` (storable), batch from consumed sub-lines via COALESCE.

**Logging:** INSERT into `llx_jobcard_printlog` on `action=download` only. Preview mode: no log (logged separately via AJAX by JS).

---

## queue.php — Print Queue

Lists MOs (Validated / In Progress / Produced) with print count, last print date, and action buttons.

**Action buttons per row:**
- **Print** (`.jc-preview-btn`) — opens jQuery UI dialog with PDF iframe; dialog Print button logs + prints + reloads
- **Download** (direct link `action=download`) — logs + forces file save

**JS in queue.php (`js/jobcard.js` → `initQueuePreview()`):**
1. Click Print → opens `#dialogforpopup` jQuery UI dialog with iframe (raw PDF)
2. Dialog **Print** button → POST `mark_printed.php` → on AJAX success: `iframe.contentWindow.print()` → on `afterprint`: close dialog → reload page
3. Dialog **Close / ✕** → close dialog → reload page (count refreshes)

---

## ajax/mark_printed.php

- Defines: `NOREQUIREMENU`, `NOREQUIREHTML`, `NOREQUIRETRAN`, `NOCSRFCHECK`, `NOTOKENRENEWAL`
- Requires `../../../main.inc.php` (3 levels up from `ajax/`)
- Checks: `isModEnabled('jobcard')`, `$user->rights->jobcard->print || $user->admin`
- POST params: `mo_id` (int), `copies` (int)
- Returns JSON: `{status:'ok', rowid:N}` or `{status:'error', error:'...'}`

---

## Left Menu

Under MRP, entry "Print Job Card" with printer icon:
```php
'prefix' => img_picto('', 'printer', 'class="pictofixedwidth paddingright"')
```
Requires module disable/re-enable to update cached `llx_menu`.

---

## PHASE Checklist

### PHASE 1 — Core ✔ COMPLETE
- [x] Module descriptor, SQL table, install/uninstall
- [x] `print_pdf.php` — A4 landscape PDF (TCPDF), logo, QR, BOM materials
- [x] `queue.php` — MO list, print count, filter, Print + Download buttons
- [x] `ajax/mark_printed.php` — log endpoint (all auth bugs fixed)
- [x] `js/jobcard.js` — jQuery dialog preview, Print logs + closes + reloads, Close reloads
- [x] Left menu icon (`prefix` + `img_picto`)
- [x] Hook: Print button injected into MO card view (`actions_jobcard.class.php`)
- [x] Download button — logs immediately on file save

### PHASE 2 — Future
- [ ] Print status badge on MO list view (✅ printed / ⚠️ not printed)
- [ ] Queue widget on MRP/planning dashboard
- [ ] Auto-open print on MO validation (configurable)
- [ ] Block MO from "In Progress" until job card printed (configurable flag)
- [ ] Copies selector in queue dialog

---

## Known Issues Fixed

| Issue | Root Cause | Fix |
|---|---|---|
| SQL table not created on enable | `sqlcreate` ignored when `init()` overridden | Added `init()`/`remove()` with `_load_tables()` |
| DROP ran immediately after CREATE | `_load_tables` runs ALL `llx_*.sql` alphabetically | Renamed drop file to `uninstall.sql` |
| Empty MO list default | `GETPOST('int')` returns `0` not `''` | Changed to `GETPOST('alpha')`, default `m.status >= 0` |
| Operations empty (SQL error) | `rank` is a MySQL 8 reserved word | Renamed alias to `op_rank` |
| `mark_printed.php` 500 | Bootstrap path `../../main.inc.php` wrong (2 levels, not 3) | Changed to `../../../main.inc.php` |
| `mark_printed.php` 403 | `define('NOLOGIN', 0)` — `defined()` only checks existence, not value → skipped `loadRights()` | Removed `NOLOGIN`, added `NOCSRFCHECK` + `NOTOKENRENEWAL` |
| Print dialog closes before user prints | `window.location.reload()` ran immediately after AJAX | Moved reload to `afterprint` event on iframe |
| Count not updating on Close / ✕ | No reload on dialog close | Added `close` callback → `window.location.reload()` |
