- 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.
242 lines
9.0 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|