- 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.
424 lines
16 KiB
PHP
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;
|
|
}
|
|
}
|