sackey e3b2b220d2
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
Enhance UI and functionality across various components
- 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.
2025-08-16 14:36:58 +00:00

275 lines
17 KiB
PHP

<div>
<flux:header class="space-y-2">
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">Technician Management</flux:heading>
<flux:subheading>Manage technician profiles, skills, and performance</flux:subheading>
</div>
<flux:button variant="primary" icon="plus" wire:click="$dispatch('create-technician')">
Add Technician
</flux:button>
</div>
<!-- Filters and Search -->
<div class="flex flex-col lg:flex-row gap-4">
<div class="flex-1 relative">
<flux:input wire:model.live="search" placeholder="Search technicians..." icon="magnifying-glass" :loading="false" />
<!-- Custom loading indicator with user icon -->
<div wire:loading wire:target="search" class="absolute right-3 top-1/2 transform -translate-y-1/2">
<flux:icon.user class="w-6 h-6 text-zinc-400 animate-bounce" />
</div>
</div>
<div class="flex gap-2">
<flux:select wire:model.live="statusFilter" placeholder="All Statuses">
<option value="">All Statuses</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="on_leave">On Leave</option>
</flux:select>
<flux:select wire:model.live="skillFilter" placeholder="All Skills">
<option value="">All Skills</option>
@foreach($availableSkills as $skill)
<option value="{{ $skill }}">{{ ucfirst(str_replace('_', ' ', $skill)) }}</option>
@endforeach
</flux:select>
</div>
</div>
</flux:header>
<!-- Technicians Table -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl p-6 space-y-6">
@if($technicians->count() > 0)
<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('first_name')" class="flex items-center space-x-1 hover:text-blue-600">
<span>Name</span>
@if($sortBy === 'first_name')
<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('employee_id')" class="flex items-center space-x-1 hover:text-blue-600">
<span>Employee ID</span>
@if($sortBy === 'employee_id')
<flux:icon name="{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="size-3" />
@endif
</button>
</th>
<th class="text-left py-3 px-4">Email</th>
<th class="text-left py-3 px-4">Phone</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">Primary Skills</th>
<th class="text-left py-3 px-4">Performance</th>
<th class="text-left py-3 px-4">Utilization</th>
<th class="text-left py-3 px-4">Actions</th>
</tr>
</thead>
<tbody>
@foreach($technicians as $technician)
<tr class="border-b border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800">
<td class="py-3 px-4">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-zinc-100 rounded-full flex items-center justify-center text-sm font-medium text-zinc-600">
{{ strtoupper(substr($technician->first_name, 0, 1) . substr($technician->last_name, 0, 1)) }}
</div>
<div>
<div class="font-semibold">{{ $technician->full_name }}</div>
<div class="text-sm text-zinc-500">
${{ number_format($technician->hourly_rate, 2) }}/hr
</div>
</div>
</div>
</td>
<td class="py-3 px-4">{{ $technician->employee_id }}</td>
<td class="py-3 px-4">{{ $technician->email }}</td>
<td class="py-3 px-4">{{ $technician->phone }}</td>
<td class="py-3 px-4">
<flux:badge size="sm"
:color="$technician->status === 'active' ? 'green' : ($technician->status === 'inactive' ? 'red' : 'yellow')">
{{ ucfirst(str_replace('_', ' ', $technician->status)) }}
</flux:badge>
</td>
<td class="py-3 px-4">
<div class="flex flex-wrap gap-1">
@php
$primarySkills = $technician->skills->where('is_primary_skill', true);
@endphp
@foreach($primarySkills->take(3) as $skill)
<flux:badge size="sm" color="blue">
{{ ucfirst(str_replace('_', ' ', $skill->skill_name)) }}
({{ $skill->proficiency_level }})
</flux:badge>
@endforeach
@if($primarySkills->count() > 3)
<flux:badge size="sm" color="zinc">
+{{ $primarySkills->count() - 3 }} more
</flux:badge>
@endif
</div>
</td>
<td class="py-3 px-4">
<div class="flex items-center space-x-2">
<div class="text-sm">
{{ number_format($technician->getAverageRating(), 1) }}/5
</div>
<div class="flex text-yellow-400">
@for($i = 1; $i <= 5; $i++)
@if($i <= floor($technician->getAverageRating()))
<flux:icon name="star" variant="solid" class="w-3 h-3" />
@elseif($i - 0.5 <= $technician->getAverageRating())
<flux:icon name="star" class="w-3 h-3" />
@else
<flux:icon name="star" variant="outline" class="w-3 h-3" />
@endif
@endfor
</div>
</div>
<div class="text-xs text-zinc-500">
{{ $technician->getTotalJobsCompleted() }} jobs completed
</div>
</td>
<td class="py-3 px-4">
<div class="text-sm font-medium">
{{ number_format($technician->getCurrentUtilizationRate(), 1) }}%
</div>
<div class="w-full bg-zinc-200 rounded-full h-2 mt-1">
<div class="h-2 rounded-full {{ $technician->getCurrentUtilizationRate() >= 80 ? 'bg-green-500' : ($technician->getCurrentUtilizationRate() >= 60 ? 'bg-yellow-500' : 'bg-red-500') }}"
style="width: {{ min($technician->getCurrentUtilizationRate(), 100) }}%"></div>
</div>
</td>
<td class="py-3 px-4">
<div class="flex items-center space-x-2">
<flux:button size="sm" variant="ghost" icon="eye" wire:click="showDetails({{ $technician->id }})">
View
</flux:button>
<flux:button size="sm" variant="ghost" icon="pencil" wire:click="$dispatch('edit-technician', [{{ $technician->id }}])">
Edit
</flux:button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="mt-6">
{{ $technicians->links() }}
</div>
@else
<div class="text-center py-12">
<flux:icon name="users" class="w-12 h-12 text-zinc-400 mx-auto mb-4" />
<flux:heading size="lg" class="text-zinc-600 mb-2">No technicians found</flux:heading>
<flux:subheading class="text-zinc-500 mb-4">
@if($search || $statusFilter || $skillFilter)
Try adjusting your filters to see more results.
@else
Get started by adding your first technician.
@endif
</flux:subheading>
<flux:button variant="primary" icon="plus" wire:click="$dispatch('create-technician')">
Add Technician
</flux:button>
</div>
@endif
</div>
<!-- Technician Details Modal -->
@if($showingDetails && $selectedTechnician)
<div class="fixed inset-0 z-50 overflow-y-auto" x-data="{ show: @entangle('showingDetails') }" x-show="show" x-cloak>
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity bg-zinc-50 dark:bg-zinc-9000 bg-opacity-75" x-show="show" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">&#8203;</span>
<div class="inline-block align-bottom bg-white dark:bg-zinc-800 rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full sm:p-6" x-show="show" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<flux:heading size="lg">{{ $selectedTechnician->full_name }} - Details</flux:heading>
<flux:button variant="ghost" size="sm" icon="x-mark" wire:click="closeDetails"></flux:button>
</div>
<!-- Body -->
<div class="space-y-6">
<!-- Basic Info -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<flux:heading size="lg" class="mb-3">Basic Information</flux:heading>
<div class="space-y-2">
<div><strong>Employee ID:</strong> {{ $selectedTechnician->employee_id }}</div>
<div><strong>Email:</strong> {{ $selectedTechnician->email }}</div>
<div><strong>Phone:</strong> {{ $selectedTechnician->phone }}</div>
<div><strong>Hourly Rate:</strong> ${{ number_format($selectedTechnician->hourly_rate, 2) }}</div>
<div><strong>Status:</strong>
<flux:badge :color="$selectedTechnician->status === 'active' ? 'green' : ($selectedTechnician->status === 'inactive' ? 'red' : 'yellow')">
{{ ucfirst(str_replace('_', ' ', $selectedTechnician->status)) }}
</flux:badge>
</div>
</div>
</div>
<div>
<flux:heading size="lg" class="mb-3">Performance Overview</flux:heading>
<div class="space-y-2">
<div><strong>Average Rating:</strong> {{ number_format($selectedTechnician->getAverageRating(), 1) }}/5</div>
<div><strong>Jobs Completed:</strong> {{ $selectedTechnician->getTotalJobsCompleted() }}</div>
<div><strong>Current Utilization:</strong> {{ number_format($selectedTechnician->getCurrentUtilizationRate(), 1) }}%</div>
</div>
</div>
</div>
<!-- Skills -->
<div>
<flux:heading size="lg" class="mb-3">Skills & Certifications</flux:heading>
@if($selectedTechnician->skills->count() > 0)
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@foreach($selectedTechnician->skills as $skill)
<div class="flex items-center justify-between p-3 border rounded-lg">
<div>
<div class="font-medium">{{ ucfirst(str_replace('_', ' ', $skill->skill_name)) }}</div>
<div class="text-sm text-zinc-500">{{ ucfirst($skill->category) }}</div>
@if($skill->certification_body)
<div class="text-xs text-blue-600">{{ $skill->certification_body }}</div>
@endif
</div>
<div class="text-right">
<div class="text-lg font-bold">{{ $skill->proficiency_level }}/5</div>
@if($skill->is_primary_skill)
<flux:badge size="sm" color="blue">Primary</flux:badge>
@endif
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-4 text-zinc-500">No skills recorded</div>
@endif
</div>
<!-- Footer -->
<div class="mt-6 flex justify-end space-x-3">
<flux:button variant="ghost" wire:click="closeDetails">Close</flux:button>
<flux:button variant="primary" icon="pencil" wire:click="$dispatch('edit-technician', [{{ $selectedTechnician->id }}])">
Edit Technician
</flux:button>
</div>
</div>
</div>
</div>
@endif
</div>