feat: supplier item name editing, N/A checkbox, and adjusted indicator on compare view
This commit is contained in:
parent
7b399d5167
commit
6061e8ca4f
@ -67,8 +67,10 @@ class RfqPortalController extends Controller
|
|||||||
'payment_terms' => ['nullable', 'string', 'max:200'],
|
'payment_terms' => ['nullable', 'string', 'max:200'],
|
||||||
'notes' => ['nullable', 'string', 'max:1000'],
|
'notes' => ['nullable', 'string', 'max:1000'],
|
||||||
'items' => ['required', 'array'],
|
'items' => ['required', 'array'],
|
||||||
'items.*.unit_price' => ['required', 'numeric', 'min:0'],
|
'items.*.unit_price' => ['nullable', 'numeric', 'min:0'],
|
||||||
'items.*.is_vatable' => ['nullable', 'boolean'],
|
'items.*.is_vatable' => ['nullable', 'boolean'],
|
||||||
|
'items.*.not_available' => ['nullable', 'boolean'],
|
||||||
|
'items.*.supplier_description' => ['nullable', 'string', 'max:500'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$expectedCode = session('rfq_confirm_' . $token);
|
$expectedCode = session('rfq_confirm_' . $token);
|
||||||
@ -98,10 +100,15 @@ class RfqPortalController extends Controller
|
|||||||
$vatRate = (float) Setting::get('vat_rate', 0);
|
$vatRate = (float) Setting::get('vat_rate', 0);
|
||||||
|
|
||||||
foreach ($purchaseItems as $i => $item) {
|
foreach ($purchaseItems as $i => $item) {
|
||||||
$unitPrice = (float)($validated['items'][$i]['unit_price'] ?? 0);
|
$notAvailable = !empty($validated['items'][$i]['not_available']);
|
||||||
|
$unitPrice = $notAvailable ? 0 : (float)($validated['items'][$i]['unit_price'] ?? 0);
|
||||||
$qty = (float)$item->quantity_required;
|
$qty = (float)$item->quantity_required;
|
||||||
$totalPrice = round($unitPrice * $qty, 3);
|
$totalPrice = $notAvailable ? 0 : round($unitPrice * $qty, 3);
|
||||||
$isVatable = !empty($validated['items'][$i]['is_vatable']);
|
$isVatable = !$notAvailable && !empty($validated['items'][$i]['is_vatable']);
|
||||||
|
$supplierDescription = !empty($validated['items'][$i]['supplier_description'])
|
||||||
|
? trim($validated['items'][$i]['supplier_description'])
|
||||||
|
: null;
|
||||||
|
|
||||||
$subtotal += $totalPrice;
|
$subtotal += $totalPrice;
|
||||||
|
|
||||||
if ($isVatable && $vatRate > 0) {
|
if ($isVatable && $vatRate > 0) {
|
||||||
@ -111,11 +118,13 @@ class RfqPortalController extends Controller
|
|||||||
SupplierQuoteItem::create([
|
SupplierQuoteItem::create([
|
||||||
'supplier_quote_id' => $quote->id,
|
'supplier_quote_id' => $quote->id,
|
||||||
'description' => $item->description,
|
'description' => $item->description,
|
||||||
|
'supplier_description'=> $supplierDescription,
|
||||||
'unit' => $item->unit ?? '',
|
'unit' => $item->unit ?? '',
|
||||||
'quantity' => $qty,
|
'quantity' => $qty,
|
||||||
'unit_price' => $unitPrice,
|
'unit_price' => $unitPrice,
|
||||||
'total_price' => $totalPrice,
|
'total_price' => $totalPrice,
|
||||||
'is_vatable' => $isVatable,
|
'is_vatable' => $isVatable,
|
||||||
|
'not_available' => $notAvailable,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,13 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
class SupplierQuoteItem extends Model
|
class SupplierQuoteItem extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'supplier_quote_id', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'is_vatable',
|
'supplier_quote_id', 'description', 'supplier_description', 'unit', 'quantity',
|
||||||
|
'unit_price', 'total_price', 'is_vatable', 'not_available',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'is_vatable' => 'boolean',
|
'is_vatable' => 'boolean',
|
||||||
|
'not_available' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function quote()
|
public function quote()
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('supplier_quote_items', function (Blueprint $table) {
|
||||||
|
$table->string('supplier_description')->nullable()->after('description');
|
||||||
|
$table->boolean('not_available')->default(false)->after('is_vatable');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('supplier_quote_items', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['supplier_description', 'not_available']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
440
docs/superpowers/plans/2026-06-01-vat-rfq.md
Normal file
440
docs/superpowers/plans/2026-06-01-vat-rfq.md
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
# VAT Settings + RFQ Per-Item Vatable Checkbox — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add a global VAT rate setting and per-item vatable checkboxes on the supplier RFQ portal, with live VAT breakdown in totals.
|
||||||
|
|
||||||
|
**Architecture:** Global VAT rate stored in existing `settings` key/value table. Per-item `is_vatable` flag added to `supplier_quote_items`. RFQ portal view updated with checkbox column and three-row footer (subtotal / VAT / grand total). New `VatSettingController` + `settings/vat` page added under the Admin-only System sidebar section.
|
||||||
|
|
||||||
|
**Tech Stack:** Laravel 12, PHP 8.2, SQLite, Blade, Alpine-free vanilla JS, Tailwind (inline styles per project convention)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Migration — add `is_vatable` to `supplier_quote_items`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `database/migrations/2026_06_01_090734_add_is_vatable_to_supplier_quote_items.php`
|
||||||
|
- Modify: `app/Models/SupplierQuoteItem.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the migration**
|
||||||
|
|
||||||
|
Replace the generated migration body with:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('supplier_quote_items', function (Blueprint $table) {
|
||||||
|
$table->boolean('is_vatable')->default(false)->after('total_price');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('supplier_quote_items', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('is_vatable');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run migration**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `Migrating: 2026_06_01_090734_add_is_vatable_to_supplier_quote_items` then `Migrated`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update SupplierQuoteItem model**
|
||||||
|
|
||||||
|
In `app/Models/SupplierQuoteItem.php`, update `$fillable`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected $fillable = [
|
||||||
|
'supplier_quote_id', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'is_vatable',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_vatable' => 'boolean',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add database/migrations/2026_06_01_090734_add_is_vatable_to_supplier_quote_items.php app/Models/SupplierQuoteItem.php
|
||||||
|
git commit -m "feat: add is_vatable column to supplier_quote_items"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: VatSettingController + route + view
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `app/Http/Controllers/Settings/VatSettingController.php`
|
||||||
|
- Create: `resources/views/settings/vat.blade.php`
|
||||||
|
- Modify: `routes/web.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create VatSettingController**
|
||||||
|
|
||||||
|
Create `app/Http/Controllers/Settings/VatSettingController.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Settings;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class VatSettingController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$vatRate = Setting::get('vat_rate', '0');
|
||||||
|
return view('settings.vat', compact('vatRate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'vat_rate' => ['required', 'numeric', 'min:0', 'max:100'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Setting::set('vat_rate', (string) $validated['vat_rate']);
|
||||||
|
|
||||||
|
return response()->json(['message' => 'VAT rate saved.', 'vat_rate' => $validated['vat_rate']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add routes**
|
||||||
|
|
||||||
|
In `routes/web.php`, after the integrations routes (around line 148), add:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// VAT settings
|
||||||
|
Route::get('settings/vat', [VatSettingController::class, 'index'])->name('settings.vat');
|
||||||
|
Route::post('settings/vat', [VatSettingController::class, 'update'])->name('settings.vat.update');
|
||||||
|
```
|
||||||
|
|
||||||
|
Also add the import at the top of the file with the other Settings controllers:
|
||||||
|
```php
|
||||||
|
use App\Http\Controllers\Settings\VatSettingController;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create the VAT settings view**
|
||||||
|
|
||||||
|
Create `resources/views/settings/vat.blade.php`:
|
||||||
|
|
||||||
|
```blade
|
||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Settings — VAT')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-5">
|
||||||
|
<h1 class="page-title">VAT Settings</h1>
|
||||||
|
<p class="page-subtitle">Set the global VAT rate applied to vatable items on supplier quotes.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="max-width:480px;">
|
||||||
|
<div style="background:white;border:1px solid #e2e8f0;border-radius:0.875rem;overflow:hidden;">
|
||||||
|
<div style="padding:1.25rem 1.5rem;border-bottom:1px solid #e2e8f0;background:#f8fafc;">
|
||||||
|
<div style="font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em;">VAT Configuration</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:1.5rem;">
|
||||||
|
<label style="display:block;font-size:12px;font-weight:600;color:#374151;margin-bottom:6px;">
|
||||||
|
VAT Rate (%)
|
||||||
|
</label>
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;">
|
||||||
|
<input type="number" id="vat-rate-input" value="{{ $vatRate }}"
|
||||||
|
min="0" max="100" step="0.01" placeholder="e.g. 10"
|
||||||
|
style="width:160px;padding:9px 12px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:14px;font-weight:600;outline:none;"
|
||||||
|
onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#e2e8f0'">
|
||||||
|
<span style="font-size:14px;color:#64748b;font-weight:500;">%</span>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:12px;color:#94a3b8;margin-top:8px;">
|
||||||
|
Enter 0 to disable VAT. Suppliers will see the VAT checkbox on their quote form when this is greater than 0.
|
||||||
|
</p>
|
||||||
|
<button onclick="saveVat()"
|
||||||
|
style="margin-top:20px;padding:10px 24px;background:#2563eb;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;">
|
||||||
|
Save VAT Rate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var CSRF = document.querySelector('meta[name="csrf-token"]').content;
|
||||||
|
function saveVat() {
|
||||||
|
var rate = document.getElementById('vat-rate-input').value;
|
||||||
|
fetch('{{ route('settings.vat.update') }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json', 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ vat_rate: rate })
|
||||||
|
}).then(function(r) {
|
||||||
|
return r.json().then(function(body) {
|
||||||
|
if (!r.ok) return Promise.reject(body);
|
||||||
|
return body;
|
||||||
|
});
|
||||||
|
}).then(function() {
|
||||||
|
showToast('VAT rate saved.', 'success');
|
||||||
|
}).catch(function(err) {
|
||||||
|
showToast((err.errors && err.errors.vat_rate ? err.errors.vat_rate[0] : null) || 'Failed to save.', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/Http/Controllers/Settings/VatSettingController.php resources/views/settings/vat.blade.php routes/web.php
|
||||||
|
git commit -m "feat: add VAT settings page and controller"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Add VAT link to sidebar
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `resources/views/layouts/app.blade.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add sidebar link**
|
||||||
|
|
||||||
|
In `resources/views/layouts/app.blade.php`, after the Integrations `<a>` tag (around line 198, just before `@endrole`), add:
|
||||||
|
|
||||||
|
```blade
|
||||||
|
<a href="{{ route('settings.vat') }}" style="
|
||||||
|
display:flex; align-items:center; gap:8px;
|
||||||
|
padding:7px 12px 7px 24px; border-radius:7px; margin-bottom:1px;
|
||||||
|
font-size:13px; text-decoration:none;
|
||||||
|
{{ request()->routeIs('settings.vat*') ? 'background:#1e293b;color:#fff;font-weight:500;' : 'color:#94a3b8;' }}
|
||||||
|
" onmouseover="if(!this.style.color.includes('fff'))this.style.color='#e2e8f0'" onmouseout="if(!this.style.background.includes('1e293b'))this.style.color='#94a3b8'">
|
||||||
|
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="flex-shrink:0;">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2z"/>
|
||||||
|
</svg>
|
||||||
|
VAT Settings
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add resources/views/layouts/app.blade.php
|
||||||
|
git commit -m "feat: add VAT Settings link to sidebar"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Update RfqPortalController — load VAT rate and store is_vatable
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `app/Http/Controllers/Purchase/RfqPortalController.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update show() to pass VAT rate**
|
||||||
|
|
||||||
|
Add `use App\Models\Setting;` at the top of the file.
|
||||||
|
|
||||||
|
Update the `show()` method — add before the `return view(...)` line:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$vatRate = (float) Setting::get('vat_rate', 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the return to:
|
||||||
|
|
||||||
|
```php
|
||||||
|
return view('rfq.show', compact('invitation', 'purchaseRequest', 'items', 'confirmCode', 'vatRate'));
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update submit() to store is_vatable and calculate VAT-inclusive total**
|
||||||
|
|
||||||
|
Add `'items.*.is_vatable' => ['nullable', 'boolean']` to the validation rules array.
|
||||||
|
|
||||||
|
Replace the items loop and total calculation:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$subtotal = 0;
|
||||||
|
$vatAmount = 0;
|
||||||
|
$vatRate = (float) Setting::get('vat_rate', 0);
|
||||||
|
|
||||||
|
foreach ($purchaseItems as $i => $item) {
|
||||||
|
$unitPrice = (float)($validated['items'][$i]['unit_price'] ?? 0);
|
||||||
|
$qty = (float)$item->quantity_required;
|
||||||
|
$totalPrice = round($unitPrice * $qty, 3);
|
||||||
|
$isVatable = !empty($validated['items'][$i]['is_vatable']);
|
||||||
|
$subtotal += $totalPrice;
|
||||||
|
|
||||||
|
if ($isVatable && $vatRate > 0) {
|
||||||
|
$vatAmount += round($totalPrice * $vatRate / 100, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
SupplierQuoteItem::create([
|
||||||
|
'supplier_quote_id' => $quote->id,
|
||||||
|
'description' => $item->description,
|
||||||
|
'unit' => $item->unit ?? '',
|
||||||
|
'quantity' => $qty,
|
||||||
|
'unit_price' => $unitPrice,
|
||||||
|
'total_price' => $totalPrice,
|
||||||
|
'is_vatable' => $isVatable,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$grand = round($subtotal + $vatAmount, 3);
|
||||||
|
$quote->update(['total_amount' => $grand]);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/Http/Controllers/Purchase/RfqPortalController.php
|
||||||
|
git commit -m "feat: load VAT rate and store is_vatable in RFQ portal"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Update rfq/show.blade.php — checkbox column + VAT footer + JS
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `resources/views/rfq/show.blade.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add VAT rate JS variable**
|
||||||
|
|
||||||
|
At the start of the `<script>` block, add:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var _vatRate = {{ $vatRate }};
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace calcRow() and add recalcTotals()**
|
||||||
|
|
||||||
|
Replace the entire `calcRow` function and grand total logic with:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var _totals = {};
|
||||||
|
var _vatable = {};
|
||||||
|
|
||||||
|
function calcRow(i, qty) {
|
||||||
|
var inp = document.querySelector('input[name="items[' + i + '][unit_price]"]');
|
||||||
|
var cb = document.querySelector('input[name="items[' + i + '][is_vatable]"]');
|
||||||
|
var up = parseFloat(inp ? inp.value : 0) || 0;
|
||||||
|
var tot = Math.round(up * qty * 1000) / 1000;
|
||||||
|
_totals[i] = tot;
|
||||||
|
_vatable[i] = cb ? cb.checked : false;
|
||||||
|
var el = document.getElementById('tot-' + i);
|
||||||
|
if (el) el.textContent = tot > 0 ? 'BD ' + tot.toFixed(3) : '—';
|
||||||
|
recalcTotals();
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalcTotals() {
|
||||||
|
var subtotal = 0;
|
||||||
|
var vatAmount = 0;
|
||||||
|
Object.keys(_totals).forEach(function(i) {
|
||||||
|
subtotal += _totals[i];
|
||||||
|
if (_vatable[i] && _vatRate > 0) {
|
||||||
|
vatAmount += Math.round(_totals[i] * _vatRate / 100 * 1000) / 1000;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var grand = Math.round((subtotal + vatAmount) * 1000) / 1000;
|
||||||
|
|
||||||
|
var elSub = document.getElementById('subtotal-row');
|
||||||
|
var elVat = document.getElementById('vat-row');
|
||||||
|
var elGrand = document.getElementById('grand-total');
|
||||||
|
|
||||||
|
if (elSub) elSub.textContent = 'BD ' + subtotal.toFixed(3);
|
||||||
|
if (elVat) {
|
||||||
|
elVat.textContent = 'BD ' + vatAmount.toFixed(3);
|
||||||
|
var vatTr = document.getElementById('vat-tr');
|
||||||
|
if (vatTr) vatTr.style.display = _vatRate > 0 ? '' : 'none';
|
||||||
|
}
|
||||||
|
if (elGrand) elGrand.textContent = 'BD ' + grand.toFixed(3);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update table header**
|
||||||
|
|
||||||
|
Replace the `<thead>` block:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Qty</th>
|
||||||
|
<th>Unit</th>
|
||||||
|
<th style="text-align:center;">VAT?</th>
|
||||||
|
<th style="text-align:right;">Unit Price (BD)</th>
|
||||||
|
<th style="text-align:right;">Total (BD)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update each item row**
|
||||||
|
|
||||||
|
Replace the `@foreach` tbody rows:
|
||||||
|
|
||||||
|
```blade
|
||||||
|
@foreach($items as $i => $item)
|
||||||
|
<tr>
|
||||||
|
<td style="color:#94a3b8;font-size:12px;">{{ $i + 1 }}</td>
|
||||||
|
<td style="font-weight:500;">{{ $item->description }}</td>
|
||||||
|
<td>{{ rtrim(rtrim(number_format((float)$item->quantity_required, 3), '0'), '.') }}</td>
|
||||||
|
<td style="color:#64748b;">{{ $item->unit ?: '—' }}</td>
|
||||||
|
<td style="text-align:center;">
|
||||||
|
@if($vatRate > 0)
|
||||||
|
<input type="checkbox" name="items[{{ $i }}][is_vatable]" value="1"
|
||||||
|
onchange="calcRow({{ $i }}, {{ (float)$item->quantity_required }})"
|
||||||
|
style="width:16px;height:16px;accent-color:#2563eb;cursor:pointer;">
|
||||||
|
@else
|
||||||
|
<span style="color:#cbd5e1;font-size:11px;">—</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td style="text-align:right;">
|
||||||
|
<input type="number" class="price" name="items[{{ $i }}][unit_price]"
|
||||||
|
min="0" step="0.001" required placeholder="0.000"
|
||||||
|
oninput="calcRow({{ $i }}, {{ (float)$item->quantity_required }})">
|
||||||
|
</td>
|
||||||
|
<td style="text-align:right;font-weight:600;" id="tot-{{ $i }}">—</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Update tfoot**
|
||||||
|
|
||||||
|
Replace the `<tfoot>` block:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<tfoot>
|
||||||
|
<tr style="background:#f8fafc;">
|
||||||
|
<td colspan="6" style="text-align:right;font-size:13px;color:#475569;">Subtotal:</td>
|
||||||
|
<td style="text-align:right;font-size:14px;color:#475569;" id="subtotal-row">BD 0.000</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="vat-tr" style="background:#fffbeb;{{ $vatRate > 0 ? '' : 'display:none;' }}">
|
||||||
|
<td colspan="6" style="text-align:right;font-size:13px;color:#92400e;">VAT ({{ $vatRate }}%):</td>
|
||||||
|
<td style="text-align:right;font-size:14px;color:#92400e;" id="vat-row">BD 0.000</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="background:#f8fafc;border-top:2px solid #e2e8f0;">
|
||||||
|
<td colspan="6" style="text-align:right;font-size:13px;color:#475569;font-weight:700;">Grand Total:</td>
|
||||||
|
<td style="text-align:right;font-size:15px;color:#2563eb;font-weight:700;" id="grand-total">BD 0.000</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add resources/views/rfq/show.blade.php
|
||||||
|
git commit -m "feat: add VAT checkbox column and live breakdown to RFQ portal"
|
||||||
|
```
|
||||||
@ -48,7 +48,7 @@
|
|||||||
{{-- Per-item rows --}}
|
{{-- Per-item rows --}}
|
||||||
@foreach($items as $i => $reqItem)
|
@foreach($items as $i => $reqItem)
|
||||||
@php
|
@php
|
||||||
$rowPrices = $quotes->map(fn($q) => optional($q->items->get($i))->unit_price)->filter()->values();
|
$rowPrices = $quotes->map(fn($q) => ($q->items->get($i) && !$q->items->get($i)->not_available) ? $q->items->get($i)->unit_price : null)->filter()->values();
|
||||||
$minPrice = $rowPrices->count() ? $rowPrices->min() : null;
|
$minPrice = $rowPrices->count() ? $rowPrices->min() : null;
|
||||||
@endphp
|
@endphp
|
||||||
<tr>
|
<tr>
|
||||||
@ -57,14 +57,24 @@
|
|||||||
<div style="font-size:11px;color:#94a3b8;margin-top:2px;">Qty: {{ $reqItem->quantity }} {{ $reqItem->unit }}</div>
|
<div style="font-size:11px;color:#94a3b8;margin-top:2px;">Qty: {{ $reqItem->quantity }} {{ $reqItem->unit }}</div>
|
||||||
</td>
|
</td>
|
||||||
@foreach($quotes as $q)
|
@foreach($quotes as $q)
|
||||||
@php $qItem = $q->items->get($i); $isMin = $qItem && $minPrice !== null && (float)$qItem->unit_price === (float)$minPrice && $rowPrices->count() > 1; @endphp
|
@php $qItem = $q->items->get($i); $isMin = $qItem && !$qItem->not_available && $minPrice !== null && (float)$qItem->unit_price === (float)$minPrice && $rowPrices->count() > 1; @endphp
|
||||||
<td style="padding:10px 14px;border-bottom:1px solid #f1f5f9;text-align:center;background:{{ $isMin ? '#f0fdf4' : '' }};">
|
<td style="padding:10px 14px;border-bottom:1px solid #f1f5f9;text-align:center;background:{{ $isMin ? '#f0fdf4' : '' }};">
|
||||||
@if($qItem)
|
@if($qItem)
|
||||||
|
@if($qItem->not_available)
|
||||||
|
<span style="font-size:11px;font-weight:700;color:#dc2626;background:#fef2f2;padding:2px 8px;border-radius:4px;border:1px solid #fecaca;">Not available</span>
|
||||||
|
@else
|
||||||
|
@if($qItem->supplier_description)
|
||||||
|
<div style="font-size:10px;color:#64748b;margin-bottom:2px;font-style:italic;">
|
||||||
|
"{{ $qItem->supplier_description }}"
|
||||||
|
<span style="background:#fef3c7;color:#92400e;font-size:9px;font-weight:700;padding:1px 5px;border-radius:3px;border:1px solid #fde68a;font-style:normal;margin-left:2px;">adjusted</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
<div style="font-weight:600;color:{{ $isMin ? '#15803d' : '#0f172a' }};">
|
<div style="font-weight:600;color:{{ $isMin ? '#15803d' : '#0f172a' }};">
|
||||||
@if($isMin)<span style="font-size:10px;font-weight:700;color:#15803d;display:block;">LOWEST</span>@endif
|
@if($isMin)<span style="font-size:10px;font-weight:700;color:#15803d;display:block;">LOWEST</span>@endif
|
||||||
BD {{ number_format($qItem->unit_price, 3) }}
|
BD {{ number_format($qItem->unit_price, 3) }}
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:11px;color:#64748b;margin-top:2px;">BD {{ number_format($qItem->total_price, 3) }}</div>
|
<div style="font-size:11px;color:#64748b;margin-top:2px;">BD {{ number_format($qItem->total_price, 3) }}</div>
|
||||||
|
@endif
|
||||||
@else
|
@else
|
||||||
<span style="color:#e2e8f0;">—</span>
|
<span style="color:#e2e8f0;">—</span>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@ -78,6 +78,7 @@
|
|||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Qty</th>
|
<th>Qty</th>
|
||||||
<th>Unit</th>
|
<th>Unit</th>
|
||||||
|
<th style="text-align:center;">N/A?</th>
|
||||||
<th style="text-align:center;">VAT?</th>
|
<th style="text-align:center;">VAT?</th>
|
||||||
<th style="text-align:right;">Unit Price (BD)</th>
|
<th style="text-align:right;">Unit Price (BD)</th>
|
||||||
<th style="text-align:right;">Total (BD)</th>
|
<th style="text-align:right;">Total (BD)</th>
|
||||||
@ -85,14 +86,46 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach($items as $i => $item)
|
@foreach($items as $i => $item)
|
||||||
<tr>
|
<tr id="row-{{ $i }}">
|
||||||
<td style="color:#94a3b8;font-size:12px;">{{ $i + 1 }}</td>
|
<td style="color:#94a3b8;font-size:12px;">{{ $i + 1 }}</td>
|
||||||
<td style="font-weight:500;">{{ $item->description }}</td>
|
<td>
|
||||||
|
{{-- Display mode --}}
|
||||||
|
<div id="desc-display-{{ $i }}" style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
|
||||||
|
<span id="desc-text-{{ $i }}" style="font-weight:500;">{{ $item->description }}</span>
|
||||||
|
<button type="button" onclick="editDesc({{ $i }})"
|
||||||
|
title="Edit item name"
|
||||||
|
style="flex-shrink:0;background:none;border:1px solid #e2e8f0;border-radius:5px;padding:2px 6px;cursor:pointer;color:#64748b;font-size:11px;line-height:1.4;"
|
||||||
|
onmouseover="this.style.borderColor='#2563eb';this.style.color='#2563eb'"
|
||||||
|
onmouseout="this.style.borderColor='#e2e8f0';this.style.color='#64748b'">
|
||||||
|
✎ Edit
|
||||||
|
</button>
|
||||||
|
<span id="desc-adj-{{ $i }}" style="display:none;font-size:10px;font-weight:700;background:#fef3c7;color:#92400e;padding:1px 6px;border-radius:4px;border:1px solid #fde68a;">adjusted</span>
|
||||||
|
</div>
|
||||||
|
{{-- Edit mode --}}
|
||||||
|
<div id="desc-edit-{{ $i }}" style="display:none;align-items:center;gap:6px;">
|
||||||
|
<input type="text" id="desc-input-{{ $i }}"
|
||||||
|
value="{{ $item->description }}"
|
||||||
|
style="flex:1;min-width:0;padding:5px 8px;border:1.5px solid #2563eb;border-radius:6px;font-size:13px;font-weight:500;outline:none;width:auto;">
|
||||||
|
<button type="button" onclick="saveDesc({{ $i }}, {{ json_encode($item->description) }})"
|
||||||
|
style="flex-shrink:0;background:#2563eb;color:#fff;border:none;border-radius:5px;padding:4px 8px;cursor:pointer;font-size:12px;font-weight:700;">✓</button>
|
||||||
|
<button type="button" onclick="cancelDesc({{ $i }})"
|
||||||
|
style="flex-shrink:0;background:#f1f5f9;color:#64748b;border:1px solid #e2e8f0;border-radius:5px;padding:4px 8px;cursor:pointer;font-size:12px;">✕</button>
|
||||||
|
</div>
|
||||||
|
{{-- Hidden field submitted with form --}}
|
||||||
|
<input type="hidden" name="items[{{ $i }}][supplier_description]" id="desc-hidden-{{ $i }}" value="">
|
||||||
|
</td>
|
||||||
<td>{{ rtrim(rtrim(number_format((float)$item->quantity_required, 3), '0'), '.') }}</td>
|
<td>{{ rtrim(rtrim(number_format((float)$item->quantity_required, 3), '0'), '.') }}</td>
|
||||||
<td style="color:#64748b;">{{ $item->unit ?: '—' }}</td>
|
<td style="color:#64748b;">{{ $item->unit ?: '—' }}</td>
|
||||||
|
{{-- N/A checkbox --}}
|
||||||
|
<td style="text-align:center;">
|
||||||
|
<input type="checkbox" name="items[{{ $i }}][not_available]" id="na-{{ $i }}" value="1"
|
||||||
|
onchange="toggleNA({{ $i }}, {{ (float)$item->quantity_required }})"
|
||||||
|
style="width:16px;height:16px;accent-color:#dc2626;cursor:pointer;">
|
||||||
|
</td>
|
||||||
|
{{-- VAT checkbox --}}
|
||||||
<td style="text-align:center;">
|
<td style="text-align:center;">
|
||||||
@if($vatRate > 0)
|
@if($vatRate > 0)
|
||||||
<input type="checkbox" name="items[{{ $i }}][is_vatable]" value="1"
|
<input type="checkbox" name="items[{{ $i }}][is_vatable]" id="vat-cb-{{ $i }}" value="1"
|
||||||
onchange="calcRow({{ $i }}, {{ (float)$item->quantity_required }})"
|
onchange="calcRow({{ $i }}, {{ (float)$item->quantity_required }})"
|
||||||
style="width:16px;height:16px;accent-color:#2563eb;cursor:pointer;">
|
style="width:16px;height:16px;accent-color:#2563eb;cursor:pointer;">
|
||||||
@else
|
@else
|
||||||
@ -100,7 +133,7 @@
|
|||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:right;">
|
<td style="text-align:right;">
|
||||||
<input type="number" class="price" name="items[{{ $i }}][unit_price]"
|
<input type="number" class="price" id="price-{{ $i }}" name="items[{{ $i }}][unit_price]"
|
||||||
min="0" step="0.001" required placeholder="0.000"
|
min="0" step="0.001" required placeholder="0.000"
|
||||||
oninput="calcRow({{ $i }}, {{ (float)$item->quantity_required }})">
|
oninput="calcRow({{ $i }}, {{ (float)$item->quantity_required }})">
|
||||||
</td>
|
</td>
|
||||||
@ -110,15 +143,15 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr style="background:#f8fafc;">
|
<tr style="background:#f8fafc;">
|
||||||
<td colspan="6" style="text-align:right;font-size:13px;color:#475569;">Subtotal:</td>
|
<td colspan="7" style="text-align:right;font-size:13px;color:#475569;">Subtotal:</td>
|
||||||
<td style="text-align:right;font-size:14px;color:#475569;" id="subtotal-row">BD 0.000</td>
|
<td style="text-align:right;font-size:14px;color:#475569;" id="subtotal-row">BD 0.000</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr id="vat-tr" style="background:#fffbeb;{{ $vatRate > 0 ? '' : 'display:none;' }}">
|
<tr id="vat-tr" style="background:#fffbeb;{{ $vatRate > 0 ? '' : 'display:none;' }}">
|
||||||
<td colspan="6" style="text-align:right;font-size:13px;color:#92400e;">VAT ({{ $vatRate }}%):</td>
|
<td colspan="7" style="text-align:right;font-size:13px;color:#92400e;">VAT ({{ $vatRate }}%):</td>
|
||||||
<td style="text-align:right;font-size:14px;color:#92400e;" id="vat-row">BD 0.000</td>
|
<td style="text-align:right;font-size:14px;color:#92400e;" id="vat-row">BD 0.000</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style="background:#f8fafc;border-top:2px solid #e2e8f0;">
|
<tr style="background:#f8fafc;border-top:2px solid #e2e8f0;">
|
||||||
<td colspan="6" style="text-align:right;font-size:13px;color:#475569;font-weight:700;">Grand Total:</td>
|
<td colspan="7" style="text-align:right;font-size:13px;color:#475569;font-weight:700;">Grand Total:</td>
|
||||||
<td style="text-align:right;font-size:15px;color:#2563eb;font-weight:700;" id="grand-total">BD 0.000</td>
|
<td style="text-align:right;font-size:15px;color:#2563eb;font-weight:700;" id="grand-total">BD 0.000</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
@ -225,10 +258,74 @@
|
|||||||
var _vatRate = {{ $vatRate }};
|
var _vatRate = {{ $vatRate }};
|
||||||
var _totals = {};
|
var _totals = {};
|
||||||
var _vatable = {};
|
var _vatable = {};
|
||||||
|
var _navail = {};
|
||||||
|
|
||||||
|
// ── Description inline edit ──────────────────────────────
|
||||||
|
function editDesc(i) {
|
||||||
|
document.getElementById('desc-display-' + i).style.display = 'none';
|
||||||
|
var ed = document.getElementById('desc-edit-' + i);
|
||||||
|
ed.style.display = 'flex';
|
||||||
|
document.getElementById('desc-input-' + i).focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveDesc(i, original) {
|
||||||
|
var val = document.getElementById('desc-input-' + i).value.trim();
|
||||||
|
if (!val) val = original;
|
||||||
|
document.getElementById('desc-text-' + i).textContent = val;
|
||||||
|
document.getElementById('desc-hidden-' + i).value = (val !== original) ? val : '';
|
||||||
|
var adj = document.getElementById('desc-adj-' + i);
|
||||||
|
adj.style.display = (val !== original) ? 'inline-block' : 'none';
|
||||||
|
document.getElementById('desc-edit-' + i).style.display = 'none';
|
||||||
|
document.getElementById('desc-display-' + i).style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelDesc(i) {
|
||||||
|
var original = document.getElementById('desc-text-' + i).textContent;
|
||||||
|
document.getElementById('desc-input-' + i).value = original;
|
||||||
|
document.getElementById('desc-edit-' + i).style.display = 'none';
|
||||||
|
document.getElementById('desc-display-' + i).style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── N/A toggle ───────────────────────────────────────────
|
||||||
|
function toggleNA(i, qty) {
|
||||||
|
var naCb = document.getElementById('na-' + i);
|
||||||
|
var priceIn = document.getElementById('price-' + i);
|
||||||
|
var vatCb = document.getElementById('vat-cb-' + i);
|
||||||
|
var totEl = document.getElementById('tot-' + i);
|
||||||
|
var row = document.getElementById('row-' + i);
|
||||||
|
var na = naCb.checked;
|
||||||
|
_navail[i] = na;
|
||||||
|
|
||||||
|
if (na) {
|
||||||
|
priceIn.value = '';
|
||||||
|
priceIn.disabled = true;
|
||||||
|
priceIn.removeAttribute('required');
|
||||||
|
priceIn.style.opacity = '.35';
|
||||||
|
if (vatCb) { vatCb.checked = false; vatCb.disabled = true; vatCb.style.opacity = '.35'; }
|
||||||
|
totEl.innerHTML = '<span style="font-size:11px;font-weight:700;color:#dc2626;background:#fef2f2;padding:2px 7px;border-radius:4px;">Not available</span>';
|
||||||
|
row.style.opacity = '.6';
|
||||||
|
_totals[i] = 0;
|
||||||
|
_vatable[i] = false;
|
||||||
|
} else {
|
||||||
|
priceIn.disabled = false;
|
||||||
|
priceIn.setAttribute('required', '');
|
||||||
|
priceIn.style.opacity = '1';
|
||||||
|
if (vatCb) { vatCb.disabled = false; vatCb.style.opacity = '1'; }
|
||||||
|
totEl.textContent = '—';
|
||||||
|
row.style.opacity = '1';
|
||||||
|
_totals[i] = 0;
|
||||||
|
_vatable[i] = false;
|
||||||
|
calcRow(i, qty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
recalcTotals();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Price / VAT calculation ──────────────────────────────
|
||||||
function calcRow(i, qty) {
|
function calcRow(i, qty) {
|
||||||
var inp = document.querySelector('input[name="items[' + i + '][unit_price]"]');
|
if (_navail[i]) return;
|
||||||
var cb = document.querySelector('input[name="items[' + i + '][is_vatable]"]');
|
var inp = document.getElementById('price-' + i);
|
||||||
|
var cb = document.getElementById('vat-cb-' + i);
|
||||||
var up = parseFloat(inp ? inp.value : 0) || 0;
|
var up = parseFloat(inp ? inp.value : 0) || 0;
|
||||||
var tot = Math.round(up * qty * 1000) / 1000;
|
var tot = Math.round(up * qty * 1000) / 1000;
|
||||||
_totals[i] = tot;
|
_totals[i] = tot;
|
||||||
@ -242,6 +339,7 @@ function recalcTotals() {
|
|||||||
var subtotal = 0;
|
var subtotal = 0;
|
||||||
var vatAmount = 0;
|
var vatAmount = 0;
|
||||||
Object.keys(_totals).forEach(function(i) {
|
Object.keys(_totals).forEach(function(i) {
|
||||||
|
if (_navail[i]) return;
|
||||||
subtotal += _totals[i];
|
subtotal += _totals[i];
|
||||||
if (_vatable[i] && _vatRate > 0) {
|
if (_vatable[i] && _vatRate > 0) {
|
||||||
vatAmount += Math.round(_totals[i] * _vatRate / 100 * 1000) / 1000;
|
vatAmount += Math.round(_totals[i] * _vatRate / 100 * 1000) / 1000;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user