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

515 lines
18 KiB
PHP

<?php
namespace App\Livewire\JobCards;
use Livewire\Component;
use Livewire\WithPagination;
use App\Models\JobCard;
use App\Models\Branch;
use App\Models\User;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class Index extends Component
{
use WithPagination, AuthorizesRequests;
public $search = '';
public $statusFilter = '';
public $branchFilter = '';
public $priorityFilter = '';
public $serviceAdvisorFilter = '';
public $dateRange = '';
public $sortBy = 'created_at';
public $sortDirection = 'desc';
// Bulk actions
public $selectedJobCards = [];
public $selectAll = false;
public $bulkAction = '';
// Statistics
public $statistics = [
'total' => 0,
'received' => 0,
'in_progress' => 0,
'pending_approval' => 0,
'completed_today' => 0,
'delivered_today' => 0,
'overdue' => 0,
];
protected $queryString = [
'search' => ['except' => ''],
'statusFilter' => ['except' => ''],
'branchFilter' => ['except' => ''],
'priorityFilter' => ['except' => ''],
'serviceAdvisorFilter' => ['except' => ''],
'dateRange' => ['except' => ''],
'sortBy' => ['except' => 'created_at'],
'sortDirection' => ['except' => 'desc'],
];
public function boot()
{
// Ensure properties are properly initialized
$this->selectedJobCards = $this->selectedJobCards ?? [];
$this->statistics = $this->statistics ?? [
'total' => 0,
'received' => 0,
'in_progress' => 0,
'pending_approval' => 0,
'completed_today' => 0,
'delivered_today' => 0,
'overdue' => 0,
];
}
public function mount()
{
$this->boot(); // Ensure properties are initialized
$this->authorize('viewAny', JobCard::class);
// Add debug information to debugbar
if (app()->bound('debugbar')) {
debugbar()->info('JobCard Index component mounted');
debugbar()->addMessage('User: ' . auth()->user()->name, 'user');
debugbar()->addMessage('User permissions checked for JobCard access', 'auth');
}
$this->loadStatistics();
}
public function updatingSearch()
{
$this->resetPage();
$this->selectedJobCards = [];
$this->selectAll = false;
$this->loadStatistics();
}
public function updatingStatusFilter()
{
$this->resetPage();
$this->selectedJobCards = [];
$this->selectAll = false;
$this->loadStatistics();
}
public function updatingBranchFilter()
{
$this->resetPage();
$this->selectedJobCards = [];
$this->selectAll = false;
$this->loadStatistics();
}
public function updatingPriorityFilter()
{
$this->resetPage();
$this->selectedJobCards = [];
$this->selectAll = false;
$this->loadStatistics();
}
public function updatingServiceAdvisorFilter()
{
$this->resetPage();
$this->selectedJobCards = [];
$this->selectAll = false;
$this->loadStatistics();
}
public function updatingDateRange()
{
$this->resetPage();
$this->selectedJobCards = [];
$this->selectAll = false;
$this->loadStatistics();
}
public function sortBy($field)
{
if ($this->sortBy === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $field;
$this->sortDirection = 'asc';
}
$this->resetPage();
}
public function refreshData()
{
$this->loadStatistics();
$this->selectedJobCards = [];
$this->selectAll = false;
session()->flash('success', 'Data refreshed successfully.');
}
public function clearFilters()
{
$this->search = '';
$this->statusFilter = '';
$this->branchFilter = '';
$this->priorityFilter = '';
$this->serviceAdvisorFilter = '';
$this->dateRange = '';
$this->selectedJobCards = [];
$this->selectAll = false;
$this->resetPage();
$this->loadStatistics();
session()->flash('success', 'Filters cleared successfully.');
}
/**
* Get workflow progress percentage for a job card
*/
public function getWorkflowProgress($status)
{
$steps = [
JobCard::STATUS_RECEIVED => 1,
JobCard::STATUS_INSPECTED => 2,
JobCard::STATUS_ASSIGNED_FOR_DIAGNOSIS => 3,
JobCard::STATUS_IN_DIAGNOSIS => 4,
JobCard::STATUS_ESTIMATE_SENT => 5,
JobCard::STATUS_APPROVED => 6,
JobCard::STATUS_PARTS_PROCUREMENT => 7,
JobCard::STATUS_IN_PROGRESS => 8,
JobCard::STATUS_QUALITY_REVIEW_REQUIRED => 9,
JobCard::STATUS_COMPLETED => 10,
JobCard::STATUS_DELIVERED => 11,
];
$currentStep = $steps[$status] ?? 1;
return round(($currentStep / 11) * 100);
}
public function loadStatistics()
{
try {
if (app()->bound('debugbar')) {
debugbar()->startMeasure('statistics', 'Loading JobCard Statistics');
}
$user = auth()->user();
$query = JobCard::query();
// Apply branch filtering based on user permissions
if (!$user->hasPermission('job-cards.view-all')) {
if ($user->hasPermission('job-cards.view-own')) {
$query->where('service_advisor_id', $user->id);
} elseif ($user->hasPermission('job-cards.view')) {
$query->where('branch_code', $user->branch_code);
}
}
$this->statistics = [
'total' => $query->count(),
'received' => (clone $query)->where('status', JobCard::STATUS_RECEIVED)->count(),
'in_progress' => (clone $query)->whereIn('status', [
JobCard::STATUS_IN_DIAGNOSIS,
JobCard::STATUS_IN_PROGRESS,
JobCard::STATUS_PARTS_PROCUREMENT
])->count(),
'pending_approval' => (clone $query)->where('status', JobCard::STATUS_ESTIMATE_SENT)->count(),
'completed_today' => (clone $query)->where('status', JobCard::STATUS_COMPLETED)
->whereDate('completion_datetime', today())->count(),
'delivered_today' => (clone $query)->where('status', JobCard::STATUS_DELIVERED)
->whereDate('completion_datetime', today())->count(),
'overdue' => (clone $query)->where('expected_completion_date', '<', now())
->whereNotIn('status', [JobCard::STATUS_COMPLETED, JobCard::STATUS_DELIVERED])
->count(),
];
if (app()->bound('debugbar')) {
debugbar()->stopMeasure('statistics');
debugbar()->addMessage('Statistics loaded: ' . json_encode($this->statistics), 'statistics');
}
} catch (\Exception $e) {
// Fallback statistics if there's an error
$this->statistics = [
'total' => 0,
'received' => 0,
'in_progress' => 0,
'pending_approval' => 0,
'completed_today' => 0,
'delivered_today' => 0,
'overdue' => 0,
];
if (app()->bound('debugbar')) {
debugbar()->error('Error loading JobCard statistics: ' . $e->getMessage());
}
logger()->error('Error loading JobCard statistics: ' . $e->getMessage());
}
}
public function updatedSelectAll()
{
if ($this->selectAll) {
try {
$this->selectedJobCards = $this->getJobCards()->pluck('id')->toArray();
} catch (\Exception $e) {
$this->selectedJobCards = [];
$this->selectAll = false;
session()->flash('error', 'Unable to select all job cards. Please try again.');
}
} else {
$this->selectedJobCards = [];
}
}
public function processBulkAction()
{
if (empty($this->selectedJobCards) || empty($this->bulkAction)) {
session()->flash('error', 'Please select job cards and an action.');
return;
}
$successCount = 0;
$errorCount = 0;
foreach ($this->selectedJobCards as $jobCardId) {
try {
$jobCard = JobCard::find($jobCardId);
if (!$jobCard) continue;
switch ($this->bulkAction) {
case 'export_csv':
return $this->exportSelected();
break;
}
} catch (\Exception $e) {
$errorCount++;
}
}
$this->selectedJobCards = [];
$this->selectAll = false;
$this->bulkAction = '';
$this->loadStatistics(); // Refresh statistics after bulk operations
if ($successCount > 0) {
session()->flash('success', "{$successCount} job cards processed successfully.");
}
if ($errorCount > 0) {
session()->flash('error', "{$errorCount} job cards failed to process.");
}
}
public function exportSelected()
{
if (empty($this->selectedJobCards)) {
session()->flash('error', 'Please select job cards to export.');
return;
}
$jobCards = JobCard::with(['customer', 'vehicle', 'serviceAdvisor'])
->whereIn('id', $this->selectedJobCards)
->get();
$csv = "Job Card Number,Customer,Vehicle,Service Advisor,Status,Priority,Created Date,Expected Completion\n";
foreach ($jobCards as $jobCard) {
$csv .= sprintf(
"%s,%s,%s,%s,%s,%s,%s,%s\n",
$jobCard->job_card_number,
$jobCard->customer->full_name ?? '',
$jobCard->vehicle->display_name ?? '',
$jobCard->serviceAdvisor->name ?? '',
$jobCard->status,
$jobCard->priority,
$jobCard->created_at->format('Y-m-d'),
$jobCard->expected_completion_date ? $jobCard->expected_completion_date->format('Y-m-d') : ''
);
}
return response()->streamDownload(function () use ($csv) {
echo $csv;
}, 'job-cards-' . date('Y-m-d') . '.csv', [
'Content-Type' => 'text/csv',
]);
}
protected function getJobCards()
{
try {
$user = auth()->user();
$query = JobCard::query()
->with(['customer', 'vehicle', 'serviceAdvisor']);
// Apply permission-based filtering
if (!$user->hasPermission('job-cards.view-all')) {
if ($user->hasPermission('job-cards.view-own')) {
$query->where('service_advisor_id', $user->id);
} elseif ($user->hasPermission('job-cards.view')) {
$query->where('branch_code', $user->branch_code);
}
}
// Apply filters
if ($this->search) {
$query->where(function ($q) {
$q->where('job_card_number', 'like', '%' . $this->search . '%')
->orWhereHas('customer', function ($customerQuery) {
$customerQuery->where('first_name', 'like', '%' . $this->search . '%')
->orWhere('last_name', 'like', '%' . $this->search . '%')
->orWhere('email', 'like', '%' . $this->search . '%');
})
->orWhereHas('vehicle', function ($vehicleQuery) {
$vehicleQuery->where('license_plate', 'like', '%' . $this->search . '%')
->orWhere('vin', 'like', '%' . $this->search . '%');
});
});
}
if ($this->statusFilter) {
$query->where('status', $this->statusFilter);
}
if ($this->branchFilter) {
$query->where('branch_code', $this->branchFilter);
}
if ($this->priorityFilter) {
$query->where('priority', $this->priorityFilter);
}
if ($this->serviceAdvisorFilter) {
$query->where('service_advisor_id', $this->serviceAdvisorFilter);
}
if ($this->dateRange) {
switch ($this->dateRange) {
case 'today':
$query->whereDate('created_at', today());
break;
case 'week':
$query->whereBetween('created_at', [now()->startOfWeek(), now()->endOfWeek()]);
break;
case 'month':
$query->whereMonth('created_at', now()->month)
->whereYear('created_at', now()->year);
break;
case 'overdue':
$query->where('expected_completion_date', '<', now())
->whereNotIn('status', [JobCard::STATUS_COMPLETED, JobCard::STATUS_DELIVERED]);
break;
}
}
return $query->orderBy($this->sortBy, $this->sortDirection);
} catch (\Exception $e) {
logger()->error('Error in getJobCards query: ' . $e->getMessage());
// Return empty query as fallback
return JobCard::query()->whereRaw('1 = 0'); // Returns empty result set
}
}
public function render()
{
try {
// Ensure statistics are always fresh and available
if (empty($this->statistics) || !isset($this->statistics['total'])) {
$this->loadStatistics();
}
$jobCards = $this->getJobCards()->paginate(20);
$statusOptions = JobCard::getStatusOptions();
$priorityOptions = [
'low' => 'Low',
'medium' => 'Medium',
'high' => 'High',
'urgent' => 'Urgent',
];
$branchOptions = Branch::active()
->orderBy('name')
->pluck('name', 'code')
->toArray();
$serviceAdvisorOptions = User::whereHas('roles', function ($query) {
$query->whereIn('name', ['service_advisor', 'service_supervisor']);
})
->where('status', 'active')
->orderBy('name')
->pluck('name', 'id')
->toArray();
$dateRangeOptions = [
'today' => 'Today',
'week' => 'This Week',
'month' => 'This Month',
'overdue' => 'Overdue',
];
return view('livewire.job-cards.index', compact(
'jobCards',
'statusOptions',
'priorityOptions',
'branchOptions',
'serviceAdvisorOptions',
'dateRangeOptions'
))->with([
'statistics' => $this->statistics,
'selectedJobCards' => $this->selectedJobCards ?? [],
'selectAll' => $this->selectAll ?? false,
'bulkAction' => $this->bulkAction ?? '',
'search' => $this->search ?? '',
'statusFilter' => $this->statusFilter ?? '',
'branchFilter' => $this->branchFilter ?? '',
'priorityFilter' => $this->priorityFilter ?? '',
'serviceAdvisorFilter' => $this->serviceAdvisorFilter ?? '',
'dateRange' => $this->dateRange ?? '',
'sortBy' => $this->sortBy ?? 'created_at',
'sortDirection' => $this->sortDirection ?? 'desc'
]);
} catch (\Exception $e) {
logger()->error('Error rendering JobCard Index: ' . $e->getMessage());
// Provide fallback data
$jobCards = collect()->paginate(20);
$statusOptions = [];
$priorityOptions = [];
$branchOptions = [];
$serviceAdvisorOptions = [];
$dateRangeOptions = [];
$statistics = $this->statistics ?? [];
session()->flash('error', 'There was an error loading the job cards. Please try again.');
return view('livewire.job-cards.index', compact(
'jobCards',
'statusOptions',
'priorityOptions',
'branchOptions',
'serviceAdvisorOptions',
'dateRangeOptions'
))->with([
'statistics' => $statistics,
'selectedJobCards' => $this->selectedJobCards ?? [],
'selectAll' => $this->selectAll ?? false,
'bulkAction' => $this->bulkAction ?? '',
'search' => $this->search ?? '',
'statusFilter' => $this->statusFilter ?? '',
'branchFilter' => $this->branchFilter ?? '',
'priorityFilter' => $this->priorityFilter ?? '',
'serviceAdvisorFilter' => $this->serviceAdvisorFilter ?? '',
'dateRange' => $this->dateRange ?? ''
]);
}
}
/**
* Handle the component invocation for route compatibility
*/
public function __invoke()
{
return $this->render();
}
}