# Material Validation Flow, Task Import & PO-Based Assignment ## Goal Implement three interconnected features: 1. **Task Import** — Excel-based bulk task importation into projects 2. **MR → PO → Approval Pipeline** — Material Requisition and Purchase Order with role-based approval and downloadable PDFs 3. **PO-Based Material Assignment** — Tasks select materials via approved PO document lookup (not dropdown) 4. **Role Hierarchy** — 16 Spatie roles with approval permissions **Project Type:** WEB (Laravel 11 + Inertia React + TypeScript) --- ## Confirmed Decisions | Decision | Answer | |----------|--------| | MR → PO relationship | **1:many** — One MR can produce multiple POs (split to suppliers) | | Supplier field | **Free text** (no suppliers table) | | Task import assignee | **Assign after import** (no email column in template) | | Approval model | **1-step** — Whoever has permission approves (not sequential) | | Approval visibility | **All approvers' names + roles** shown on approved documents + PDFs | | Role system | **Spatie roles** seeded (16 roles), permissions: `approve_mr`, `approve_po` | --- ## Success Criteria - [ ] 16 Spatie roles seeded with correct approval permissions - [ ] Users can import tasks from Excel template (no assignee in template) - [ ] Users can create MR with items, submit for approval, download PDF - [ ] Users can create PO from approved MR(s), submit for approval, download PDF - [ ] MR PDF shows approver name + role when approved - [ ] PO PDF shows approver name + role when approved - [ ] Only approved PO materials are assignable to project tasks via lookup - [ ] All form inputs show object **names** (not IDs) and use max-width styling --- ## Tech Stack | Component | Technology | Rationale | |-----------|-----------|-----------| | Backend | Laravel 11 (nWidart Modules) | Existing | | Frontend | React 18 + Inertia.js + TypeScript | Existing | | Excel | Maatwebsite/Excel | Already installed | | PDF | barryvdh/laravel-dompdf | Lightweight, industry standard | | Approval | ApprovalWorkflow module + `HasApprovable` | Already built, polymorphic | | Roles | Spatie Permission (`HasRoles` on User) | Already installed, not yet seeded | --- ## File Structure (New/Modified) ``` Modules/MaterialLogistics/ ├── app/Http/Controllers/ │ ├── MaterialRequisitionController.php [NEW] │ └── PurchaseOrderController.php [NEW] ├── app/Models/ │ ├── MaterialRequisition.php [NEW] │ ├── MaterialRequisitionItem.php [NEW] │ ├── PurchaseOrder.php [NEW] │ └── PurchaseOrderItem.php [NEW] ├── app/Services/ │ └── DocumentNumberService.php [NEW] ├── database/migrations/ │ └── 2026_03_28_000001_create_mr_po_tables.php [NEW] ├── resources/ │ ├── js/Pages/ │ │ ├── Requisitions/Index.tsx [NEW] │ │ └── PurchaseOrders/Index.tsx [NEW] │ └── views/pdf/ │ ├── material-requisition.blade.php [NEW] │ └── purchase-order.blade.php [NEW] ├── routes/web.php [MODIFY] Modules/ProjectManagement/ ├── app/Http/Controllers/ │ ├── TaskImportController.php [NEW] │ └── TaskMaterialController.php [MODIFY] ├── app/Exports/TaskTemplateExport.php [NEW] ├── app/Imports/TaskImport.php [NEW] ├── database/migrations/ │ └── 2026_03_28_000002_add_po_to_task_materials.php [NEW] ├── resources/js/ │ ├── Pages/Projects/Show.tsx [MODIFY] │ └── Components/PurchaseOrderLookup.tsx [NEW] ├── routes/web.php [MODIFY] database/seeders/ └── RolePermissionSeeder.php [NEW] ``` --- ## Task Breakdown ### Task 1: Install dompdf + Seed Roles & Permissions **Install:** - `composer require barryvdh/laravel-dompdf` **Seeder:** `database/seeders/RolePermissionSeeder.php` - Create 16 Spatie roles (see table below) - Create 2 permissions: `approve_mr`, `approve_po` - Assign: `project_manager` → both permissions; `project_engineer` → `approve_mr` only **Update `project_user.role`** — Migrate existing values (`pm` → `project_manager`, `engineer` → `project_engineer`, etc.) | # | Role | Slug | approve_mr | approve_po | |---|------|------|:---:|:---:| | 1 | Project Manager | `project_manager` | ✅ | ✅ | | 2 | Project Engineer | `project_engineer` | ✅ | ❌ | | 3 | Electrical Engineer | `electrical_engineer` | ❌ | ❌ | | 4 | Mechanical Engineer | `mechanical_engineer` | ❌ | ❌ | | 5 | AutoCAD Operator | `autocad_operator` | ❌ | ❌ | | 6 | Safety Officer | `safety_officer` | ❌ | ❌ | | 7 | First Aider | `first_aider` | ❌ | ❌ | | 8 | General Foreman | `general_foreman` | ❌ | ❌ | | 9 | Foreman | `foreman` | ❌ | ❌ | | 10 | Timekeeper | `timekeeper` | ❌ | ❌ | | 11 | Leadman | `leadman` | ❌ | ❌ | | 12 | Heavy Equipment Operator | `heavy_equipment_operator` | ❌ | ❌ | | 13 | Dump Truck Operator | `dump_truck_operator` | ❌ | ❌ | | 14 | Batch Concrete Mixer Operator | `batch_concrete_mixer_operator` | ❌ | ❌ | | 15 | Skilled Worker | `skilled_worker` | ❌ | ❌ | | 16 | Helper | `helper` | ❌ | ❌ | → **Verify:** `php artisan db:seed --class=RolePermissionSeeder`, then in tinker: `User::first()->assignRole('project_manager')` → `$user->can('approve_po')` returns true --- ### Task 2: Migration — MR, PO tables + task_materials FK **Migration `2026_03_28_000001_create_mr_po_tables.php`:** ``` material_requisitions: id, ulid, project_id (FK), document_number (unique, MR-YYYY-NNNN), status (draft|submitted|approved|rejected), requested_by (FK users), approved_by (FK users nullable), approved_at (timestamp nullable), notes, timestamps material_requisition_items: id, requisition_id (FK cascade), material_id (FK cascade), quantity (decimal 12,2), unit_cost (decimal 12,2), timestamps purchase_orders: id, ulid, project_id (FK), document_number (unique, PO-YYYY-NNNN), supplier (string), status (draft|submitted|approved|rejected), requested_by (FK users), approved_by (FK users nullable), approved_at (timestamp nullable), notes, timestamps purchase_order_items: id, purchase_order_id (FK cascade), material_id (FK cascade), requisition_item_id (FK nullable — traces back to MR item), quantity (decimal 12,2), unit_cost (decimal 12,2), timestamps purchase_order_requisitions (pivot — 1:many MR→PO): id, purchase_order_id (FK cascade), requisition_id (FK cascade), timestamps ``` **Migration `2026_03_28_000002_add_po_to_task_materials.php`:** - Add `purchase_order_id` FK (nullable) to `task_materials` → **Verify:** `php artisan migrate` succeeds --- ### Task 3: Backend Models (6 new) + DocumentNumberService | Model | Key Features | |-------|-------------| | `MaterialRequisition` | `HasPublicIdentifier`, `HasApprovable`, belongsTo Project/RequestedBy/ApprovedBy, hasMany Items, belongsToMany PurchaseOrders | | `MaterialRequisitionItem` | belongsTo Requisition + Material | | `PurchaseOrder` | `HasPublicIdentifier`, `HasApprovable`, belongsTo Project/RequestedBy/ApprovedBy, hasMany Items, belongsToMany Requisitions | | `PurchaseOrderItem` | belongsTo PurchaseOrder + Material + RequisitionItem (nullable) | **`DocumentNumberService`:** Generates `MR-YYYY-0001`, `PO-YYYY-0001` with auto-increment per year. **Update `TaskMaterial`:** Add `purchaseOrder()` belongsTo relation. → **Verify:** Create model instances in tinker, check all relations --- ### Task 4: MR Controller (CRUD + Approval + PDF) `MaterialRequisitionController`: - `index()` — List MRs with items, status, requester name (Inertia page) - `store(Request)` — Create MR with items; validate material_id as ulid - `submitForApproval()` — Find project personnel with `approve_mr` permission → create 1-step chain - `downloadPdf()` — Blade template → dompdf; shows approver name + role if approved **Approval callback:** Listen for `ApprovalCompleted` event → update `approved_by` and `approved_at` on the MR. → **Verify:** Create MR, submit, approve, check `approved_by` populated, download PDF --- ### Task 5: PO Controller (CRUD + Approval + PDF + Search API) `PurchaseOrderController`: - `index()` — List POs with Inertia - `store(Request)` — Create PO; optionally link to approved MR(s) and copy items - `submitForApproval()` — Find project personnel with `approve_po` permission → create chain - `downloadPdf()` — Blade template with approver name + role, linked MR references - `searchApproved(Request)` — **JSON API**: Search approved POs by document_number for lookup component → **Verify:** Create PO from MR, approve, search returns it, PDF shows approver details --- ### Task 6: PDF Blade Templates (MR & PO) **`material-requisition.blade.php`:** - Header: Document number, date, project name, location - Items table: Material Name, Unit, Qty, Unit Cost, Line Total - Grand total row - Footer: Requested By (name, role), Approved By (name, role, date) — shown only when approved **`purchase-order.blade.php`:** - Header: Document number, date, project name, supplier - Referenced MR number(s) - Items table: Material Name, Unit, Qty, Unit Cost, Line Total - Grand total - Footer: Requested By, Approved By (name, role, date) → **Verify:** Download PDF, check layout, approver details visible --- ### Task 7: Task Import Backend **`TaskTemplateExport`** — Excel template with columns: Name, Description, Start Date, End Date, Estimated Hours, Labor Cost, Sort Order **`TaskImport`** — Parse Excel, validate, create tasks (no assignee column) **`TaskImportController`:** - `template()` → Download .xlsx template - `import(Request, Project)` → Upload, validate, bulk create tasks → **Verify:** Download template, fill 5 rows, upload, tasks appear in project --- ### Task 8: Modify TaskMaterialController for PO Enforcement Update `store()` and `storeBulk()`: - Require `purchase_order_id` (ulid) - Validate PO status === `approved` - Validate material exists in PO items - Save `purchase_order_id` on `task_materials` → **Verify:** Add material without approved PO → fails. With approved PO → succeeds. --- ### Task 9: Frontend — MR & PO Management Pages **`Requisitions/Index.tsx`:** - Create MR dialog with material picker + quantity inputs (max-width inputs) - Table: doc number, project, status badge, requester, items count, total, actions - Submit for Approval button, Download PDF button - All selects show material **name** not ID **`PurchaseOrders/Index.tsx`:** - Create PO dialog: select approved MR(s) → auto-populate items; or add manually - Supplier text input, items table - Submit + PDF buttons - Status badge + approved by name/role shown inline → **Verify:** Navigate, create MR, approve, create PO, approve, download PDFs --- ### Task 10: Frontend — PO Lookup + Show.tsx + Task Import UI **`PurchaseOrderLookup.tsx`:** - Debounced search input (300ms) - Calls `GET /purchase-orders/search?q=...&project_ulid=...` - Results: `PO-2026-0001 — Supplier (5 items)` - On select → shows PO items as checkbox list with quantities - Input displays **document_number**, not hash **`Show.tsx` changes:** - Tasks tab: "Import Tasks" button → file upload dialog (template download link + upload) - Material assignment: Replace MaterialPickerModal with PO Lookup flow - Show `purchase_order.document_number` on task material rows → **Verify:** Open project, import tasks, search approved PO, add materials to task --- ## Routes Summary ### MaterialLogistics (new): ```php // Material Requisitions Route::get('requisitions', ...)->name('requisitions.index'); Route::post('requisitions', ...)->name('requisitions.store'); Route::get('requisitions/{requisition}', ...)->name('requisitions.show'); Route::patch('requisitions/{requisition}/submit', ...)->name('requisitions.submit'); Route::get('requisitions/{requisition}/pdf', ...)->name('requisitions.pdf'); // Purchase Orders Route::get('purchase-orders', ...)->name('purchase-orders.index'); Route::post('purchase-orders', ...)->name('purchase-orders.store'); Route::get('purchase-orders/{purchaseOrder}', ...)->name('purchase-orders.show'); Route::patch('purchase-orders/{purchaseOrder}/submit', ...)->name('purchase-orders.submit'); Route::get('purchase-orders/{purchaseOrder}/pdf', ...)->name('purchase-orders.pdf'); Route::get('purchase-orders/search', ...)->name('purchase-orders.search'); ``` ### ProjectManagement (new): ```php Route::get('projects/{project}/tasks/template', ...)->name('projects.tasks.template'); Route::post('projects/{project}/tasks/import', ...)->name('projects.tasks.import'); ``` --- ## Verification Plan ### Automated (Terminal) ```bash # 1. Run seeder php artisan db:seed --class=RolePermissionSeeder # 2. Migrate php artisan migrate # 3. Verify roles in tinker php artisan tinker # > Spatie\Permission\Models\Role::pluck('name') → 16 roles # > Spatie\Permission\Models\Permission::pluck('name') → ['approve_mr', 'approve_po'] ``` ### Manual Testing (Browser — requires `php artisan serve` + `npm run dev`) **Flow 1 — Task Import:** 1. Go to Projects → select a project → Tasks tab 2. Click "Import Tasks" → download template 3. Fill template with 3 task rows → upload 4. Verify 3 tasks appear in the task list **Flow 2 — MR → Approval → PDF:** 1. Go to Material Requisitions page 2. Create new MR: select project, add 2 materials with quantities 3. Submit for Approval → status changes to "Submitted" 4. Log in as Project Engineer or Project Manager → Approve 5. MR status → "Approved" → Download PDF 6. PDF shows items table + approver name/role at bottom **Flow 3 — PO → Approval → PDF:** 1. Go to Purchase Orders page 2. Create PO: select approved MR → items auto-populate, enter supplier 3. Submit for Approval → must be Project Manager 4. Approve → Download PDF → shows MR reference, supplier, items, approver **Flow 4 — PO-Based Material Assignment:** 1. Go to a Project → Tasks tab → expand a task → "Add Materials" 2. Type PO doc number in lookup → approved POs appear 3. Select PO → see its items → check materials → submit 4. Verify materials show PO reference on task row **Flow 5 — Form UX:** 1. Check all inputs have max-width styling 2. Check selects show names (e.g., "Portland Cement Bag" not "42") 3. Check lookup shows "PO-2026-0001" not a UUID --- ## Done When - [ ] 16 Spatie roles seeded with approval permissions - [ ] Tasks importable from Excel into project - [ ] MR CRUD + approval + PDF with approver name/role - [ ] PO CRUD + approval + PDF with approver name/role - [ ] 1:many MR → PO relationship works - [ ] PO lookup component works on task material assignment - [ ] All forms show max-width inputs and object names