You have been invited to submit a price quotation for a purchase request. Please click the button below to view the required items and submit your quote.
Thank you, {{ $invitation->supplier->name }}. Your quote for {{ $invitation->purchaseRequest->request_number }} has been received. You will be contacted if you are selected.
{{-- Award buttons --}}
@if(!$request->awardedQuote)
Award to:
@foreach($quotes as $quote)
@endforeach
@else
✓ Quote awarded to {{ $request->awardedQuote->supplier->name }}
@endif
@endif
Award Quote
@endsection
```
- [ ] **Step 4: Commit**
```bash
git add app/Http/Controllers/Purchase/SupplierQuoteController.php resources/views/purchase/quotes/
git commit -m "feat: supplier quote controller — index, compare, award views"
```
---
## Task 10: Pipeline index view with vertical timeline
**Files:**
- Create: `app/Http/Controllers/Purchase/PurchasePipelineController.php`
- Create: `resources/views/purchase/pipeline/index.blade.php`
- [ ] **Step 1: Write PurchasePipelineController**
```php
orderByRaw("CASE stage
WHEN 'draft' THEN 0 WHEN 'gm_approval' THEN 1 WHEN 'rfq' THEN 2
WHEN 'quoting' THEN 3 WHEN 'comparison' THEN 4 WHEN 'lpo' THEN 5
WHEN 'receiving' THEN 6 WHEN 'payment' THEN 7 WHEN 'complete' THEN 8
ELSE 9 END")
->latest()->get();
return view('purchase.pipeline.index', compact('requests', 'stages'));
}
}
```
- [ ] **Step 2: Write the pipeline index view**
```blade
@extends('layouts.app')
@section('content')
{{-- Stage detail text --}}
@if($done || $current)
@if($stage === 'draft') Created by {{ $pr->requestedBy->name }} · {{ $pr->created_at->format('d M Y') }}
@elseif($stage === 'gm_approval' && $pr->signature) Signed by {{ $pr->signature->signedBy->name }} · {{ $pr->signature->signed_at->format('d M Y') }}
@elseif($stage === 'rfq' || $stage === 'quoting') {{ $pr->rfqInvitations->count() }} supplier(s) invited
@elseif($stage === 'comparison') {{ $pr->supplierQuotes->count() }} quote(s) received
@elseif($stage === 'lpo' && $pr->awardedQuote) Awarded to {{ $pr->awardedQuote->supplier->name }}
@endif
@endif
@endforeach
@endforeach
@endsection
```
- [ ] **Step 3: Commit**
```bash
git add app/Http/Controllers/Purchase/PurchasePipelineController.php resources/views/purchase/pipeline/
git commit -m "feat: purchase pipeline index — card list with vertical timeline"
```
---
## Task 11: Route wiring + sidebar navigation link
**Files:**
- Modify: `routes/web.php`
- Modify: `resources/views/layouts/app.blade.php`
- [ ] **Step 1: Add all new routes to `routes/web.php`**
Add these imports at the top of `web.php` (after existing Purchase imports):
```php
use App\Http\Controllers\Purchase\PurchasePipelineController;
use App\Http\Controllers\Purchase\PurchaseSignatureController;
use App\Http\Controllers\Purchase\RfqController;
use App\Http\Controllers\Purchase\SupplierQuoteController;
use App\Http\Controllers\Purchase\RfqPortalController;
```
Add the public RFQ route **before** the auth middleware group:
```php
// Public — no auth required
Route::get('/rfq/{token}', [RfqPortalController::class, 'show'])->name('rfq.show');
Route::post('/rfq/{token}', [RfqPortalController::class, 'submit'])->name('rfq.submit');
```
Inside the `purchase.` prefix group, add:
```php
Route::get('pipeline', [PurchasePipelineController::class, 'index'])->name('pipeline.index');
Route::get('requests/{request}/sign', [PurchaseSignatureController::class, 'show'])->name('requests.sign');
Route::post('requests/{request}/sign', [PurchaseSignatureController::class, 'store'])->name('requests.sign.store');
Route::get('requests/{request}/rfq', [RfqController::class, 'show'])->name('requests.rfq');
Route::post('requests/{request}/rfq', [RfqController::class, 'store'])->name('requests.rfq.store');
Route::get('requests/{request}/quotes', [SupplierQuoteController::class, 'index'])->name('requests.quotes');
Route::get('requests/{request}/compare', [SupplierQuoteController::class, 'compare'])->name('requests.compare');
Route::post('requests/{request}/quotes/{quote}/award', [SupplierQuoteController::class, 'award'])->name('requests.quotes.award');
```
- [ ] **Step 2: Add sidebar link to the layout**
Open `resources/views/layouts/app.blade.php`. Find where the sidebar purchase links are. Add a "Pipeline" link as the first item under Purchase:
```blade
Pipeline
```
(Match the exact element tag/class used by the rest of the sidebar navigation links in the file.)
- [ ] **Step 3: Verify routes load**
```
php artisan route:list --path=purchase/pipeline
php artisan route:list --path=rfq
```
Expected: `purchase.pipeline.index`, `rfq.show`, `rfq.submit` listed.
- [ ] **Step 4: Commit**
```bash
git add routes/web.php resources/views/layouts/app.blade.php
git commit -m "feat: wire pipeline routes and sidebar navigation"
```
---
## Task 12: Seed existing purchase requests with `draft` stage
Existing `purchase_requests` rows will have a NULL `stage` (migration sets default `draft` for new rows but existing NULL rows won't match the pipeline logic).
**Files:**
- Create: `database/seeders/PurchaseRequestStagePatchSeeder.php`
- [ ] **Step 1: Write the one-time patch seeder**
```php
where('status', 'approved')->whereNull('stage')->update(['stage' => 'rfq']);
DB::table('purchase_requests')->where('status', 'pending')->whereNull('stage')->update(['stage' => 'gm_approval']);
DB::table('purchase_requests')->whereNull('stage')->update(['stage' => 'draft']);
}
}
```
- [ ] **Step 2: Run the seeder**
```
php artisan db:seed --class=PurchaseRequestStagePatchSeeder
```
Expected: No errors; existing rows now have non-null stage values.
- [ ] **Step 3: Commit**
```bash
git add database/seeders/PurchaseRequestStagePatchSeeder.php
git commit -m "feat: patch existing purchase_requests with initial stage values"
```
---
## Task 13: GRN receiving view — toggle Inventory / Consumable per item
**Files:**
- Modify: `resources/views/purchase/grns/create.blade.php`
This is a targeted edit to the existing GRN creation form. Find the item rows section and add a toggle after the quantity/cost fields for each item.
- [ ] **Step 1: Read the existing GRN create view** to understand how item rows are rendered before editing.
```
Read: resources/views/purchase/grns/create.blade.php
```
- [ ] **Step 2: For each item row in the form, add a type toggle**
After the existing quantity / unit cost inputs for each GRN item, add:
```blade
```
And a small JS helper (once in the view, not per row):
```javascript
function setGrnType(span, val) {
const parent = span.closest('label').parentElement;
parent.querySelectorAll('.grn-type-btn').forEach(b => {
b.style.background = '#fff'; b.style.color = '#64748b';
});
span.style.background = val === 'inventory' ? '#eff6ff' : '#fef3c7';
span.style.color = val === 'inventory' ? '#2563eb' : '#92400e';
}
```
- [ ] **Step 3: Update GoodsReceiptNoteController to accept and store the `type` field**
In `GoodsReceiptNoteController::store()`, when creating each `GrnItem`, pass `type` from the request:
```php
$item->type = $itemData['type'] ?? 'inventory';
$item->save();
```
(Read the exact controller code first to insert at the right location.)
- [ ] **Step 4: Commit**
```bash
git add resources/views/purchase/grns/create.blade.php app/Http/Controllers/Purchase/GoodsReceiptNoteController.php
git commit -m "feat: GRN item type toggle — inventory or consumable"
```
---
## Self-Review Checklist
**Spec coverage:**
- [x] Stage 1 (Purchase Request) — existing, no new code needed; stage seeded via Task 12
- [x] Stage 2 (GM Signature) — Task 6
- [x] Stage 3 (Select Suppliers + RFQ) — Task 7
- [x] Stage 4 (Supplier Quotes via private link) — Task 8
- [x] Stage 5 (Comparison + Award) — Task 9
- [x] Stage 6 (LPO) — action button links to existing `purchase.orders.create`; no new controller needed
- [x] Stage 7 (Receiving with inventory/consumable) — Task 13
- [x] Stage 8 (Payment) — action button links to existing `purchase.payments.create`; no new controller needed
- [x] Pipeline index view — Task 10
- [x] Stage advancement via `PurchaseStageService` — Task 5
- [x] Token expiry, one-submit-only — `RfqPortalController::show()` checks both
- [x] Awarding is irreversible / locks comparison — `SupplierQuoteController::award()` guard
- [x] WhatsApp deep link — `RfqInvitationService::whatsappLink()`
- [x] Email send — `RfqInvitationMail` + `rfq-invitation.blade.php`
- [x] Public route outside auth — Task 11 adds routes before middleware group
- [x] `grn_items.type` field — Task 2 + Task 13
**Known dependency:** Stage 6 (LPO) and Stage 8 (Payment) action buttons link to existing controllers. Those controllers don't advance the pipeline stage automatically. A follow-up task should add `$stages->setStage($pr, 'receiving')` after LPO creation, and `$stages->setStage($pr, 'complete')` after payment creation. This can be done as a polish pass without blocking the core pipeline.