feat: add pre-delivery item rejection tracking to invoices

- Add pre_delivery_status column to order_items table
- Track approved/rejected items during buyer pre-delivery review
- Display rejected items with strikethrough styling in invoices
- Add isPreDeliveryRejected() and shouldBeProcessed() helper methods
- Show rejection badges on invoice line items
- Handle delivered_qty fallback to picked_qty for invoicing
- Apply styling to buyer, seller, and PDF invoice views

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jon Leopard
2025-11-17 14:32:01 -07:00
parent a95d875564
commit b37cb2b5c9
5 changed files with 175 additions and 54 deletions

View File

@@ -22,6 +22,8 @@ class OrderItem extends Model implements AuditableContract
'batch_number',
'quantity',
'picked_qty',
'pre_delivery_status',
'delivered_qty',
'accepted_qty',
'rejected_qty',
'rejection_reason',
@@ -45,6 +47,7 @@ class OrderItem extends Model implements AuditableContract
protected $casts = [
'quantity' => 'integer',
'picked_qty' => 'integer',
'delivered_qty' => 'integer',
'accepted_qty' => 'integer',
'rejected_qty' => 'integer',
'unit_price' => 'decimal:2',
@@ -132,4 +135,21 @@ class OrderItem extends Model implements AuditableContract
return ($this->accepted_qty / $this->quantity) * 100;
}
/**
* Check if item was rejected during pre-delivery review
*/
public function isPreDeliveryRejected(): bool
{
return $this->pre_delivery_status === 'rejected';
}
/**
* Check if item should be included in picking, delivery, and invoicing
* Items rejected pre-delivery are excluded from the fulfillment process
*/
public function shouldBeProcessed(): bool
{
return $this->pre_delivery_status !== 'rejected';
}
}

View File

@@ -0,0 +1,34 @@
<?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('order_items', function (Blueprint $table) {
// Add pre_delivery_status to track buyer's pre-delivery approval/rejection
// NULL = not yet reviewed (backwards compatibility)
// 'approved' = buyer approved item for delivery
// 'rejected' = buyer rejected item before delivery (won't be picked/delivered/invoiced)
$table->string('pre_delivery_status', 20)->nullable()->after('picked_qty');
$table->index('pre_delivery_status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('order_items', function (Blueprint $table) {
$table->dropIndex(['pre_delivery_status']);
$table->dropColumn('pre_delivery_status');
});
}
};

View File

@@ -109,12 +109,15 @@
<!-- Order Items List -->
<div class="space-y-2 mb-4">
@foreach($invoice->order->items as $item)
<div class="flex justify-between text-sm">
<div class="flex justify-between text-sm {{ $item->isPreDeliveryRejected() ? 'opacity-50' : '' }}">
<div class="flex-1">
<span class="font-medium">{{ $item->product_name }}</span>
<span class="font-medium {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">{{ $item->product_name }}</span>
<span class="text-base-content/60 ml-2">× {{ $item->delivered_qty ?? $item->picked_qty }}</span>
@if($item->isPreDeliveryRejected())
<span class="badge badge-xs badge-ghost ml-2">Rejected</span>
@endif
</div>
<span class="font-semibold">${{ number_format($item->line_total, 2) }}</span>
<span class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">${{ number_format($item->line_total, 2) }}</span>
</div>
@endforeach
</div>
@@ -156,32 +159,58 @@
<!-- Invoice Line Items -->
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title mb-4">Line Items</h2>
<h2 class="card-title mb-4">
<span class="icon-[lucide--package] size-5"></span>
Line Items
</h2>
<div class="overflow-x-auto">
<table class="table w-full">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>Product</th>
<th>SKU</th>
<th>Brand</th>
<th>Quantity</th>
<th>Quantity (Fulfilled/Ordered)</th>
<th>Unit Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
@foreach($invoice->order->items as $item)
<tr>
<tr class="{{ $item->isPreDeliveryRejected() ? 'opacity-60' : '' }}">
<td>
<div class="font-semibold">{{ $item->product_name }}</div>
<div class="flex items-center gap-3">
@if($item->product)
<div class="avatar">
<div class="w-12 h-12 rounded-lg bg-base-200">
@if($item->product->image_path)
<img src="{{ Storage::url($item->product->image_path) }}" alt="{{ $item->product_name }}">
@else
<span class="icon-[lucide--package] size-6 text-base-content/40"></span>
@endif
</div>
</div>
@endif
<div class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">
{{ $item->product_name }}
@if($item->isPreDeliveryRejected())
<span class="badge badge-sm badge-ghost ml-2">Rejected</span>
@endif
</div>
</div>
</td>
<td class="font-mono text-sm">{{ $item->product_sku }}</td>
<td>{{ $item->brand_name }}</td>
<td class="font-mono text-sm {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">{{ $item->product_sku }}</td>
<td class="{{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">{{ $item->brand_name }}</td>
<td>
<span class="font-semibold">{{ $item->delivered_qty ?? $item->picked_qty }}</span>
<span class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">
{{ $item->delivered_qty ?? $item->picked_qty }}
<span class="text-gray-400">/{{ $item->quantity }}</span>
</span>
</td>
<td class="{{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">${{ number_format($item->unit_price, 2) }}</td>
<td class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">
${{ number_format($item->line_total, 2) }}
</td>
<td>${{ number_format($item->unit_price, 2) }}</td>
<td class="font-semibold">${{ number_format($item->line_total, 2) }}</td>
</tr>
@endforeach
</tbody>
@@ -190,6 +219,12 @@
<td colspan="5" class="text-right">Subtotal:</td>
<td>${{ number_format($invoice->subtotal, 2) }}</td>
</tr>
@if($invoice->tax > 0)
<tr>
<td colspan="5" class="text-right">Tax:</td>
<td>${{ number_format($invoice->tax, 2) }}</td>
</tr>
@endif
<tr class="font-bold text-lg">
<td colspan="5" class="text-right">Total:</td>
<td class="text-primary">${{ number_format($invoice->total, 2) }}</td>

View File

@@ -331,13 +331,18 @@
</thead>
<tbody>
@foreach($items as $item)
<tr>
<td>{{ $item->product_sku }}</td>
<td>{{ $item->product_name }}</td>
<td>{{ $item->brand_name }}</td>
<td class="right">{{ number_format($item->delivered_qty ?? $item->picked_qty, 0) }}</td>
<td class="right">${{ number_format($item->unit_price, 2) }}</td>
<td class="right">${{ number_format($item->line_total, 2) }}</td>
<tr @if($item->isPreDeliveryRejected()) style="opacity: 0.6;" @endif>
<td @if($item->isPreDeliveryRejected()) style="text-decoration: line-through;" @endif>{{ $item->product_sku }}</td>
<td @if($item->isPreDeliveryRejected()) style="text-decoration: line-through;" @endif>
{{ $item->product_name }}
@if($item->isPreDeliveryRejected())
<span style="color: #6b7280; font-size: 9pt; font-weight: bold;"> [REJECTED]</span>
@endif
</td>
<td @if($item->isPreDeliveryRejected()) style="text-decoration: line-through;" @endif>{{ $item->brand_name }}</td>
<td class="right" @if($item->isPreDeliveryRejected()) style="text-decoration: line-through;" @endif>{{ number_format($item->delivered_qty ?? $item->picked_qty, 0) }}</td>
<td class="right" @if($item->isPreDeliveryRejected()) style="text-decoration: line-through;" @endif>${{ number_format($item->unit_price, 2) }}</td>
<td class="right" @if($item->isPreDeliveryRejected()) style="text-decoration: line-through;" @endif>${{ number_format($item->line_total, 2) }}</td>
</tr>
@endforeach
</tbody>

View File

@@ -13,6 +13,10 @@
<span class="icon-[lucide--arrow-left] size-5"></span>
Back to Order
</a>
<a href="{{ route('seller.business.invoices.pdf', [$business->slug, $invoice]) }}" class="btn btn-primary" target="_blank">
<span class="icon-[lucide--download] size-5"></span>
Download PDF
</a>
</div>
</div>
@@ -74,7 +78,7 @@
<!-- Invoice Information Grid -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<!-- Invoice Details -->
<div class="card bg-base-100 shadow-lg">
<div class="card bg-base-100 shadow">
<div class="card-body">
<h3 class="font-bold text-lg mb-4">
<span class="icon-[lucide--file-text] size-5 inline"></span>
@@ -107,7 +111,7 @@
</div>
<!-- Company Information -->
<div class="card bg-base-100 shadow-lg">
<div class="card bg-base-100 shadow">
<div class="card-body">
<h3 class="font-bold text-lg mb-4">
<span class="icon-[lucide--building-2] size-5 inline"></span>
@@ -153,47 +157,65 @@
</div>
</div>
<!-- Payment Summary -->
<div class="card bg-base-100 shadow-lg">
<!-- Order Summary -->
<div class="card bg-base-100 shadow">
<div class="card-body">
<h3 class="font-bold text-lg mb-4">
<span class="icon-[lucide--dollar-sign] size-5 inline"></span>
Payment Summary
</h3>
<h3 class="font-bold text-lg mb-4">Order Summary</h3>
<!-- Order Items List -->
<div class="space-y-2 mb-4">
@foreach($invoice->order->items as $item)
<div class="flex justify-between text-sm {{ $item->isPreDeliveryRejected() ? 'opacity-50' : '' }}">
<div class="flex-1">
<span class="font-medium {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">{{ $item->product_name }}</span>
<span class="text-base-content/60 ml-2">× {{ $item->delivered_qty ?? $item->picked_qty }}</span>
@if($item->isPreDeliveryRejected())
<span class="badge badge-xs badge-ghost ml-2">Rejected</span>
@endif
</div>
<span class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">${{ number_format($item->line_total, 2) }}</span>
</div>
@endforeach
</div>
<div class="divider my-2"></div>
<div class="space-y-3">
<div class="flex justify-between">
@if($invoice->order && $invoice->order->surcharge > 0)
<div class="flex justify-between text-sm">
<span class="text-gray-600">Subtotal</span>
<span class="font-semibold">${{ number_format($invoice->subtotal, 2) }}</span>
</div>
@if($invoice->order && $invoice->order->surcharge > 0)
<div class="flex justify-between text-sm">
<span class="text-gray-600">Payment Terms Surcharge ({{ \App\Models\Order::getSurchargePercentage($invoice->order->payment_terms) }}%)</span>
<span class="font-semibold">${{ number_format($invoice->order->surcharge, 2) }}</span>
</div>
@endif
<div class="divider my-2"></div>
<div class="flex justify-between text-lg">
<span class="font-bold">Total</span>
<span class="font-bold text-primary">${{ number_format($invoice->total, 2) }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Amount Paid</span>
<span class="font-semibold text-success">${{ number_format($invoice->amount_paid, 2) }}</span>
</div>
<div class="flex justify-between text-lg">
<span class="font-bold">Amount Due</span>
<span class="font-bold text-error">${{ number_format($invoice->amount_due, 2) }}</span>
</div>
<div class="divider my-2"></div>
<div>
<p class="text-sm text-gray-600">Payment Terms</p>
<p class="font-semibold">
<div class="flex justify-between text-sm">
<span class="text-gray-600">Payment Terms</span>
<span class="font-semibold">
@if($invoice->order->payment_terms === 'cod')
COD
@else
{{ ucfirst(str_replace('_', ' ', $invoice->order->payment_terms)) }}
@endif
</p>
</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Payment Status</span>
<div>
<span class="badge badge-sm {{ $invoice->payment_status === 'paid' ? 'badge-success' : ($invoice->isOverdue() ? 'badge-error' : 'badge-warning') }}">
{{ ucfirst(str_replace('_', ' ', $invoice->payment_status)) }}
@if($invoice->isOverdue())
(Overdue)
@endif
</span>
</div>
</div>
</div>
</div>
@@ -201,7 +223,7 @@
</div>
<!-- Invoice Line Items -->
<div class="card bg-base-100 shadow-lg">
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title mb-4">
<span class="icon-[lucide--package] size-5"></span>
@@ -221,7 +243,7 @@
</thead>
<tbody>
@foreach($invoice->order->items as $item)
<tr>
<tr class="{{ $item->isPreDeliveryRejected() ? 'opacity-60' : '' }}">
<td>
<div class="flex items-center gap-3">
@if($item->product)
@@ -235,20 +257,25 @@
</div>
</div>
@endif
<div class="font-semibold">{{ $item->product_name }}</div>
<div class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">
{{ $item->product_name }}
@if($item->isPreDeliveryRejected())
<span class="badge badge-sm badge-ghost ml-2">Rejected</span>
@endif
</div>
</div>
</td>
<td class="font-mono text-sm">{{ $item->product_sku }}</td>
<td>{{ $item->brand_name }}</td>
<td class="font-mono text-sm {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">{{ $item->product_sku }}</td>
<td class="{{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">{{ $item->brand_name }}</td>
<td>
<span class="font-semibold">
<span class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">
{{ $item->delivered_qty ?? $item->picked_qty }}
<span class="text-gray-400">/{{ $item->quantity }}</span>
</span>
</td>
<td>${{ number_format($item->unit_price, 2) }}</td>
<td class="font-semibold">
${{ number_format(($item->delivered_qty ?? $item->picked_qty) * $item->unit_price, 2) }}
<td class="{{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">${{ number_format($item->unit_price, 2) }}</td>
<td class="font-semibold {{ $item->isPreDeliveryRejected() ? 'line-through' : '' }}">
${{ number_format($item->line_total, 2) }}
</td>
</tr>
@endforeach
@@ -276,7 +303,7 @@
<!-- Change History -->
@if(isset($invoice->changes) && $invoice->changes->isNotEmpty())
<div class="card bg-base-100 shadow-lg mt-6">
<div class="card bg-base-100 shadow mt-6">
<div class="card-body">
<h2 class="card-title mb-4">
<span class="icon-[lucide--history] size-5"></span>
@@ -322,7 +349,7 @@
@if($invoice->notes)
<!-- Notes Section -->
<div class="card bg-base-100 shadow-lg mt-6">
<div class="card bg-base-100 shadow mt-6">
<div class="card-body">
<h2 class="card-title mb-4">
<span class="icon-[lucide--message-square] size-5"></span>