386 lines
15 KiB
Markdown
386 lines
15 KiB
Markdown
# 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
|