Files
GSB-Construction/docs/PLAN-material-validation-flow.md

15 KiB

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_engineerapprove_mr only

Update project_user.role — Migrate existing values (pmproject_manager, engineerproject_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):

// 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:

  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