Car-Repairs-Shop/app/Livewire/Reports/WorkflowAnalytics.php
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

242 lines
9.0 KiB
PHP

<?php
namespace App\Livewire\Reports;
use Livewire\Component;
use App\Models\JobCard;
use App\Models\Branch;
use App\Models\Timesheet;
use App\Models\Part;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class WorkflowAnalytics extends Component
{
public $selectedBranch = '';
public $dateRange = '30';
public $reportData = [];
public function mount()
{
$this->generateReport();
}
public function updatedSelectedBranch()
{
$this->generateReport();
}
public function updatedDateRange()
{
$this->generateReport();
}
public function generateReport()
{
$startDate = Carbon::now()->subDays($this->dateRange);
$endDate = Carbon::now();
$this->reportData = [
'revenue_by_branch' => $this->getRevenueByBranch($startDate, $endDate),
'labor_utilization' => $this->getLaborUtilization($startDate, $endDate),
'parts_usage' => $this->getPartsUsage($startDate, $endDate),
'customer_approval_trends' => $this->getCustomerApprovalTrends($startDate, $endDate),
'turnaround_times' => $this->getTurnaroundTimes($startDate, $endDate),
'workflow_bottlenecks' => $this->getWorkflowBottlenecks($startDate, $endDate),
'quality_metrics' => $this->getQualityMetrics($startDate, $endDate),
];
}
private function getRevenueByBranch($startDate, $endDate): array
{
$query = JobCard::with('estimates')
->whereBetween('completion_datetime', [$startDate, $endDate])
->where('status', 'delivered');
if ($this->selectedBranch) {
$query->where('branch_code', $this->selectedBranch);
}
return $query->get()
->groupBy('branch_code')
->map(function ($jobs, $branchCode) {
$totalRevenue = $jobs->sum(function ($job) {
return $job->estimates->where('status', 'approved')->sum('total_amount');
});
return [
'branch' => $branchCode,
'jobs_completed' => $jobs->count(),
'total_revenue' => $totalRevenue,
'average_job_value' => $jobs->count() > 0 ? $totalRevenue / $jobs->count() : 0,
];
})
->values()
->toArray();
}
private function getLaborUtilization($startDate, $endDate): array
{
$query = Timesheet::with('technician')
->whereBetween('date', [$startDate, $endDate]);
if ($this->selectedBranch) {
$query->whereHas('technician', function ($q) {
$q->where('branch_code', $this->selectedBranch);
});
}
$timesheets = $query->get();
return $timesheets->groupBy('technician.name')
->map(function ($entries, $technicianName) {
$totalHours = $entries->sum('hours_worked');
$billableHours = $entries->sum('billable_hours');
return [
'technician' => $technicianName,
'total_hours' => $totalHours,
'billable_hours' => $billableHours,
'utilization_rate' => $totalHours > 0 ? ($billableHours / $totalHours) * 100 : 0,
];
})
->values()
->toArray();
}
private function getPartsUsage($startDate, $endDate): array
{
return DB::table('estimate_line_items')
->join('estimates', 'estimate_line_items.estimate_id', '=', 'estimates.id')
->join('job_cards', 'estimates.job_card_id', '=', 'job_cards.id')
->join('parts', 'estimate_line_items.part_id', '=', 'parts.id')
->whereBetween('job_cards.completion_datetime', [$startDate, $endDate])
->where('estimates.status', 'approved')
->where('estimate_line_items.type', 'part')
->when($this->selectedBranch, function ($query) {
return $query->where('job_cards.branch_code', $this->selectedBranch);
})
->select(
'parts.part_number',
'parts.name',
DB::raw('SUM(estimate_line_items.quantity) as total_used'),
DB::raw('SUM(estimate_line_items.total_price) as total_value'),
'parts.current_stock'
)
->groupBy('parts.id', 'parts.part_number', 'parts.name', 'parts.current_stock')
->orderBy('total_used', 'desc')
->limit(20)
->get()
->toArray();
}
private function getCustomerApprovalTrends($startDate, $endDate): array
{
$estimates = DB::table('estimates')
->join('job_cards', 'estimates.job_card_id', '=', 'job_cards.id')
->whereBetween('estimates.created_at', [$startDate, $endDate])
->when($this->selectedBranch, function ($query) {
return $query->where('job_cards.branch_code', $this->selectedBranch);
})
->select('estimates.status', DB::raw('COUNT(*) as count'))
->groupBy('estimates.status')
->get();
$total = $estimates->sum('count');
return [
'total_estimates' => $total,
'approval_rate' => $total > 0 ? ($estimates->where('status', 'approved')->first()?->count ?? 0) / $total * 100 : 0,
'rejection_rate' => $total > 0 ? ($estimates->where('status', 'rejected')->first()?->count ?? 0) / $total * 100 : 0,
'pending_rate' => $total > 0 ? ($estimates->where('status', 'sent')->first()?->count ?? 0) / $total * 100 : 0,
];
}
private function getTurnaroundTimes($startDate, $endDate): array
{
$jobs = JobCard::whereBetween('completion_datetime', [$startDate, $endDate])
->where('status', 'delivered')
->when($this->selectedBranch, function ($query) {
return $query->where('branch_code', $this->selectedBranch);
})
->get();
$turnaroundTimes = $jobs->map(function ($job) {
return $job->completion_datetime->diffInHours($job->arrival_datetime);
});
return [
'average_turnaround' => $turnaroundTimes->avg(),
'median_turnaround' => $turnaroundTimes->median(),
'min_turnaround' => $turnaroundTimes->min(),
'max_turnaround' => $turnaroundTimes->max(),
'total_jobs' => $jobs->count(),
];
}
private function getWorkflowBottlenecks($startDate, $endDate): array
{
$statusCounts = JobCard::whereBetween('created_at', [$startDate, $endDate])
->when($this->selectedBranch, function ($query) {
return $query->where('branch_code', $this->selectedBranch);
})
->select('status', DB::raw('COUNT(*) as count'))
->groupBy('status')
->get()
->pluck('count', 'status')
->toArray();
// Calculate average time in each status
$avgTimeInStatus = [];
foreach (JobCard::getStatusOptions() as $status => $label) {
$avgTimeInStatus[$status] = $this->getAverageTimeInStatus($status, $startDate, $endDate);
}
return [
'status_counts' => $statusCounts,
'average_time_in_status' => $avgTimeInStatus,
];
}
private function getAverageTimeInStatus($status, $startDate, $endDate): float
{
// This would require status change tracking - simplified for now
return JobCard::where('status', $status)
->whereBetween('updated_at', [$startDate, $endDate])
->when($this->selectedBranch, function ($query) {
return $query->where('branch_code', $this->selectedBranch);
})
->avg(DB::raw('TIMESTAMPDIFF(HOUR, created_at, updated_at)')) ?? 0;
}
private function getQualityMetrics($startDate, $endDate): array
{
$inspections = DB::table('vehicle_inspections')
->join('job_cards', 'vehicle_inspections.job_card_id', '=', 'job_cards.id')
->whereBetween('vehicle_inspections.inspection_date', [$startDate, $endDate])
->when($this->selectedBranch, function ($query) {
return $query->where('job_cards.branch_code', $this->selectedBranch);
})
->get();
$totalInspections = $inspections->count();
$discrepancyCount = $inspections->where('follow_up_required', true)->count();
return [
'total_inspections' => $totalInspections,
'discrepancy_rate' => $totalInspections > 0 ? ($discrepancyCount / $totalInspections) * 100 : 0,
'quality_score' => $totalInspections > 0 ? (($totalInspections - $discrepancyCount) / $totalInspections) * 100 : 100,
];
}
public function render()
{
$branches = Branch::active()->get();
return view('livewire.reports.workflow-analytics', [
'branches' => $branches,
'reportData' => $this->reportData,
]);
}
}