15 KiB
Material Validation Flow, Task Import & PO-Based Assignment
Goal
Implement three interconnected features:
- Task Import — Excel-based bulk task importation into projects
- MR → PO → Approval Pipeline — Material Requisition and Purchase Order with role-based approval and downloadable PDFs
- PO-Based Material Assignment — Tasks select materials via approved PO document lookup (not dropdown)
- 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 |
| 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_mronly
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_idFK (nullable) totask_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 ulidsubmitForApproval()— Find project personnel withapprove_mrpermission → create 1-step chaindownloadPdf()— 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 Inertiastore(Request)— Create PO; optionally link to approved MR(s) and copy itemssubmitForApproval()— Find project personnel withapprove_popermission → create chaindownloadPdf()— Blade template with approver name + role, linked MR referencessearchApproved(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 templateimport(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_idontask_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_numberon task material rows
→ Verify: Open project, import tasks, search approved PO, add materials to task
Routes Summary
MaterialLogistics (new):
// 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):
Route::get('projects/{project}/tasks/template', ...)->name('projects.tasks.template');
Route::post('projects/{project}/tasks/import', ...)->name('projects.tasks.import');
Verification Plan
Automated (Terminal)
# 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:
- Go to Projects → select a project → Tasks tab
- Click "Import Tasks" → download template
- Fill template with 3 task rows → upload
- Verify 3 tasks appear in the task list
Flow 2 — MR → Approval → PDF:
- Go to Material Requisitions page
- Create new MR: select project, add 2 materials with quantities
- Submit for Approval → status changes to "Submitted"
- Log in as Project Engineer or Project Manager → Approve
- MR status → "Approved" → Download PDF
- PDF shows items table + approver name/role at bottom
Flow 3 — PO → Approval → PDF:
- Go to Purchase Orders page
- Create PO: select approved MR → items auto-populate, enter supplier
- Submit for Approval → must be Project Manager
- Approve → Download PDF → shows MR reference, supplier, items, approver
Flow 4 — PO-Based Material Assignment:
- Go to a Project → Tasks tab → expand a task → "Add Materials"
- Type PO doc number in lookup → approved POs appear
- Select PO → see its items → check materials → submit
- Verify materials show PO reference on task row
Flow 5 — Form UX:
- Check all inputs have max-width styling
- Check selects show names (e.g., "Portland Cement Bag" not "42")
- 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