- Increased icon sizes in service items, service orders, users, and technician management for better visibility. - Added custom loading indicators with appropriate icons in search fields for vehicles, work orders, and technicians. - Introduced invoice management routes for better organization and access control. - Created a new test for the estimate PDF functionality to ensure proper rendering and data integrity.
318 lines
18 KiB
PHP
318 lines
18 KiB
PHP
<div class="space-y-6">
|
|
<!-- Header -->
|
|
<div class="bg-gradient-to-r from-accent/10 to-accent/5 rounded-xl p-6 border border-accent/20 dark:border-accent/30">
|
|
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
|
|
<div class="flex items-center space-x-4">
|
|
<div class="h-16 w-16 bg-gradient-to-br from-accent to-accent-content rounded-xl flex items-center justify-center shadow-lg">
|
|
<svg class="h-8 w-8 text-accent-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
|
Estimate #{{ $estimate->estimate_number }}
|
|
</h1>
|
|
<p class="text-gray-600 dark:text-gray-400 mt-1">
|
|
@if($estimate->jobCard)
|
|
Job Card #{{ $estimate->jobCard->job_number }} • {{ $estimate->created_at->format('M j, Y') }}
|
|
@else
|
|
Standalone Estimate • {{ $estimate->created_at->format('M j, Y') }}
|
|
@endif
|
|
</p>
|
|
<div class="flex items-center space-x-3 mt-2">
|
|
<flux:badge
|
|
:color="match($estimate->status) {
|
|
'draft' => 'zinc',
|
|
'sent' => 'blue',
|
|
'approved' => 'green',
|
|
'rejected' => 'red',
|
|
'expired' => 'orange',
|
|
default => 'zinc'
|
|
}"
|
|
size="sm"
|
|
>
|
|
{{ ucfirst($estimate->status) }}
|
|
</flux:badge>
|
|
<flux:badge
|
|
:color="match($estimate->customer_approval_status) {
|
|
'pending' => 'yellow',
|
|
'approved' => 'green',
|
|
'rejected' => 'red',
|
|
default => 'zinc'
|
|
}"
|
|
size="sm"
|
|
>
|
|
Customer: {{ ucfirst($estimate->customer_approval_status) }}
|
|
</flux:badge>
|
|
@if($estimate->validity_period_days)
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
Valid until {{ $estimate->valid_until->format('M j, Y') }}
|
|
</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex items-center space-x-3 mt-4 lg:mt-0">
|
|
@if($estimate->status === 'draft')
|
|
<flux:button wire:click="sendToCustomer" variant="primary">
|
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
|
|
</svg>
|
|
Send to Customer
|
|
</flux:button>
|
|
@endif
|
|
|
|
<flux:dropdown align="end">
|
|
<flux:button variant="ghost" icon="ellipsis-horizontal" />
|
|
|
|
<flux:menu>
|
|
<flux:menu.item icon="pencil" href="{{ route('estimates.edit', $estimate) }}">
|
|
Edit Estimate
|
|
</flux:menu.item>
|
|
<flux:menu.item icon="document-duplicate" wire:click="duplicateEstimate">
|
|
Duplicate Estimate
|
|
</flux:menu.item>
|
|
<flux:menu.item icon="arrow-down-tray" wire:click="downloadPDF">
|
|
Download PDF
|
|
</flux:menu.item>
|
|
@if($estimate->status === 'approved')
|
|
<flux:menu.item icon="plus" href="{{ route('work-orders.create', $estimate) }}">
|
|
Create Work Order
|
|
</flux:menu.item>
|
|
@endif
|
|
@if($estimate->customer_approval_status === 'pending' && auth()->user()->can('approve', $estimate))
|
|
<flux:menu.separator />
|
|
<flux:menu.item icon="check" wire:click="approveEstimate">
|
|
Approve Estimate
|
|
</flux:menu.item>
|
|
<flux:menu.item icon="x-mark" wire:click="rejectEstimate">
|
|
Reject Estimate
|
|
</flux:menu.item>
|
|
@endif
|
|
</flux:menu>
|
|
</flux:dropdown>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Customer & Vehicle Information -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<!-- Customer Information -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Customer Information</h3>
|
|
@php
|
|
$customer = $estimate->customer ?? $estimate->jobCard?->customer;
|
|
@endphp
|
|
@if($customer)
|
|
<div class="space-y-3">
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Name:</span>
|
|
<p class="text-gray-900 dark:text-gray-100">{{ $customer->name }}</p>
|
|
</div>
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Email:</span>
|
|
<p class="text-gray-900 dark:text-gray-100">{{ $customer->email }}</p>
|
|
</div>
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Phone:</span>
|
|
<p class="text-gray-900 dark:text-gray-100">{{ $customer->phone }}</p>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<p class="text-gray-500 dark:text-gray-400">No customer information available</p>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Vehicle Information -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Vehicle Information</h3>
|
|
@php
|
|
$vehicle = $estimate->vehicle ?? $estimate->jobCard?->vehicle;
|
|
@endphp
|
|
@if($vehicle)
|
|
<div class="space-y-3">
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Vehicle:</span>
|
|
<p class="text-gray-900 dark:text-gray-100">{{ $vehicle->year }} {{ $vehicle->make }} {{ $vehicle->model }}</p>
|
|
</div>
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">License Plate:</span>
|
|
<p class="text-gray-900 dark:text-gray-100">{{ $vehicle->license_plate }}</p>
|
|
</div>
|
|
@if($vehicle->vin)
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">VIN:</span>
|
|
<p class="text-gray-900 dark:text-gray-100 font-mono text-sm">{{ $vehicle->vin }}</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@else
|
|
<p class="text-gray-500 dark:text-gray-400">No vehicle specified</p>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Line Items -->
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
|
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Line Items</h3>
|
|
<flux:button wire:click="$toggle('showItemDetails')" variant="ghost" size="sm">
|
|
{{ $showItemDetails ? 'Hide Details' : 'Show Details' }}
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
@if($estimate->lineItems->count() > 0)
|
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Type</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Description</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Qty</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Unit Price</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
@foreach($estimate->lineItems as $item)
|
|
<tr>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<flux:badge
|
|
:color="match($item->type) {
|
|
'labour' => 'blue',
|
|
'parts' => 'green',
|
|
'miscellaneous' => 'gray',
|
|
default => 'gray'
|
|
}"
|
|
size="sm"
|
|
>
|
|
{{ ucfirst($item->type) }}
|
|
</flux:badge>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $item->description }}</div>
|
|
@if($showItemDetails && $item->part)
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
Part #: {{ $item->part->part_number }}
|
|
</div>
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
{{ $item->quantity }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
${{ number_format($item->unit_price, 2) }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
${{ number_format($item->total_amount, 2) }}
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
@else
|
|
<div class="p-12 text-center">
|
|
<svg class="w-12 h-12 mx-auto text-gray-400 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
</svg>
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">No line items</h3>
|
|
<p class="text-gray-500 dark:text-gray-400">This estimate doesn't have any line items yet.</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary & Details -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Summary -->
|
|
<div class="lg:col-span-1">
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Summary</h3>
|
|
<div class="space-y-3">
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-gray-600 dark:text-gray-400">Subtotal:</span>
|
|
<span class="font-medium text-gray-900 dark:text-gray-100">${{ number_format($estimate->subtotal, 2) }}</span>
|
|
</div>
|
|
@if($estimate->discount_amount > 0)
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-gray-600 dark:text-gray-400">Discount:</span>
|
|
<span class="font-medium text-red-600 dark:text-red-400">-${{ number_format($estimate->discount_amount, 2) }}</span>
|
|
</div>
|
|
@endif
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-gray-600 dark:text-gray-400">Tax ({{ $estimate->tax_rate }}%):</span>
|
|
<span class="font-medium text-gray-900 dark:text-gray-100">${{ number_format($estimate->tax_amount, 2) }}</span>
|
|
</div>
|
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-3">
|
|
<div class="flex justify-between">
|
|
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">Total:</span>
|
|
<span class="text-lg font-bold text-accent">${{ number_format($estimate->total_amount, 2) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notes & Terms -->
|
|
<div class="lg:col-span-2 space-y-6">
|
|
@if($estimate->notes)
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Customer Notes</h3>
|
|
<p class="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{ $estimate->notes }}</p>
|
|
</div>
|
|
@endif
|
|
|
|
@if($estimate->internal_notes && auth()->user()->can('view', $estimate))
|
|
<div class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800 p-6">
|
|
<h3 class="text-lg font-semibold text-yellow-800 dark:text-yellow-200 mb-4">Internal Notes</h3>
|
|
<p class="text-yellow-700 dark:text-yellow-300 whitespace-pre-wrap">{{ $estimate->internal_notes }}</p>
|
|
</div>
|
|
@endif
|
|
|
|
@if($estimate->terms_and_conditions)
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Terms & Conditions</h3>
|
|
<p class="text-gray-700 dark:text-gray-300 whitespace-pre-wrap text-sm">{{ $estimate->terms_and_conditions }}</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audit Trail -->
|
|
@if($estimate->created_at)
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Estimate History</h3>
|
|
<div class="space-y-3 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-600 dark:text-gray-400">Created:</span>
|
|
<span class="text-gray-900 dark:text-gray-100">{{ $estimate->created_at->format('M j, Y g:i A') }}</span>
|
|
</div>
|
|
@if($estimate->sent_at)
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-600 dark:text-gray-400">Sent to Customer:</span>
|
|
<span class="text-gray-900 dark:text-gray-100">{{ $estimate->sent_at->format('M j, Y g:i A') }}</span>
|
|
</div>
|
|
@endif
|
|
@if($estimate->customer_approved_at)
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-600 dark:text-gray-400">Customer Response:</span>
|
|
<span class="text-gray-900 dark:text-gray-100">
|
|
{{ ucfirst($estimate->customer_approval_status) }} on {{ $estimate->customer_approved_at->format('M j, Y g:i A') }}
|
|
</span>
|
|
</div>
|
|
@endif
|
|
@if($estimate->preparedBy)
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-600 dark:text-gray-400">Prepared By:</span>
|
|
<span class="text-gray-900 dark:text-gray-100">{{ $estimate->preparedBy->name }}</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|