Car-Repairs-Shop/app/Services/WorkflowService.php
sackey 5403c3591d
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
feat: Enhance job card workflow with diagnosis actions and technician assignment modal
- Added buttons for assigning diagnosis and starting diagnosis based on job card status in the job card view.
- Implemented a modal for assigning technicians for diagnosis, including form validation and technician selection.
- Updated routes to include a test route for job cards.
- Created a new Blade view for testing inspection inputs.
- Developed comprehensive feature tests for the estimate module, including creation, viewing, editing, and validation of estimates.
- Added tests for estimate model relationships and statistics calculations.
- Introduced a basic feature test for job cards index.
2025-08-15 08:37:45 +00:00

424 lines
16 KiB
PHP

<?php
namespace App\Services;
use App\Models\Diagnosis;
use App\Models\Estimate;
use App\Models\JobCard;
use App\Models\VehicleInspection;
use App\Models\WorkOrder;
use Illuminate\Support\Facades\DB;
class WorkflowService
{
public function __construct(
private NotificationService $notificationService,
private InspectionChecklistService $inspectionService
) {}
/**
* STEP 1: Vehicle Reception & Data Capture
* Create job card when vehicle arrives with full data capture
*/
public function createJobCard(array $data): JobCard
{
return DB::transaction(function () use ($data) {
$jobCard = JobCard::create([
'customer_id' => $data['customer_id'],
'vehicle_id' => $data['vehicle_id'],
'service_advisor_id' => $data['service_advisor_id'],
'branch_code' => $data['branch_code'] ?? config('app.default_branch_code', 'ACC'),
'arrival_datetime' => $data['arrival_datetime'] ?? now(),
'mileage_in' => $data['mileage_in'] ?? null,
'fuel_level_in' => $data['fuel_level_in'] ?? null,
'customer_reported_issues' => $data['customer_reported_issues'] ?? '',
'vehicle_condition_notes' => $data['vehicle_condition_notes'] ?? '',
'keys_location' => $data['keys_location'] ?? 'service_desk',
'personal_items_removed' => $data['personal_items_removed'] ?? false,
'photos_taken' => $data['photos_taken'] ?? false,
'expected_completion_date' => $data['expected_completion_date'] ?? null,
'priority' => $data['priority'] ?? 'medium',
'notes' => $data['notes'] ?? null,
'status' => 'received', // Initial status
]);
// STEP 2: Create incoming inspection checklist automatically
if (isset($data['inspection_checklist'])) {
$this->performInitialInspection($jobCard, $data, $data['inspector_id']);
}
return $jobCard;
});
}
/**
* STEP 2: Initial Inspection by Service Supervisor
* Perform arrival inspection checklist
*/
public function performInitialInspection(JobCard $jobCard, array $inspectionData, int $inspectorId): JobCard
{
// Update job card with inspection data
$jobCard->update([
'status' => JobCard::STATUS_INSPECTED,
'mileage_in' => $inspectionData['mileage_in'],
'fuel_level_in' => $inspectionData['fuel_level_in'],
'incoming_inspection_data' => $inspectionData['inspection_checklist'],
]);
// Send notification that inspection is complete and ready for diagnosis assignment
$this->notificationService->sendInspectionCompletedNotification($jobCard);
return $jobCard->fresh();
}
/**
* STEP 3: Assignment to Service Coordination
* Assign job card to service coordinator and start diagnosis
*/
public function assignToServiceCoordinator(JobCard $jobCard, int $serviceCoordinatorId): Diagnosis
{
// Validate workflow progression - must complete initial inspection first
if ($jobCard->status !== JobCard::STATUS_INSPECTED) {
throw new \InvalidArgumentException('Initial vehicle inspection must be completed before assignment to service coordinator');
}
// Ensure incoming inspection exists
if (! $jobCard->incomingInspection) {
throw new \InvalidArgumentException('Incoming inspection record is required before proceeding to diagnosis');
}
$diagnosis = Diagnosis::create([
'job_card_id' => $jobCard->id,
'service_coordinator_id' => $serviceCoordinatorId,
'customer_reported_issues' => $jobCard->customer_reported_issues,
'diagnosis_status' => 'in_progress',
'diagnosis_date' => now(),
]);
$jobCard->update(['status' => 'assigned_for_diagnosis']);
return $diagnosis;
}
/**
* STEP 4: Start Diagnostic Process with Timesheet Tracking
*/
public function startDiagnosisTimesheet(Diagnosis $diagnosis, int $technicianId): void
{
// Start timesheet for diagnosis
$timesheet = Timesheet::create([
'job_card_id' => $diagnosis->job_card_id,
'technician_id' => $technicianId,
'task_type' => 'diagnosis',
'start_time' => now(),
'description' => 'Diagnostic assessment',
'status' => 'in_progress',
]);
$diagnosis->update([
'diagnosis_status' => 'in_progress',
'assigned_technician_id' => $technicianId,
'started_at' => now(),
]);
}
/**
* STEP 5: Complete diagnosis and create estimate with customer notifications
*/
public function completeDiagnosis(Diagnosis $diagnosis, array $diagnosisData, array $estimateItems): Estimate
{
return DB::transaction(function () use ($diagnosis, $diagnosisData, $estimateItems) {
// Update diagnosis
$diagnosis->update([
'diagnostic_findings' => $diagnosisData['diagnostic_findings'],
'root_cause_analysis' => $diagnosisData['root_cause_analysis'],
'recommended_repairs' => $diagnosisData['recommended_repairs'],
'additional_issues_found' => $diagnosisData['additional_issues_found'] ?? null,
'priority_level' => $diagnosisData['priority_level'] ?? 'medium',
'estimated_repair_time' => $diagnosisData['estimated_repair_time'],
'parts_required' => $diagnosisData['parts_required'] ?? [],
'labor_operations' => $diagnosisData['labor_operations'] ?? [],
'special_tools_required' => $diagnosisData['special_tools_required'] ?? [],
'safety_concerns' => $diagnosisData['safety_concerns'] ?? null,
'customer_authorization_required' => $diagnosisData['customer_authorization_required'] ?? false,
'diagnosis_status' => 'completed',
'notes' => $diagnosisData['notes'] ?? null,
'completed_at' => now(),
]);
// Create estimate
$estimate = Estimate::create([
'job_card_id' => $diagnosis->job_card_id,
'diagnosis_id' => $diagnosis->id,
'estimate_number' => $this->generateEstimateNumber($diagnosis->jobCard->branch_code),
'prepared_by_id' => $diagnosis->service_coordinator_id,
'total_labor_cost' => $estimateItems['total_labor_cost'],
'total_parts_cost' => $estimateItems['total_parts_cost'],
'total_other_cost' => $estimateItems['total_other_cost'] ?? 0,
'tax_amount' => $estimateItems['tax_amount'],
'total_amount' => $estimateItems['total_amount'],
'status' => 'sent',
'notes' => $estimateItems['notes'] ?? null,
'validity_period_days' => $estimateItems['validity_period_days'] ?? 30,
]);
// Create estimate line items
foreach ($estimateItems['line_items'] as $item) {
$estimate->lineItems()->create($item);
}
// Update job card status
$diagnosis->jobCard->update(['status' => 'estimate_sent']);
// STEP 5: Send notifications to customer (email + SMS)
$this->notificationService->sendEstimateNotification($estimate);
return $estimate;
});
}
/**
* STEP 6: Handle estimate approval and notify team
*/
public function approveEstimate(Estimate $estimate, string $approvalMethod = 'portal'): WorkOrder
{
return DB::transaction(function () use ($estimate, $approvalMethod) {
// Update estimate
$estimate->update([
'customer_approval_status' => 'approved',
'customer_approved_at' => now(),
'customer_approval_method' => $approvalMethod,
'status' => 'approved',
]);
// Update job card
$estimate->jobCard->update(['status' => 'approved']);
// Notify team members about approval
$this->notificationService->notifyEstimateApproved($estimate);
// Create work order
return $this->createWorkOrder($estimate);
});
}
/**
* STEP 7: Parts Procurement & Inventory Management
*/
public function initiatePartsProcurement(Estimate $estimate): array
{
$procurementStatus = [];
foreach ($estimate->lineItems()->where('type', 'part')->get() as $item) {
// Check inventory availability
$part = Part::find($item->part_id);
if (! $part || $part->current_stock < $item->quantity) {
// Create purchase order if parts are out of stock
$procurementStatus[] = [
'part_id' => $item->part_id,
'part_name' => $item->description,
'required_quantity' => $item->quantity,
'available_stock' => $part->current_stock ?? 0,
'shortage' => $item->quantity - ($part->current_stock ?? 0),
'status' => 'procurement_required',
'action' => 'create_purchase_order',
];
} else {
// Reserve parts from inventory
$part->decrement('current_stock', $item->quantity);
$part->increment('reserved_stock', $item->quantity);
$procurementStatus[] = [
'part_id' => $item->part_id,
'part_name' => $item->description,
'required_quantity' => $item->quantity,
'status' => 'reserved',
'action' => 'reserved_from_stock',
];
}
}
return $procurementStatus;
}
/**
* Assign work order to technician and start work
*/
public function assignWorkOrder(WorkOrder $workOrder, int $technicianId): void
{
$workOrder->update([
'assigned_technician_id' => $technicianId,
'status' => 'assigned',
'actual_start_time' => now(),
]);
$workOrder->jobCard->update(['status' => 'in_progress']);
}
/**
* Complete work order and perform quality check
*/
public function completeWorkOrder(WorkOrder $workOrder, int $qualityCheckerId): void
{
$workOrder->update([
'status' => 'quality_check',
'actual_completion_time' => now(),
'quality_checked_by_id' => $qualityCheckerId,
'quality_check_date' => now(),
'completion_percentage' => 100,
]);
}
/**
* STEP 8: Final Inspection & Quality Assurance
* Perform outgoing inspection and final quality check
*/
public function performOutgoingInspection(JobCard $jobCard, array $inspectionData, int $inspectorId): void
{
// Create outgoing inspection
$outgoingInspection = VehicleInspection::create([
'job_card_id' => $jobCard->id,
'vehicle_id' => $jobCard->vehicle_id,
'inspector_id' => $inspectorId,
'inspection_type' => 'outgoing',
'current_mileage' => $inspectionData['mileage_out'],
'fuel_level' => $inspectionData['fuel_level_out'],
'inspection_checklist' => $inspectionData['inspection_checklist'],
'photos' => $inspectionData['photos'] ?? [],
'overall_condition' => $inspectionData['overall_condition'],
'inspection_date' => now(),
'notes' => $inspectionData['notes'] ?? null,
]);
// Compare with incoming inspection using service
$incomingInspection = $jobCard->incomingInspection;
if ($incomingInspection) {
$differences = $this->inspectionService->compareInspections($incomingInspection, $outgoingInspection);
if (! empty($differences)) {
// Generate quality alert for significant discrepancies
$qualityAlert = $this->inspectionService->generateQualityAlert($jobCard, $differences);
if (! empty($qualityAlert)) {
$this->notificationService->sendQualityAlert($jobCard, $differences);
$outgoingInspection->update([
'discrepancies_found' => $differences,
'follow_up_required' => true,
]);
$jobCard->update(['status' => 'quality_review_required']);
return;
}
}
}
// No discrepancies, mark as completed
$jobCard->update([
'status' => 'completed',
'mileage_out' => $inspectionData['mileage_out'],
'fuel_level_out' => $inspectionData['fuel_level_out'],
'completion_datetime' => now(),
]);
// Notify customer that vehicle is ready
$this->notificationService->notifyVehicleReady($jobCard);
}
/**
* STEP 9: Accounting & Invoicing
*/
public function generateFinalInvoice(JobCard $jobCard): array
{
$estimate = $jobCard->estimates()->where('status', 'approved')->first();
$actualLabor = $jobCard->timesheets()->sum('billable_hours');
$actualParts = $jobCard->workOrders()->with('usedParts')->get()
->flatMap->usedParts->sum('actual_cost');
$invoiceData = [
'job_card_id' => $jobCard->id,
'estimate_amount' => $estimate->total_amount,
'actual_labor_cost' => $actualLabor * $estimate->labor_rate,
'actual_parts_cost' => $actualParts,
'tax_amount' => ($actualLabor + $actualParts) * $estimate->tax_rate / 100,
'total_amount' => ($actualLabor + $actualParts) * (1 + $estimate->tax_rate / 100),
'variance_from_estimate' => null,
];
$invoiceData['variance_from_estimate'] = $invoiceData['total_amount'] - $estimate->total_amount;
return $invoiceData;
}
/**
* STEP 10: Vehicle Delivery & Job Closure
*/
public function closeJobCard(JobCard $jobCard, array $deliveryData): void
{
$jobCard->update([
'status' => 'delivered',
'delivery_method' => $deliveryData['delivery_method'],
'customer_satisfaction_rating' => $deliveryData['satisfaction_rating'] ?? null,
'completion_datetime' => now(),
'delivered_by_id' => $deliveryData['delivered_by_id'] ?? auth()->id(),
'delivery_notes' => $deliveryData['delivery_notes'] ?? null,
]);
// Archive all associated documents
$this->archiveJobCardDocuments($jobCard);
}
/**
* STEP 11: Archive job card documents
*/
private function archiveJobCardDocuments(JobCard $jobCard): void
{
// This would typically move documents to long-term storage
// For now, we'll just mark them as archived
$jobCard->update(['archived_at' => now()]);
// Update related records
$jobCard->inspections()->update(['archived' => true]);
$jobCard->timesheets()->update(['archived' => true]);
$jobCard->estimates()->update(['archived' => true]);
$jobCard->workOrders()->update(['archived' => true]);
}
/**
* Get workflow status for a job card
*/
public function getWorkflowStatus(JobCard $jobCard): array
{
return [
'job_card' => $jobCard,
'incoming_inspection' => $jobCard->incomingInspection,
'diagnosis' => $jobCard->diagnosis,
'estimate' => $jobCard->estimates()->latest()->first(),
'work_orders' => $jobCard->workOrders,
'outgoing_inspection' => $jobCard->outgoingInspection,
'completion_percentage' => $this->calculateCompletionPercentage($jobCard),
];
}
/**
* Calculate completion percentage for a job card
*/
private function calculateCompletionPercentage(JobCard $jobCard): int
{
$steps = [
'received' => 10,
'in_diagnosis' => 25,
'estimate_sent' => 40,
'approved' => 50,
'in_progress' => 75,
'quality_check' => 90,
'completed' => 95,
'delivered' => 100,
];
return $steps[$jobCard->status] ?? 0;
}
}