- 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.
309 lines
18 KiB
PHP
309 lines
18 KiB
PHP
<div class="space-y-6">
|
|
<!-- Header with Welcome Message -->
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-zinc-900 dark:text-white dark:text-white">Inventory Dashboard</h1>
|
|
<p class="mt-2 text-lg text-zinc-600 dark:text-zinc-400 dark:text-gray-300">
|
|
Welcome back! Here's your inventory overview for {{ now()->format('F j, Y') }}
|
|
</p>
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<flux:button wire:navigate href="{{ route('inventory.parts.index') }}" variant="outline" icon="cube" size="sm">
|
|
View Parts
|
|
</flux:button>
|
|
<flux:button wire:navigate href="{{ route('inventory.suppliers.index') }}" variant="outline" icon="building-office" size="sm">
|
|
Suppliers
|
|
</flux:button>
|
|
<flux:button wire:navigate href="{{ route('inventory.purchase-orders.index') }}" variant="primary" icon="shopping-cart" size="sm">
|
|
New Order
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Alert Banner for Critical Issues -->
|
|
@if($outOfStockParts > 0 || $lowStockParts > 5)
|
|
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<flux:icon.exclamation-triangle class="w-6 h-6 text-red-600 dark:text-red-400 mr-3" />
|
|
<div>
|
|
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">
|
|
Attention Required
|
|
</h3>
|
|
<p class="text-sm text-red-700 dark:text-red-300 mt-1">
|
|
@if($outOfStockParts > 0)
|
|
{{ $outOfStockParts }} parts are out of stock.
|
|
@endif
|
|
@if($lowStockParts > 5)
|
|
{{ $lowStockParts }} parts are running low.
|
|
@endif
|
|
Consider creating purchase orders to restock.
|
|
</p>
|
|
</div>
|
|
<div class="ml-auto">
|
|
<flux:button wire:navigate href="{{ route('inventory.parts.index', ['stockFilter' => 'out_of_stock']) }}" size="xs" variant="outline">
|
|
View Issues
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Key Metrics with Enhanced Design -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<!-- Total Parts -->
|
|
<div class="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-lg border border-blue-200 dark:border-blue-700 p-6 hover:border border-zinc-200 dark:border-zinc-700 transition-shadow">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="p-3 bg-blue-500 rounded-lg mb-4">
|
|
<flux:icon.cube class="w-6 h-6 text-white" />
|
|
</div>
|
|
<p class="text-sm font-medium text-blue-800 dark:text-blue-200">Total Parts</p>
|
|
<p class="text-3xl font-bold text-blue-900 dark:text-blue-100">{{ number_format($totalParts) }}</p>
|
|
</div>
|
|
<flux:button wire:navigate href="{{ route('inventory.parts.index') }}" size="xs" variant="subtle">
|
|
View All
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Low Stock Alert -->
|
|
<div class="bg-gradient-to-br from-orange-50 to-orange-100 dark:from-orange-900/20 dark:to-orange-800/20 rounded-lg border border-orange-200 dark:border-orange-700 p-6 hover:border border-zinc-200 dark:border-zinc-700 transition-shadow">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="p-3 bg-orange-500 rounded-lg mb-4">
|
|
<flux:icon.exclamation-triangle class="w-6 h-6 text-white" />
|
|
</div>
|
|
<p class="text-sm font-medium text-orange-800 dark:text-orange-200">Low Stock Items</p>
|
|
<p class="text-3xl font-bold text-orange-900 dark:text-orange-100">{{ number_format($lowStockParts) }}</p>
|
|
<p class="text-xs text-orange-600 dark:text-orange-300 mt-1">Needs attention</p>
|
|
</div>
|
|
<flux:button wire:navigate href="{{ route('inventory.parts.index', ['stockFilter' => 'low_stock']) }}" size="xs" variant="subtle">
|
|
Review
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Out of Stock -->
|
|
<div class="bg-gradient-to-br from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/20 rounded-lg border border-red-200 dark:border-red-700 p-6 hover:border border-zinc-200 dark:border-zinc-700 transition-shadow">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="p-3 bg-red-500 rounded-lg mb-4">
|
|
<flux:icon.x-circle class="w-6 h-6 text-white" />
|
|
</div>
|
|
<p class="text-sm font-medium text-red-800 dark:text-red-200">Out of Stock</p>
|
|
<p class="text-3xl font-bold text-red-900 dark:text-red-100">{{ number_format($outOfStockParts) }}</p>
|
|
<p class="text-xs text-red-600 dark:text-red-300 mt-1">{{ $outOfStockParts > 0 ? 'Urgent action needed' : 'All stocked' }}</p>
|
|
</div>
|
|
<flux:button wire:navigate href="{{ route('inventory.parts.index', ['stockFilter' => 'out_of_stock']) }}" size="xs" variant="subtle">
|
|
Fix Now
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Total Value -->
|
|
<div class="bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 rounded-lg border border-green-200 dark:border-green-700 p-6 hover:border border-zinc-200 dark:border-zinc-700 transition-shadow">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<div class="p-3 bg-green-500 rounded-lg mb-4">
|
|
<flux:icon.currency-dollar class="w-6 h-6 text-white" />
|
|
</div>
|
|
<p class="text-sm font-medium text-green-800 dark:text-green-200">Inventory Value</p>
|
|
<p class="text-3xl font-bold text-green-900 dark:text-green-100">${{ number_format($totalStockValue, 0) }}</p>
|
|
<p class="text-xs text-green-600 dark:text-green-300 mt-1">Total asset value</p>
|
|
</div>
|
|
<flux:button wire:navigate href="{{ route('inventory.parts.index') }}" size="xs" variant="subtle">
|
|
Details
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Low Stock Parts -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<div class="p-6 border-b border-zinc-200 dark:border-zinc-700">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-lg font-medium text-zinc-900 dark:text-white dark:text-white">Low Stock Alert</h2>
|
|
<flux:button wire:navigate href="{{ route('inventory.parts.index', ['stockFilter' => 'low_stock']) }}" size="sm" variant="subtle">
|
|
View All
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
<div class="p-6">
|
|
@if($lowStockPartsList->count() > 0)
|
|
<div class="space-y-3">
|
|
@foreach($lowStockPartsList as $part)
|
|
<div class="flex items-center justify-between p-3 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
|
|
<div>
|
|
<p class="font-medium text-zinc-900 dark:text-white dark:text-white">{{ $part->name }}</p>
|
|
<p class="text-sm text-zinc-500 dark:text-zinc-400 dark:text-gray-400">{{ $part->part_number }}</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="text-sm font-medium text-orange-600 dark:text-orange-400">{{ $part->quantity_on_hand }} left</p>
|
|
<p class="text-xs text-zinc-500 dark:text-zinc-400 dark:text-gray-400">Min: {{ $part->minimum_stock_level }}</p>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<div class="text-center py-8">
|
|
<flux:icon.check-circle class="mx-auto h-12 w-12 text-green-400 mb-4" />
|
|
<p class="text-zinc-500 dark:text-zinc-400 dark:text-gray-400">All parts are adequately stocked</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Stock Movements -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<div class="p-6 border-b border-zinc-200 dark:border-zinc-700">
|
|
<h2 class="text-lg font-medium text-zinc-900 dark:text-white dark:text-white">Recent Stock Movements</h2>
|
|
</div>
|
|
<div class="p-6">
|
|
@if($recentMovements->count() > 0)
|
|
<div class="space-y-3">
|
|
@foreach($recentMovements as $movement)
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-3">
|
|
<div class="flex-shrink-0">
|
|
@if($movement->movement_type === 'in')
|
|
<div class="p-1 bg-green-100 dark:bg-green-900 rounded">
|
|
<flux:icon.arrow-down class="w-6 h-6 text-green-600 dark:text-green-400" />
|
|
</div>
|
|
@else
|
|
<div class="p-1 bg-red-100 dark:bg-red-900 rounded">
|
|
<flux:icon.arrow-up class="w-6 h-6 text-red-600 dark:text-red-400" />
|
|
</div>
|
|
@endif
|
|
</div>
|
|
<div>
|
|
<p class="text-sm font-medium text-zinc-900 dark:text-white dark:text-white">{{ $movement->part->name }}</p>
|
|
<p class="text-xs text-zinc-500 dark:text-zinc-400 dark:text-gray-400">{{ $movement->created_at->diffForHumans() }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="text-sm font-medium {{ $movement->movement_type_color }}">
|
|
{{ $movement->formatted_quantity }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<div class="text-center py-8">
|
|
<flux:icon.clock class="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
|
<p class="text-zinc-500 dark:text-zinc-400 dark:text-gray-400">No recent movements</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Stock by Category -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<div class="p-6 border-b border-zinc-200 dark:border-zinc-700">
|
|
<h2 class="text-lg font-medium text-zinc-900 dark:text-white dark:text-white">Stock Value by Category</h2>
|
|
</div>
|
|
<div class="p-6">
|
|
@if($stockByCategory->count() > 0)
|
|
<div class="space-y-3">
|
|
@foreach($stockByCategory as $category)
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="font-medium text-zinc-900 dark:text-white dark:text-white">{{ ucfirst($category->category ?: 'Uncategorized') }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm font-medium text-zinc-900 dark:text-white dark:text-white">${{ number_format($category->total_value, 2) }}</p>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<div class="text-center py-8">
|
|
<flux:icon.chart-bar class="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
|
<p class="text-zinc-500 dark:text-zinc-400 dark:text-gray-400">No categorized inventory</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Suppliers -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<div class="p-6 border-b border-zinc-200 dark:border-zinc-700">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-lg font-medium text-zinc-900 dark:text-white dark:text-white">Top Suppliers</h2>
|
|
<flux:button wire:navigate href="{{ route('inventory.suppliers.index') }}" size="sm" variant="subtle">
|
|
View All
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
<div class="p-6">
|
|
@if($topSuppliers->count() > 0)
|
|
<div class="space-y-3">
|
|
@foreach($topSuppliers as $supplier)
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="font-medium text-zinc-900 dark:text-white dark:text-white">{{ $supplier->full_name }}</p>
|
|
@if($supplier->email)
|
|
<p class="text-xs text-zinc-500 dark:text-zinc-400 dark:text-gray-400">{{ $supplier->email }}</p>
|
|
@endif
|
|
</div>
|
|
<div>
|
|
<flux:badge size="sm" color="blue">{{ $supplier->parts_count }} parts</flux:badge>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<div class="text-center py-8">
|
|
<flux:icon.building-office class="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
|
<p class="text-zinc-500 dark:text-zinc-400 dark:text-gray-400">No suppliers found</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 p-6">
|
|
<h2 class="text-lg font-medium text-zinc-900 dark:text-white dark:text-white mb-4">Quick Actions</h2>
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<flux:button wire:navigate href="{{ route('inventory.parts.create') }}" variant="primary" class="w-full">
|
|
<flux:icon.plus class="w-6 h-6 mr-2" />
|
|
Add Part
|
|
</flux:button>
|
|
<flux:button wire:navigate href="{{ route('inventory.purchase-orders.create') }}" variant="outline" class="w-full">
|
|
<flux:icon.shopping-cart class="w-6 h-6 mr-2" />
|
|
Create Purchase Order
|
|
</flux:button>
|
|
<flux:button wire:navigate href="{{ route('inventory.stock-movements.create') }}" variant="outline" class="w-full">
|
|
<flux:icon.clipboard-document-list class="w-6 h-6 mr-2" />
|
|
Record Stock Movement
|
|
</flux:button>
|
|
<flux:button wire:navigate href="{{ route('inventory.suppliers.create') }}" variant="outline" class="w-full">
|
|
<flux:icon.building-office class="w-6 h-6 mr-2" />
|
|
Add Supplier
|
|
</flux:button>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
|
|
<flux:button wire:navigate href="{{ route('inventory.parts.index', ['stockFilter' => 'low_stock']) }}" variant="outline" class="w-full">
|
|
<flux:icon.exclamation-triangle class="w-6 h-6 mr-2" />
|
|
Low Stock Items
|
|
</flux:button>
|
|
<flux:button wire:navigate href="{{ route('inventory.parts.index', ['stockFilter' => 'out_of_stock']) }}" variant="outline" class="w-full">
|
|
<flux:icon.x-circle class="w-6 h-6 mr-2" />
|
|
Out of Stock
|
|
</flux:button>
|
|
<flux:button wire:navigate href="{{ route('inventory.stock-movements.index') }}" variant="outline" class="w-full">
|
|
<flux:icon.clipboard-document-list class="w-6 h-6 mr-2" />
|
|
View Stock History
|
|
</flux:button>
|
|
<flux:button wire:navigate href="{{ route('inventory.purchase-orders.index') }}" variant="outline" class="w-full">
|
|
<flux:icon.shopping-cart class="w-6 h-6 mr-2" />
|
|
Purchase Orders
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
</div>
|