- 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.
289 lines
16 KiB
PHP
289 lines
16 KiB
PHP
<div class="space-y-6">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between">
|
|
<flux:heading size="xl">Service Order Management</flux:heading>
|
|
<flux:button href="/service-orders/create" size="sm">
|
|
<flux:icon name="plus" class="size-4" />
|
|
Create New Service Order
|
|
</flux:button>
|
|
</div>
|
|
|
|
<!-- Flash Messages -->
|
|
@if (session()->has('success'))
|
|
<div class="bg-green-50 border border-green-200 text-green-800 rounded-md p-4">
|
|
<div class="flex">
|
|
<flux:icon name="check-circle" class="h-5 w-5 text-green-400" />
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium">{{ session('success') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if (session()->has('error'))
|
|
<div class="bg-red-50 border border-red-200 text-red-800 rounded-md p-4">
|
|
<div class="flex">
|
|
<flux:icon name="exclamation-circle" class="h-5 w-5 text-red-400" />
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium">{{ session('error') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Quick Stats -->
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 p-4">
|
|
<div class="text-2xl font-bold text-blue-600">{{ $stats['total'] }}</div>
|
|
<div class="text-sm text-zinc-600 dark:text-zinc-400">Total Orders</div>
|
|
</div>
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 p-4">
|
|
<div class="text-2xl font-bold text-yellow-600">{{ $stats['pending'] }}</div>
|
|
<div class="text-sm text-zinc-600 dark:text-zinc-400">Pending</div>
|
|
</div>
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 p-4">
|
|
<div class="text-2xl font-bold text-orange-600">{{ $stats['in_progress'] }}</div>
|
|
<div class="text-sm text-zinc-600 dark:text-zinc-400">In Progress</div>
|
|
</div>
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 p-4">
|
|
<div class="text-2xl font-bold text-green-600">{{ $stats['completed_today'] }}</div>
|
|
<div class="text-sm text-zinc-600 dark:text-zinc-400">Completed Today</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<div class="grid grid-cols-1 md:grid-cols-6 gap-4 p-4">
|
|
<div class="relative">
|
|
<flux:input
|
|
wire:model.live="search"
|
|
placeholder="Search orders..."
|
|
icon="magnifying-glass"
|
|
:loading="false"
|
|
/>
|
|
<!-- Custom loading indicator with wrench icon -->
|
|
<div wire:loading wire:target="search" class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
|
<flux:icon.wrench class="w-6 h-6 text-zinc-400 animate-spin" />
|
|
</div>
|
|
</div>
|
|
|
|
<select wire:model.live="status" class="rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-2 focus:ring-blue-500">
|
|
<option value="">All Statuses</option>
|
|
<option value="pending">Pending</option>
|
|
<option value="in_progress">In Progress</option>
|
|
<option value="completed">Completed</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
<option value="on_hold">On Hold</option>
|
|
</select>
|
|
|
|
<select wire:model.live="priority" class="rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-2 focus:ring-blue-500">
|
|
<option value="">All Priorities</option>
|
|
<option value="low">Low</option>
|
|
<option value="normal">Normal</option>
|
|
<option value="high">High</option>
|
|
<option value="urgent">Urgent</option>
|
|
</select>
|
|
|
|
<select wire:model.live="technician_id" class="rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-2 focus:ring-blue-500">
|
|
<option value="">All Technicians</option>
|
|
@foreach($technicians as $technician)
|
|
<option value="{{ $technician->id }}">{{ $technician->full_name }}</option>
|
|
@endforeach
|
|
</select>
|
|
|
|
<input
|
|
type="date"
|
|
wire:model.live="date_from"
|
|
class="rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-2 focus:ring-blue-500"
|
|
placeholder="From Date"
|
|
/>
|
|
|
|
<input
|
|
type="date"
|
|
wire:model.live="date_to"
|
|
class="rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-2 focus:ring-blue-500"
|
|
placeholder="To Date"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Service Orders Table -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full">
|
|
<thead>
|
|
<tr class="border-b border-zinc-200 dark:border-zinc-700">
|
|
<th class="text-left py-3 px-4">
|
|
<button wire:click="sortBy('order_number')" class="flex items-center space-x-1 hover:text-blue-600">
|
|
<span>Order #</span>
|
|
@if($sortBy === 'order_number')
|
|
<flux:icon name="{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="size-3" />
|
|
@endif
|
|
</button>
|
|
</th>
|
|
<th class="text-left py-3 px-4">Customer & Vehicle</th>
|
|
<th class="text-left py-3 px-4">Complaint</th>
|
|
<th class="text-left py-3 px-4">
|
|
<button wire:click="sortBy('assigned_technician_id')" class="flex items-center space-x-1 hover:text-blue-600">
|
|
<span>Technician</span>
|
|
@if($sortBy === 'assigned_technician_id')
|
|
<flux:icon name="{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="size-3" />
|
|
@endif
|
|
</button>
|
|
</th>
|
|
<th class="text-left py-3 px-4">
|
|
<button wire:click="sortBy('priority')" class="flex items-center space-x-1 hover:text-blue-600">
|
|
<span>Priority</span>
|
|
@if($sortBy === 'priority')
|
|
<flux:icon name="{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="size-3" />
|
|
@endif
|
|
</button>
|
|
</th>
|
|
<th class="text-left py-3 px-4">
|
|
<button wire:click="sortBy('status')" class="flex items-center space-x-1 hover:text-blue-600">
|
|
<span>Status</span>
|
|
@if($sortBy === 'status')
|
|
<flux:icon name="{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="size-3" />
|
|
@endif
|
|
</button>
|
|
</th>
|
|
<th class="text-left py-3 px-4">
|
|
<button wire:click="sortBy('total_amount')" class="flex items-center space-x-1 hover:text-blue-600">
|
|
<span>Total</span>
|
|
@if($sortBy === 'total_amount')
|
|
<flux:icon name="{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="size-3" />
|
|
@endif
|
|
</button>
|
|
</th>
|
|
<th class="text-left py-3 px-4">
|
|
<button wire:click="sortBy('created_at')" class="flex items-center space-x-1 hover:text-blue-600">
|
|
<span>Created</span>
|
|
@if($sortBy === 'created_at')
|
|
<flux:icon name="{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="size-3" />
|
|
@endif
|
|
</button>
|
|
</th>
|
|
<th class="text-left py-3 px-4">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse($serviceOrders as $order)
|
|
<tr class="border-b border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-700">
|
|
<td class="py-3 px-4">
|
|
<div class="font-medium">{{ $order->order_number }}</div>
|
|
@if($order->scheduled_date)
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400">Scheduled: {{ $order->scheduled_date->format('M j, Y') }}</div>
|
|
@endif
|
|
</td>
|
|
<td class="py-3 px-4">
|
|
<div>
|
|
<div class="font-medium text-sm">{{ $order->customer->full_name }}</div>
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400">{{ $order->vehicle->display_name }}</div>
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400">{{ $order->vehicle->license_plate }}</div>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4">
|
|
<div class="text-sm max-w-xs truncate" title="{{ $order->customer_complaint }}">
|
|
{{ $order->customer_complaint }}
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4">
|
|
<div class="text-sm">
|
|
{{ $order->assignedTechnician?->full_name ?? 'Unassigned' }}
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4">
|
|
<flux:badge
|
|
:color="$order->priority === 'urgent' ? 'red' : ($order->priority === 'high' ? 'orange' : ($order->priority === 'normal' ? 'blue' : 'gray'))"
|
|
size="sm"
|
|
>
|
|
{{ ucfirst($order->priority) }}
|
|
</flux:badge>
|
|
</td>
|
|
<td class="py-3 px-4">
|
|
<div class="flex items-center space-x-2">
|
|
<flux:badge
|
|
:color="$order->status === 'completed' ? 'green' : ($order->status === 'in_progress' ? 'blue' : ($order->status === 'cancelled' ? 'red' : 'gray'))"
|
|
size="sm"
|
|
>
|
|
{{ ucfirst(str_replace('_', ' ', $order->status)) }}
|
|
</flux:badge>
|
|
|
|
@if($order->status === 'pending')
|
|
<button
|
|
wire:click="updateStatus({{ $order->id }}, 'in_progress')"
|
|
class="text-xs text-blue-600 hover:underline"
|
|
title="Start Work"
|
|
>
|
|
Start
|
|
</button>
|
|
@elseif($order->status === 'in_progress')
|
|
<button
|
|
wire:click="updateStatus({{ $order->id }}, 'completed')"
|
|
class="text-xs text-green-600 hover:underline"
|
|
title="Complete Work"
|
|
>
|
|
Complete
|
|
</button>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4">
|
|
<div class="font-medium">${{ number_format($order->total_amount, 2) }}</div>
|
|
@if($order->serviceItems->count() > 0 || $order->parts->count() > 0)
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400">
|
|
{{ $order->serviceItems->count() }} services, {{ $order->parts->count() }} parts
|
|
</div>
|
|
@endif
|
|
</td>
|
|
<td class="py-3 px-4">
|
|
<div class="text-sm">{{ $order->created_at->format('M j, Y') }}</div>
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400">{{ $order->created_at->format('g:i A') }}</div>
|
|
</td>
|
|
<td class="py-3 px-4">
|
|
<div class="flex space-x-1">
|
|
<flux:button href="/service-orders/{{ $order->id }}" variant="outline" size="sm">
|
|
View
|
|
</flux:button>
|
|
<flux:button href="/service-orders/{{ $order->id }}/edit" variant="outline" size="sm">
|
|
Edit
|
|
</flux:button>
|
|
@if($order->status === 'completed')
|
|
<flux:button href="/service-orders/{{ $order->id }}/invoice" size="sm">
|
|
Invoice
|
|
</flux:button>
|
|
@endif
|
|
<flux:button
|
|
wire:click="deleteServiceOrder({{ $order->id }})"
|
|
wire:confirm="Are you sure you want to delete service order {{ $order->order_number }}? This action cannot be undone."
|
|
variant="danger"
|
|
size="sm"
|
|
>
|
|
Delete
|
|
</flux:button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="9" class="text-center py-8 text-zinc-500 dark:text-zinc-400">
|
|
@if($search)
|
|
No service orders found matching "{{ $search }}"
|
|
@else
|
|
No service orders found. <a href="/service-orders/create" class="text-blue-600 hover:underline">Create your first service order</a>
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
@if($serviceOrders->hasPages())
|
|
<div class="mt-4 px-4 pb-4">
|
|
{{ $serviceOrders->links() }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|