sackey a65fee9d75
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
Add customer portal workflow progress component and analytics dashboard
- Implemented the customer portal workflow progress component with detailed service progress tracking, including current status, workflow steps, and contact information.
- Developed a management workflow analytics dashboard featuring key performance indicators, charts for revenue by branch, labor utilization, and recent quality issues.
- Created tests for admin-only middleware to ensure proper access control for admin routes.
- Added tests for customer portal view rendering and workflow integration, ensuring the workflow service operates correctly through various stages.
- Introduced a .gitignore file for the debugbar storage directory to prevent unnecessary files from being tracked.
2025-08-10 19:41:25 +00:00

472 lines
26 KiB
PHP

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
@fluxAppearance
</head>
<body class="min-h-screen bg-white dark:bg-zinc-800">
<!-- Static Header with Search, Theme Switcher, and User Profile -->
<header class="bg-white dark:bg-zinc-800 border-b border-zinc-200 dark:border-zinc-700 fixed top-0 left-0 right-0 z-50 px-4 h-16">
<div class="flex items-center justify-between h-full">
<!-- Left Section: Sidebar Toggle + Logo -->
<div class="flex items-center space-x-3">
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" />
<flux:brand href="{{ route('dashboard') }}" name="" wire:navigate class="flex items-center">
<x-app-logo />
</flux:brand>
</div>
<!-- Center Section: Search Bar with Quick Actions -->
<div class="flex-1 max-w-2xl mx-8">
<div class="flex items-center space-x-3">
<!-- Search Component -->
<div class="flex-1">
<livewire:global-search />
</div>
<!-- Plus Icon for More Quick Actions -->
<flux:dropdown align="end">
<flux:button variant="primary" square icon="plus" aria-label="More quick actions" />
<flux:menu>
@if(auth()->user()->hasPermission('customers.create'))
<flux:menu.item icon="user-plus" href="{{ route('customers.create') }}" wire:navigate>
New Customer
</flux:menu.item>
@endif
@if(auth()->user()->hasPermission('vehicles.create'))
<flux:menu.item icon="truck" href="{{ route('vehicles.create') }}" wire:navigate>
New Vehicle
</flux:menu.item>
@endif
@if(auth()->user()->hasPermission('job-cards.create'))
<flux:menu.item icon="clipboard-document-list" href="{{ route('job-cards.create') }}" wire:navigate>
New Job Card
</flux:menu.item>
@endif
@if(auth()->user()->hasPermission('appointments.create'))
<flux:menu.item icon="calendar" href="{{ route('appointments.create') }}" wire:navigate>
New Appointment
</flux:menu.item>
@endif
<flux:menu.separator />
@if(auth()->user()->hasPermission('estimates.create'))
<flux:menu.item icon="document-plus" href="{{ route('estimates.index') }}" wire:navigate>
New Estimate
</flux:menu.item>
@endif
@if(auth()->user()->hasPermission('work-orders.create'))
<flux:menu.item icon="wrench-screwdriver" href="{{ route('work-orders.index') }}" wire:navigate>
New Work Order
</flux:menu.item>
@endif
@if(auth()->user()->hasPermission('inspections.create'))
<flux:menu.item icon="clipboard-document-check" href="{{ route('inspections.index') }}" wire:navigate>
New Inspection
</flux:menu.item>
@endif
</flux:menu>
</flux:dropdown>
</div>
</div>
<!-- Right Section: Notifications, Theme Switcher, and User Profile -->
<div class="flex items-center space-x-3">
<!-- Notifications -->
<flux:dropdown align="end">
<flux:button variant="subtle" square class="relative" aria-label="Notifications">
<flux:icon.bell variant="mini" class="text-zinc-500 dark:text-white" />
<!-- Notification badge -->
<span class="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">3</span>
</flux:button>
<flux:menu>
<div class="px-3 py-2 border-b border-zinc-200 dark:border-zinc-700">
<div class="font-semibold text-sm">Notifications</div>
</div>
<flux:menu.item icon="exclamation-triangle" href="#">
<div>
<div class="text-sm font-medium">Low Stock Alert</div>
<div class="text-xs text-zinc-500">Brake pads running low</div>
</div>
</flux:menu.item>
<flux:menu.item icon="clock" href="#">
<div>
<div class="text-sm font-medium">Appointment Reminder</div>
<div class="text-xs text-zinc-500">Service due in 1 hour</div>
</div>
</flux:menu.item>
<flux:menu.item icon="check-circle" href="#">
<div>
<div class="text-sm font-medium">Job Completed</div>
<div class="text-xs text-zinc-500">JC-2024-001 finished</div>
</div>
</flux:menu.item>
<flux:menu.separator />
<flux:menu.item href="#" class="text-center text-sm text-zinc-500">
View all notifications
</flux:menu.item>
</flux:menu>
</flux:dropdown>
<!-- Theme Switcher -->
<flux:dropdown x-data align="end">
<flux:button variant="subtle" square class="group" aria-label="Preferred color scheme">
<flux:icon.sun x-show="$flux.appearance === 'light'" variant="mini" class="text-zinc-500 dark:text-white" />
<flux:icon.moon x-show="$flux.appearance === 'dark'" variant="mini" class="text-zinc-500 dark:text-white" />
<flux:icon.moon x-show="$flux.appearance === 'system' && $flux.dark" variant="mini" />
<flux:icon.sun x-show="$flux.appearance === 'system' && ! $flux.dark" variant="mini" />
</flux:button>
<flux:menu>
<flux:menu.item icon="sun" x-on:click="$flux.appearance = 'light'">Light</flux:menu.item>
<flux:menu.item icon="moon" x-on:click="$flux.appearance = 'dark'">Dark</flux:menu.item>
<flux:menu.item icon="computer-desktop" x-on:click="$flux.appearance = 'system'">System</flux:menu.item>
</flux:menu>
</flux:dropdown>
<!-- User Profile -->
<flux:dropdown position="top" align="start">
<flux:profile name="{{ auth()->user()->name }}" avatar="" />
<flux:menu>
<flux:menu.item icon="user" href="{{ route('settings.profile') }}" wire:navigate>
My Profile
</flux:menu.item>
<flux:menu.item icon="key" href="{{ route('settings.password') }}" wire:navigate>
Change Password
</flux:menu.item>
@if(auth()->user()->hasPermission('settings.manage'))
<flux:menu.item icon="cog-6-tooth" href="{{ route('settings.general') }}" wire:navigate>
System Settings
</flux:menu.item>
@endif
<flux:menu.separator />
<div class="px-3 py-2 text-xs text-zinc-500 dark:text-zinc-400">
<div class="font-semibold">{{ auth()->user()->position ?? 'Employee' }}</div>
<div>{{ auth()->user()->department ?? 'General' }}</div>
<div class="text-xs">ID: {{ auth()->user()->employee_id ?? 'N/A' }}</div>
</div>
<flux:menu.separator />
<form method="POST" action="{{ route('logout') }}">
@csrf
<flux:menu.item icon="arrow-right-start-on-rectangle" type="submit">
Logout
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</div>
</div>
</header>
<!-- Main Sidebar -->
<flux:sidebar sticky stashable class="bg-zinc-50 dark:bg-zinc-900 border-r rtl:border-r-0 rtl:border-l border-zinc-200 dark:border-zinc-700 pt-16" x-data="sidebarState">
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
<!-- Main Navigation -->
<flux:navlist variant="outline">
<!-- Dashboard -->
@if(auth()->user()->hasPermission('dashboard.view'))
<flux:navlist.item icon="home" href="{{ route('dashboard') }}" :current="request()->routeIs('dashboard')" wire:navigate>
Dashboard
</flux:navlist.item>
@endif
<!-- Core Operations -->
<flux:navlist.group expandable heading="Workshop Operations" x-model="collapsed.workshop">
@if(auth()->user()->hasPermission('job-cards.view'))
<flux:navlist.item icon="clipboard-document-list" href="{{ route('job-cards.index') }}" :current="request()->is('job-cards*')" wire:navigate>
Job Cards
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('work-orders.view'))
<flux:navlist.item icon="wrench-screwdriver" href="{{ route('work-orders.index') }}" :current="request()->is('work-orders*')" wire:navigate>
Work Orders
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('inspections.view'))
<flux:navlist.item icon="clipboard-document-check" href="{{ route('inspections.index') }}" :current="request()->is('inspections*')" wire:navigate>
Inspections
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('diagnosis.view'))
<flux:navlist.item icon="magnifying-glass-circle" href="{{ route('diagnosis.index') }}" :current="request()->is('diagnosis*')" wire:navigate>
Diagnostics
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('timesheets.view'))
<flux:navlist.item icon="clock" href="{{ route('timesheets.index') }}" :current="request()->is('timesheets*')" wire:navigate>
Timesheets
</flux:navlist.item>
@endif
</flux:navlist.group>
<!-- Customer Management -->
<flux:navlist.group expandable heading="Customer Management" x-model="collapsed.customers">
@if(auth()->user()->hasPermission('customers.view'))
<flux:navlist.item icon="users" href="{{ route('customers.list') }}" :current="request()->routeIs('customers.*')" wire:navigate>
Customers
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('vehicles.view'))
<flux:navlist.item icon="truck" href="{{ route('vehicles.index') }}" :current="request()->is('vehicles*')" wire:navigate>
Vehicles
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('appointments.view'))
<flux:navlist.item icon="calendar" href="{{ route('appointments.index') }}" :current="request()->is('appointments*')" wire:navigate>
Appointments
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('service-orders.view'))
<flux:navlist.item icon="document-text" href="{{ route('service-orders.list') }}" :current="request()->is('service-orders*')" wire:navigate>
Service Orders
</flux:navlist.item>
@endif
</flux:navlist.group>
<!-- Financial Management -->
<flux:navlist.group expandable heading="Financial" x-model="collapsed.financial">
@if(auth()->user()->hasPermission('estimates.view'))
<flux:navlist.item icon="document-plus" href="{{ route('estimates.index') }}" :current="request()->is('estimates*')" wire:navigate>
Estimates
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('service-orders.view'))
<flux:navlist.item icon="receipt-percent" href="#" :current="request()->is('invoices*')" wire:navigate>
Invoices
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('service-orders.view'))
<flux:navlist.item icon="credit-card" href="#" :current="request()->is('payments*')" wire:navigate>
Payments
</flux:navlist.item>
@endif
</flux:navlist.group>
<!-- Inventory & Resources -->
<flux:navlist.group expandable heading="Inventory & Resources" x-model="collapsed.inventory">
@if(auth()->user()->hasPermission('inventory.view'))
<flux:navlist.item icon="cube" href="{{ route('inventory.dashboard') }}" :current="request()->is('inventory*')" wire:navigate>
Inventory Dashboard
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('inventory.view'))
<flux:navlist.item icon="cog" href="{{ route('inventory.parts.index') }}" :current="request()->is('inventory/parts*')" wire:navigate>
Parts Catalog
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('inventory.view'))
<flux:navlist.item icon="building-storefront" href="{{ route('inventory.suppliers.index') }}" :current="request()->is('inventory/suppliers*')" wire:navigate>
Suppliers
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('inventory.purchase-orders'))
<flux:navlist.item icon="shopping-cart" href="{{ route('inventory.purchase-orders.index') }}" :current="request()->is('inventory/purchase-orders*')" wire:navigate>
Purchase Orders
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('inventory.stock-movements'))
<flux:navlist.item icon="arrows-right-left" href="{{ route('inventory.stock-movements.index') }}" :current="request()->is('inventory/stock-movements*')" wire:navigate>
Stock Movements
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('service-orders.view'))
<flux:navlist.item icon="list-bullet" href="{{ route('service-items.index') }}" :current="request()->is('service-items*')" wire:navigate>
Service Items
</flux:navlist.item>
@endif
</flux:navlist.group>
<!-- Staff Management -->
<flux:navlist.group expandable heading="Staff Management" x-model="collapsed.staff">
@if(auth()->user()->hasPermission('technicians.view'))
<flux:navlist.item icon="user-group" href="{{ route('technicians.index') }}" :current="request()->is('technicians*')" wire:navigate>
Technicians
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('technicians.update'))
<flux:navlist.item icon="academic-cap" href="{{ route('technician.skills') }}" :current="request()->is('technician/skills*')" wire:navigate>
Skills Management
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('technicians.view-performance'))
<flux:navlist.item icon="chart-bar-square" href="{{ route('technician.reports') }}" :current="request()->is('technician/reports*')" wire:navigate>
Performance Reports
</flux:navlist.item>
@endif
</flux:navlist.group>
</flux:navlist>
<flux:spacer />
<!-- Bottom Navigation -->
<flux:navlist variant="outline">
@if(auth()->user()->hasPermission('reports.view'))
<flux:navlist.item icon="chart-bar" href="{{ route('reports.index') }}" :current="request()->is('reports*')" wire:navigate>
Reports & Analytics
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('users.manage'))
<flux:navlist.item icon="users" href="{{ route('users.index') }}" :current="request()->routeIs('users.*')" wire:navigate>
User Management
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('branches.view'))
<flux:navlist.item icon="building-office" href="{{ route('branches.index') }}" :current="request()->routeIs('branches.*')" wire:navigate>
Branch Management
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('settings.manage'))
<flux:navlist.item icon="cog-6-tooth" href="{{ route('settings.general') }}" :current="request()->is('settings*')" wire:navigate>
Settings
</flux:navlist.item>
@endif
</flux:navlist>
</flux:sidebar>
<!-- Main Content -->
<flux:main class="pt-16">
{{ $slot }}
</flux:main>
<!-- Toast Notifications -->
@if(session('success'))
<div id="toast-success"
class="fixed bottom-4 right-4 z-50 flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow-lg border border-gray-200 dark:text-gray-400 dark:bg-gray-800 dark:border-gray-600 transform translate-y-0 opacity-100 transition-all duration-300 ease-in-out"
role="alert">
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
</svg>
<span class="sr-only">Check icon</span>
</div>
<div class="ms-3 text-sm font-normal">{{ session('success') }}</div>
<button type="button"
class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700"
data-dismiss-target="#toast-success"
aria-label="Close">
<span class="sr-only">Close</span>
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
</svg>
</button>
</div>
@endif
@if(session('error'))
<div id="toast-error"
class="fixed bottom-4 right-4 z-50 flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow-lg border border-gray-200 dark:text-gray-400 dark:bg-gray-800 dark:border-gray-600 transform translate-y-0 opacity-100 transition-all duration-300 ease-in-out"
role="alert">
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200">
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"/>
</svg>
<span class="sr-only">Error icon</span>
</div>
<div class="ms-3 text-sm font-normal">{{ session('error') }}</div>
<button type="button"
class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700"
data-dismiss-target="#toast-error"
aria-label="Close">
<span class="sr-only">Close</span>
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
</svg>
</button>
</div>
@endif
<script>
// Auto-hide toasts after 5 seconds with slide animation
document.addEventListener('DOMContentLoaded', function() {
const toasts = document.querySelectorAll('[id^="toast-"]');
toasts.forEach(function(toast) {
// Initial slide-in animation
toast.style.transform = 'translateX(100%)';
toast.style.opacity = '0';
setTimeout(function() {
toast.style.transform = 'translateX(0)';
toast.style.opacity = '1';
}, 100);
// Auto-hide after 5 seconds
setTimeout(function() {
hideToast(toast);
}, 5000);
});
// Handle manual close buttons
document.querySelectorAll('[data-dismiss-target]').forEach(function(button) {
button.addEventListener('click', function() {
const targetId = this.getAttribute('data-dismiss-target');
const target = document.querySelector(targetId);
if (target) {
hideToast(target);
}
});
});
function hideToast(toast) {
toast.style.transform = 'translateX(100%)';
toast.style.opacity = '0';
setTimeout(function() {
toast.remove();
}, 300);
}
});
// Sidebar collapse state persistence
document.addEventListener('alpine:init', () => {
Alpine.data('sidebarState', () => ({
collapsed: JSON.parse(localStorage.getItem('sidebarCollapsed') || '{}'),
init() {
this.$watch('collapsed', (value) => {
localStorage.setItem('sidebarCollapsed', JSON.stringify(value));
});
}
}));
});
</script>
@livewireScripts
@fluxScripts
</body>
</html>