- 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.
396 lines
24 KiB
PHP
396 lines
24 KiB
PHP
<div class="space-y-6">
|
||
<!-- Enhanced Header -->
|
||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||
<div>
|
||
<h1 class="text-3xl font-bold text-zinc-900 dark:text-white dark:text-white">Parts Catalog</h1>
|
||
<p class="mt-2 text-lg text-zinc-600 dark:text-zinc-400 dark:text-gray-300">
|
||
Manage your inventory with {{ number_format($parts->total()) }} total parts
|
||
</p>
|
||
</div>
|
||
<div class="flex flex-col sm:flex-row gap-3">
|
||
<flux:button wire:navigate href="{{ route('inventory.stock-movements.create') }}" variant="outline" icon="clipboard-document-list" size="sm">
|
||
Record Movement
|
||
</flux:button>
|
||
<flux:button wire:navigate href="{{ route('inventory.parts.create') }}" variant="primary" icon="plus" size="sm">
|
||
Add New Part
|
||
</flux:button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Enhanced Filters with Better Layout -->
|
||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 border border-zinc-200 dark:border-zinc-700">
|
||
<div class="p-6">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h3 class="text-lg font-semibold text-zinc-900 dark:text-white dark:text-white">Filter & Search</h3>
|
||
<flux:button wire:click="clearFilters" variant="outline" size="xs">
|
||
Clear All
|
||
</flux:button>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 gap-4">
|
||
<!-- Enhanced Search -->
|
||
<div class="lg:col-span-2">
|
||
<flux:field>
|
||
<flux:label>Search Parts</flux:label>
|
||
<flux:input
|
||
wire:model.live.debounce.300ms="search"
|
||
placeholder="Part name, number, or description..."
|
||
icon="magnifying-glass"
|
||
/>
|
||
</flux:field>
|
||
</div>
|
||
|
||
<!-- Category Filter with Flux Select -->
|
||
<div>
|
||
<flux:field>
|
||
<flux:label>Category</flux:label>
|
||
<flux:select wire:model.live="categoryFilter">
|
||
<option value="">All Categories</option>
|
||
@foreach($categories as $category)
|
||
<option value="{{ $category }}">{{ ucfirst(str_replace('_', ' ', $category)) }}</option>
|
||
@endforeach
|
||
</flux:select>
|
||
</flux:field>
|
||
</div>
|
||
|
||
<!-- Stock Status Filter -->
|
||
<div>
|
||
<flux:field>
|
||
<flux:label>Stock Status</flux:label>
|
||
<flux:select wire:model.live="stockFilter">
|
||
<option value="">All Stock Levels</option>
|
||
<option value="in_stock">✅ In Stock</option>
|
||
<option value="low_stock">⚠️ Low Stock</option>
|
||
<option value="out_of_stock">❌ Out of Stock</option>
|
||
<option value="overstock">📈 Overstock</option>
|
||
</flux:select>
|
||
</flux:field>
|
||
</div>
|
||
|
||
<!-- Supplier Filter -->
|
||
<div>
|
||
<flux:field>
|
||
<flux:label>Supplier</flux:label>
|
||
<flux:select wire:model.live="supplierFilter">
|
||
<option value="">All Suppliers</option>
|
||
@foreach($suppliers as $supplier)
|
||
<option value="{{ $supplier->id }}">{{ $supplier->name }}</option>
|
||
@endforeach
|
||
</flux:select>
|
||
</flux:field>
|
||
</div>
|
||
|
||
<!-- Sort Options -->
|
||
<div>
|
||
<flux:field>
|
||
<flux:label>Sort By</flux:label>
|
||
<flux:select wire:model.live="sortField">
|
||
<option value="name">Name A-Z</option>
|
||
<option value="part_number">Part Number</option>
|
||
<option value="quantity_on_hand">Stock Level</option>
|
||
<option value="cost_price">Cost Price</option>
|
||
<option value="created_at">Date Added</option>
|
||
</flux:select>
|
||
</flux:field>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Active Filters Display -->
|
||
<div class="mt-4 flex flex-wrap gap-2">
|
||
@if($search)
|
||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||
Search: "{{ $search }}"
|
||
<button wire:click="$set('search', '')" class="ml-2 text-blue-600 hover:text-blue-800">×</button>
|
||
</span>
|
||
@endif
|
||
@if($categoryFilter)
|
||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||
Category: {{ ucfirst(str_replace('_', ' ', $categoryFilter)) }}
|
||
<button wire:click="$set('categoryFilter', '')" class="ml-2 text-green-600 hover:text-green-800">×</button>
|
||
</span>
|
||
@endif
|
||
@if($stockFilter)
|
||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200">
|
||
Stock: {{ ucfirst(str_replace('_', ' ', $stockFilter)) }}
|
||
<button wire:click="$set('stockFilter', '')" class="ml-2 text-orange-600 hover:text-orange-800">×</button>
|
||
</span>
|
||
@endif
|
||
@if($supplierFilter)
|
||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
|
||
Supplier: {{ $suppliers->firstWhere('id', $supplierFilter)?->name ?? 'Unknown' }}
|
||
<button wire:click="$set('supplierFilter', '')" class="ml-2 text-purple-600 hover:text-purple-800">×</button>
|
||
</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Results Summary -->
|
||
<div class="flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400 dark:text-gray-400">
|
||
<div>
|
||
Showing {{ $parts->firstItem() ?? 0 }} to {{ $parts->lastItem() ?? 0 }} of {{ number_format($parts->total()) }} parts
|
||
</div>
|
||
<div class="flex items-center space-x-2">
|
||
<span>Per page:</span>
|
||
<flux:select wire:model.live="perPage" class="w-20">
|
||
<option value="10">10</option>
|
||
<option value="25">25</option>
|
||
<option value="50">50</option>
|
||
<option value="100">100</option>
|
||
</flux:select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Enhanced Parts Table -->
|
||
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 overflow-hidden border border-zinc-200 dark:border-zinc-700">
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700">
|
||
<thead class="bg-gradient-to-r from-gray-50 to-gray-100 dark:from-zinc-900 dark:to-zinc-800">
|
||
<tr>
|
||
<th wire:click="sortBy('part_number')" class="px-6 py-4 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-700 transition-colors">
|
||
<div class="flex items-center space-x-1">
|
||
<span>Part #</span>
|
||
@if($sortBy === 'part_number')
|
||
<flux:icon.chevron-up class="w-6 h-6 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
|
||
@endif
|
||
</div>
|
||
</th>
|
||
<th wire:click="sortBy('name')" class="px-6 py-4 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-700 transition-colors">
|
||
<div class="flex items-center space-x-1">
|
||
<span>Part Details</span>
|
||
@if($sortBy === 'name')
|
||
<flux:icon.chevron-up class="w-6 h-6 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
|
||
@endif
|
||
</div>
|
||
</th>
|
||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">Supplier</th>
|
||
<th wire:click="sortBy('quantity_on_hand')" class="px-6 py-4 text-center text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-700 transition-colors">
|
||
<div class="flex items-center justify-center space-x-1">
|
||
<span>Stock Level</span>
|
||
@if($sortBy === 'quantity_on_hand')
|
||
<flux:icon.chevron-up class="w-6 h-6 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
|
||
@endif
|
||
</div>
|
||
</th>
|
||
<th wire:click="sortBy('cost_price')" class="px-6 py-4 text-right text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-zinc-700 transition-colors">
|
||
<div class="flex items-center justify-end space-x-1">
|
||
<span>Pricing</span>
|
||
@if($sortBy === 'cost_price')
|
||
<flux:icon.chevron-up class="w-6 h-6 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
|
||
@endif
|
||
</div>
|
||
</th>
|
||
<th class="px-6 py-4 text-center text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">Status</th>
|
||
<th class="px-6 py-4 text-right text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="bg-white dark:bg-zinc-800 divide-y divide-zinc-200 dark:divide-zinc-700">
|
||
@forelse($parts as $part)
|
||
<tr class="hover:bg-zinc-50 dark:hover:bg-zinc-700/50 transition-colors">
|
||
<!-- Part Number -->
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
<div class="text-sm font-bold text-zinc-900 dark:text-white dark:text-white">
|
||
{{ $part->part_number }}
|
||
</div>
|
||
@if($part->barcode)
|
||
<div class="text-xs text-zinc-500 dark:text-zinc-400 dark:text-gray-400 font-mono">
|
||
{{ $part->barcode }}
|
||
</div>
|
||
@endif
|
||
</td>
|
||
|
||
<!-- Part Details -->
|
||
<td class="px-6 py-4">
|
||
<div class="flex items-start space-x-3">
|
||
<div class="flex-1 min-w-0">
|
||
<div class="text-sm font-semibold text-zinc-900 dark:text-white dark:text-white truncate">
|
||
{{ $part->name }}
|
||
</div>
|
||
@if($part->description)
|
||
<div class="text-xs text-zinc-500 dark:text-zinc-400 dark:text-gray-400 line-clamp-2">
|
||
{{ Str::limit($part->description, 100) }}
|
||
</div>
|
||
@endif
|
||
<div class="flex items-center space-x-2 mt-1">
|
||
@if($part->category)
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||
{{ ucfirst(str_replace('_', ' ', $part->category)) }}
|
||
</span>
|
||
@endif
|
||
@if($part->manufacturer)
|
||
<span class="text-xs text-zinc-500 dark:text-zinc-400 dark:text-gray-400">
|
||
by {{ $part->manufacturer }}
|
||
</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
|
||
<!-- Supplier -->
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
@if($part->supplier)
|
||
<div class="text-sm text-zinc-900 dark:text-white dark:text-white">
|
||
{{ $part->supplier->name }}
|
||
</div>
|
||
@if($part->supplier->contact_email)
|
||
<div class="text-xs text-zinc-500 dark:text-zinc-400 dark:text-gray-400">
|
||
{{ $part->supplier->contact_email }}
|
||
</div>
|
||
@endif
|
||
@else
|
||
<span class="text-sm text-gray-400 dark:text-zinc-500 dark:text-zinc-400">No supplier</span>
|
||
@endif
|
||
</td>
|
||
|
||
<!-- Stock Level -->
|
||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||
<div class="flex flex-col items-center">
|
||
<span class="text-lg font-bold
|
||
@if($part->quantity_on_hand <= 0) text-red-600 dark:text-red-400
|
||
@elseif($part->quantity_on_hand <= $part->minimum_stock_level) text-orange-600 dark:text-orange-400
|
||
@else text-green-600 dark:text-green-400 @endif">
|
||
{{ number_format($part->quantity_on_hand) }}
|
||
</span>
|
||
@if($part->minimum_stock_level)
|
||
<span class="text-xs text-zinc-500 dark:text-zinc-400 dark:text-gray-400">
|
||
Min: {{ $part->minimum_stock_level }}
|
||
</span>
|
||
@endif
|
||
@if($part->unit_of_measurement)
|
||
<span class="text-xs text-zinc-500 dark:text-zinc-400 dark:text-gray-400">
|
||
{{ $part->unit_of_measurement }}
|
||
</span>
|
||
@endif
|
||
</div>
|
||
</td>
|
||
|
||
<!-- Pricing -->
|
||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||
<div class="text-sm">
|
||
<div class="text-zinc-900 dark:text-white dark:text-white font-semibold">
|
||
${{ number_format($part->sell_price, 2) }}
|
||
</div>
|
||
<div class="text-zinc-500 dark:text-zinc-400 dark:text-gray-400">
|
||
Cost: ${{ number_format($part->cost_price, 2) }}
|
||
</div>
|
||
@if($part->sell_price > $part->cost_price)
|
||
<div class="text-xs text-green-600 dark:text-green-400">
|
||
{{ number_format((($part->sell_price - $part->cost_price) / $part->cost_price) * 100, 1) }}% margin
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</td>
|
||
|
||
<!-- Status -->
|
||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||
@if($part->quantity_on_hand <= 0)
|
||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
|
||
Out of Stock
|
||
</span>
|
||
@elseif($part->quantity_on_hand <= $part->minimum_stock_level)
|
||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200">
|
||
Low Stock
|
||
</span>
|
||
@elseif($part->quantity_on_hand > ($part->maximum_stock_level ?? 1000))
|
||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||
Overstock
|
||
</span>
|
||
@else
|
||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||
In Stock
|
||
</span>
|
||
@endif
|
||
</td>
|
||
|
||
<!-- Actions -->
|
||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||
<div class="flex items-center justify-end space-x-2">
|
||
<flux:button
|
||
wire:navigate
|
||
href="{{ route('inventory.parts.show', $part) }}"
|
||
variant="ghost"
|
||
size="xs"
|
||
icon="eye"
|
||
>
|
||
View
|
||
</flux:button>
|
||
<flux:button
|
||
wire:navigate
|
||
href="{{ route('inventory.parts.edit', $part) }}"
|
||
variant="ghost"
|
||
size="xs"
|
||
icon="pencil"
|
||
>
|
||
Edit
|
||
</flux:button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
@empty
|
||
</div>
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-900 dark:text-white dark:text-white">
|
||
@if($part->category)
|
||
<flux:badge size="sm" color="gray">{{ ucfirst($part->category) }}</flux:badge>
|
||
@endif
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-900 dark:text-white dark:text-white">
|
||
{{ $part->supplier?->full_name ?? 'No Supplier' }}
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap">
|
||
<div class="flex items-center space-x-2">
|
||
<span class="text-sm font-medium text-zinc-900 dark:text-white dark:text-white">{{ number_format($part->quantity_on_hand) }}</span>
|
||
<span class="text-xs {{ $part->stock_status_color }}">
|
||
{{ ucfirst(str_replace('_', ' ', $part->stock_status)) }}
|
||
</span>
|
||
</div>
|
||
<div class="text-xs text-zinc-500 dark:text-zinc-400 dark:text-gray-400">
|
||
Min: {{ $part->minimum_stock_level }}
|
||
<tr>
|
||
<td colspan="7" class="px-6 py-16 text-center">
|
||
<div class="text-zinc-500 dark:text-zinc-400 dark:text-gray-400">
|
||
<flux:icon.cube-transparent class="mx-auto h-16 w-16 mb-6 opacity-40" />
|
||
<h3 class="text-lg font-semibold text-zinc-900 dark:text-white dark:text-white mb-2">No parts found</h3>
|
||
<p class="text-zinc-600 dark:text-zinc-400 dark:text-gray-400 mb-6">
|
||
@if($search || $categoryFilter || $stockFilter || $supplierFilter)
|
||
No parts match your current filters. Try adjusting your search criteria.
|
||
@else
|
||
Get started by adding your first part to the catalog.
|
||
@endif
|
||
</p>
|
||
<div class="flex items-center justify-center space-x-3">
|
||
@if($search || $categoryFilter || $stockFilter || $supplierFilter)
|
||
<flux:button wire:click="clearFilters" variant="outline" size="sm">
|
||
Clear Filters
|
||
</flux:button>
|
||
@endif
|
||
<flux:button wire:navigate href="{{ route('inventory.parts.create') }}" variant="primary" size="sm">
|
||
Add First Part
|
||
</flux:button>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Enhanced Pagination -->
|
||
@if($parts->hasPages())
|
||
<div class="bg-zinc-50 dark:bg-zinc-900 px-6 py-4 border-t border-zinc-200 dark:border-zinc-700">
|
||
<div class="flex items-center justify-between">
|
||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
||
Showing {{ $parts->firstItem() }} to {{ $parts->lastItem() }} of {{ number_format($parts->total()) }} results
|
||
</div>
|
||
<div>
|
||
{{ $parts->links() }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|