- 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.
515 lines
18 KiB
PHP
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();
|
|
}
|
|
}
|