'General Inspection', 'electrical_diagnosis' => 'Electrical Diagnosis', 'engine_diagnosis' => 'Engine Diagnosis', 'transmission_diagnosis' => 'Transmission Diagnosis', 'brake_diagnosis' => 'Brake System Diagnosis', 'suspension_diagnosis' => 'Suspension Diagnosis', 'air_conditioning' => 'Air Conditioning Diagnosis', 'computer_diagnosis' => 'Computer/ECU Diagnosis', 'emissions_diagnosis' => 'Emissions Diagnosis', 'noise_diagnosis' => 'Noise/Vibration Diagnosis', ]; // Parts integration public $availableParts = []; public $partSearch = ''; public $partSearchTerm = ''; public $partCategoryFilter = ''; public $filteredParts; // Service items integration public $availableServiceItems = []; public $serviceItemSearch = ''; public $serviceSearchTerm = ''; public $serviceCategoryFilter = ''; public $filteredServiceItems; // UI state public $showPartsSection = false; public $showLaborSection = false; public $showDiagnosticCodesSection = false; public $showTestResultsSection = false; public $showAdvancedOptions = false; public $showTimesheetSection = true; public $createEstimateAutomatically = true; protected $rules = [ 'customer_reported_issues' => 'required|string', 'diagnostic_findings' => 'required|string|min:20', 'root_cause_analysis' => 'required|string|min:20', 'recommended_repairs' => 'required|string|min:10', 'priority_level' => 'required|in:low,medium,high,urgent', 'estimated_repair_time' => 'required|numeric|min:0.5|max:40', 'safety_concerns' => 'nullable|string', 'environmental_impact' => 'nullable|string', 'notes' => 'nullable|string', 'photos.*' => 'nullable|image|max:5120', 'selectedDiagnosisType' => 'required|string', ]; protected $messages = [ 'diagnostic_findings.min' => 'Please provide detailed diagnostic findings (at least 20 characters).', 'root_cause_analysis.min' => 'Please provide a thorough root cause analysis (at least 20 characters).', 'recommended_repairs.min' => 'Please specify the recommended repairs (at least 10 characters).', 'estimated_repair_time.max' => 'Estimated repair time cannot exceed 40 hours. For longer repairs, consider breaking into phases.', 'photos.*.max' => 'Each photo must be less than 5MB.', 'selectedDiagnosisType.required' => 'Please select a diagnosis type.', ]; public function mount(JobCard $jobCard) { // Validate that initial inspection has been completed if ($jobCard->status === 'received') { session()->flash('error', 'Initial vehicle inspection must be completed before starting diagnosis. Please complete the inspection first.'); return redirect()->route('job-cards.show', $jobCard); } if (! $jobCard->incomingInspection) { session()->flash('error', 'No incoming inspection record found. Please complete the initial inspection before proceeding to diagnosis.'); return redirect()->route('inspections.create', ['jobCard' => $jobCard, 'type' => 'incoming']); } $this->jobCard = $jobCard->load(['customer', 'vehicle', 'timesheets', 'incomingInspection']); $this->customer_reported_issues = $jobCard->customer_reported_issues ?? ''; // Initialize arrays to prevent null issues - load from session if available $this->parts_required = session()->get("diagnosis_parts_{$jobCard->id}", []); $this->labor_operations = session()->get("diagnosis_labor_{$jobCard->id}", []); $this->timesheets = []; $this->diagnostic_codes = session()->get("diagnosis_codes_{$jobCard->id}", []); $this->test_results = []; $this->special_tools_required = []; // Initialize filtered collections $this->filteredParts = collect(); $this->filteredServiceItems = collect(); // Load existing timesheets for this job card related to diagnosis $this->loadTimesheets(); // Load available parts and service items $this->loadAvailableParts(); $this->loadAvailableServiceItems(); // Initialize with one empty part and labor operation for convenience if none exist if (empty($this->parts_required)) { $this->addPart(); } if (empty($this->labor_operations)) { $this->addLaborOperation(); } } public function updatedPartsRequired() { // Save parts to session whenever they change session()->put("diagnosis_parts_{$this->jobCard->id}", $this->parts_required); } public function updatedLaborOperations() { // Save labor operations to session whenever they change session()->put("diagnosis_labor_{$this->jobCard->id}", $this->labor_operations); } public function updatedDiagnosticCodes() { // Save diagnostic codes to session whenever they change session()->put("diagnosis_codes_{$this->jobCard->id}", $this->diagnostic_codes); } public function loadTimesheets() { $this->timesheets = Timesheet::where('job_card_id', $this->jobCard->id) ->where('entry_type', 'manual') ->where('description', 'like', '%diagnosis%') ->with('user') ->orderBy('created_at', 'desc') ->get() ->toArray(); } public function loadAvailableParts() { $query = Part::where('status', 'active'); if (! empty($this->partSearch)) { $query->where(function ($q) { $q->where('name', 'like', '%'.$this->partSearch.'%') ->orWhere('part_number', 'like', '%'.$this->partSearch.'%') ->orWhere('description', 'like', '%'.$this->partSearch.'%'); }); } $this->availableParts = $query->limit(20)->get()->toArray(); } public function loadAvailableServiceItems() { $query = ServiceItem::query(); if (! empty($this->serviceItemSearch)) { $query->where(function ($q) { $q->where('service_name', 'like', '%'.$this->serviceItemSearch.'%') ->orWhere('description', 'like', '%'.$this->serviceItemSearch.'%') ->orWhere('category', 'like', '%'.$this->serviceItemSearch.'%'); }); } $this->availableServiceItems = $query->limit(20)->get()->toArray(); } // Computed properties for filtered collections public function updatedPartSearchTerm() { $this->updateFilteredParts(); } public function updatedPartCategoryFilter() { $this->updateFilteredParts(); } public function updateFilteredParts() { try { // If no search criteria provided, return empty collection if (empty($this->partSearchTerm) && empty($this->partCategoryFilter)) { $this->filteredParts = collect(); return; } // Start with active parts only $query = Part::where('status', 'active'); // Add search term filter if provided if (! empty($this->partSearchTerm)) { $searchTerm = trim($this->partSearchTerm); $query->where(function ($q) use ($searchTerm) { $q->where('name', 'like', '%'.$searchTerm.'%') ->orWhere('part_number', 'like', '%'.$searchTerm.'%') ->orWhere('description', 'like', '%'.$searchTerm.'%') ->orWhere('manufacturer', 'like', '%'.$searchTerm.'%'); }); } // Add category filter if provided if (! empty($this->partCategoryFilter)) { $query->where('category', $this->partCategoryFilter); } // Order by name for consistent results $query->orderBy('name'); // Get results and assign to property $this->filteredParts = $query->limit(20)->get(); // Log for debugging \Log::info('Parts search executed', [ 'search_term' => $this->partSearchTerm, 'category_filter' => $this->partCategoryFilter, 'results_count' => $this->filteredParts->count(), 'results' => $this->filteredParts->pluck('name')->toArray(), ]); } catch (\Exception $e) { // Log error and return empty collection \Log::error('Error in updateFilteredParts', [ 'error' => $e->getMessage(), 'search_term' => $this->partSearchTerm, 'category_filter' => $this->partCategoryFilter, ]); $this->filteredParts = collect(); } } public function getFilteredServiceItemsProperty() { $query = ServiceItem::query(); if (! empty($this->serviceSearchTerm)) { $query->where(function ($q) { $q->where('name', 'like', '%'.$this->serviceSearchTerm.'%') ->orWhere('description', 'like', '%'.$this->serviceSearchTerm.'%'); }); } if (! empty($this->serviceCategoryFilter)) { $query->where('category', $this->serviceCategoryFilter); } return $query->limit(20)->get(); } public function updatedPartSearch() { $this->loadAvailableParts(); } public function updatedServiceSearchTerm() { $this->updateFilteredServiceItems(); } public function updatedServiceCategoryFilter() { $this->updateFilteredServiceItems(); } public function updateFilteredServiceItems() { try { // If no search criteria provided, return empty collection if (empty($this->serviceSearchTerm) && empty($this->serviceCategoryFilter)) { $this->filteredServiceItems = collect(); return; } // Start with active service items only $query = ServiceItem::where('status', 'active'); // Add search term filter if provided if (! empty($this->serviceSearchTerm)) { $searchTerm = trim($this->serviceSearchTerm); $query->where(function ($q) use ($searchTerm) { $q->where('service_name', 'like', '%'.$searchTerm.'%') ->orWhere('description', 'like', '%'.$searchTerm.'%'); }); } // Add category filter if provided if (! empty($this->serviceCategoryFilter)) { $query->where('category', $this->serviceCategoryFilter); } // Order by name for consistent results $query->orderBy('service_name'); // Get results $results = $query->limit(20)->get(); $this->filteredServiceItems = $results; } catch (\Exception $e) { // Log error and return empty collection \Log::error('Error in updateFilteredServiceItems', [ 'error' => $e->getMessage(), 'search_term' => $this->serviceSearchTerm, 'category_filter' => $this->serviceCategoryFilter, ]); $this->filteredServiceItems = collect(); } } public function startTimesheet() { // End any currently running timesheet if ($this->currentTimesheet) { $this->endTimesheet(); } $this->currentTimesheet = Timesheet::create([ 'job_card_id' => $this->jobCard->id, 'user_id' => auth()->id(), 'entry_type' => 'manual', 'description' => 'Diagnosis: '.($this->diagnosisTypes[$this->selectedDiagnosisType] ?? 'General Diagnosis'), 'date' => now()->toDateString(), 'start_time' => now(), 'hourly_rate' => auth()->user()->hourly_rate ?? 85.00, 'status' => 'draft', ]); $this->loadTimesheets(); session()->flash('timesheet_message', 'Timesheet started for '.($this->diagnosisTypes[$this->selectedDiagnosisType] ?? 'Diagnosis')); } public function endTimesheet() { if (! $this->currentTimesheet) { return; } $timesheet = Timesheet::find($this->currentTimesheet['id']); if ($timesheet && ! $timesheet->end_time) { $endTime = now(); $totalMinutes = $timesheet->start_time->diffInMinutes($endTime); $billableHours = round($totalMinutes / 60, 2); $timesheet->update([ 'end_time' => $endTime, 'hours_worked' => $billableHours, 'billable_hours' => $billableHours, 'total_amount' => $billableHours * $timesheet->hourly_rate, 'status' => 'submitted', ]); session()->flash('timesheet_message', 'Timesheet ended. Total time: '.$billableHours.' hours'); } $this->currentTimesheet = null; $this->loadTimesheets(); } public function addPartFromCatalog($partId) { $part = Part::find($partId); if ($part) { $this->parts_required[] = [ 'part_id' => $part->id, 'part_name' => $part->name, 'part_number' => $part->part_number, 'quantity' => 1, 'estimated_cost' => $part->sell_price, 'availability' => $part->quantity_on_hand > 0 ? 'in_stock' : 'out_of_stock', ]; $this->updatedPartsRequired(); // Save to session } } public function addServiceItemFromCatalog($serviceItemId) { $serviceItem = ServiceItem::find($serviceItemId); if ($serviceItem) { $this->labor_operations[] = [ 'service_item_id' => $serviceItem->id, 'operation' => $serviceItem->service_name, 'description' => $serviceItem->description, 'estimated_hours' => $serviceItem->estimated_hours, 'labor_rate' => $serviceItem->labor_rate, 'category' => $serviceItem->category, 'complexity' => 'medium', ]; $this->updatedLaborOperations(); // Save to session } } public function addPart() { $this->parts_required[] = [ 'part_id' => null, 'part_name' => '', 'part_number' => '', 'quantity' => 1, 'estimated_cost' => 0, 'availability' => 'in_stock', ]; $this->updatedPartsRequired(); // Save to session } public function removePart($index) { unset($this->parts_required[$index]); $this->parts_required = array_values($this->parts_required); $this->updatedPartsRequired(); // Save to session } public function addLaborOperation() { $this->labor_operations[] = [ 'service_item_id' => null, 'operation' => '', 'description' => '', 'estimated_hours' => 0, 'labor_rate' => 85.00, 'category' => '', 'complexity' => 'medium', ]; $this->updatedLaborOperations(); // Save to session } public function removeLaborOperation($index) { unset($this->labor_operations[$index]); $this->labor_operations = array_values($this->labor_operations); $this->updatedLaborOperations(); // Save to session } public function saveProgress() { // Manually save current progress to session $this->updatedPartsRequired(); $this->updatedLaborOperations(); $this->updatedDiagnosticCodes(); session()->flash('progress_saved', 'Progress saved successfully!'); } public function addDiagnosticCode() { $this->diagnostic_codes[] = [ 'code' => '', 'description' => '', 'system' => '', 'severity' => 'medium', ]; } public function removeDiagnosticCode($index) { unset($this->diagnostic_codes[$index]); $this->diagnostic_codes = array_values($this->diagnostic_codes); } public function addTestResult() { $this->test_results[] = [ 'test_name' => '', 'result' => '', 'specification' => '', 'status' => 'pass', ]; } public function removeTestResult($index) { unset($this->test_results[$index]); $this->test_results = array_values($this->test_results); } public function addSpecialTool() { $this->special_tools_required[] = [ 'tool_name' => '', 'tool_type' => '', 'availability' => 'available', ]; } public function removeSpecialTool($index) { unset($this->special_tools_required[$index]); $this->special_tools_required = array_values($this->special_tools_required); } public function togglePartsSection() { $this->showPartsSection = ! $this->showPartsSection; } public function toggleLaborSection() { $this->showLaborSection = ! $this->showLaborSection; } public function toggleDiagnosticCodesSection() { $this->showDiagnosticCodesSection = ! $this->showDiagnosticCodesSection; } public function toggleTestResultsSection() { $this->showTestResultsSection = ! $this->showTestResultsSection; } public function toggleAdvancedOptions() { $this->showAdvancedOptions = ! $this->showAdvancedOptions; } public function toggleTimesheetSection() { $this->showTimesheetSection = ! $this->showTimesheetSection; } public function calculateTotalEstimatedCost() { $partsCost = array_sum(array_map(function ($part) { return ($part['quantity'] ?? 1) * ($part['estimated_cost'] ?? 0); }, $this->parts_required)); $laborCost = 0; foreach ($this->labor_operations as $operation) { $laborCost += ($operation['estimated_hours'] ?? 0) * ($operation['labor_rate'] ?? 85); } // Include diagnostic time costs $diagnosticCost = collect($this->timesheets)->sum('total_amount'); return $partsCost + $laborCost + $diagnosticCost; } private function createEstimateFromDiagnosis($diagnosis) { // Calculate totals $partsCost = array_sum(array_map(function ($part) { return ($part['quantity'] ?? 1) * ($part['estimated_cost'] ?? 0); }, $this->parts_required)); $laborCost = 0; foreach ($this->labor_operations as $operation) { $laborCost += ($operation['estimated_hours'] ?? 0) * ($operation['labor_rate'] ?? 85); } $subtotal = $partsCost + $laborCost; $taxRate = 0.0875; // 8.75% tax rate - should be configurable $taxAmount = $subtotal * $taxRate; $totalAmount = $subtotal + $taxAmount; // Create the estimate $estimate = Estimate::create([ 'job_card_id' => $this->jobCard->id, 'diagnosis_id' => $diagnosis->id, 'estimate_number' => 'EST-'.str_pad(Estimate::max('id') + 1, 6, '0', STR_PAD_LEFT), 'customer_id' => $this->jobCard->customer_id, 'vehicle_id' => $this->jobCard->vehicle_id, 'prepared_by_id' => auth()->id(), 'status' => 'draft', 'priority_level' => $this->priority_level, 'estimated_completion_date' => now()->addHours($this->estimated_repair_time), 'subtotal' => $subtotal, 'tax_rate' => $taxRate, 'tax_amount' => $taxAmount, 'total_amount' => $totalAmount, 'notes' => 'Auto-generated from diagnosis: '.$diagnosis->id, 'validity_period_days' => 30, ]); // Create line items for parts foreach ($this->parts_required as $part) { if (! empty($part['part_name'])) { EstimateLineItem::create([ 'estimate_id' => $estimate->id, 'type' => 'part', 'part_id' => $part['part_id'] ?? null, 'description' => $part['part_name'], 'part_number' => $part['part_number'] ?? null, 'quantity' => $part['quantity'] ?? 1, 'unit_price' => $part['estimated_cost'] ?? 0, 'total_price' => ($part['quantity'] ?? 1) * ($part['estimated_cost'] ?? 0), ]); } } // Create line items for labor foreach ($this->labor_operations as $operation) { if (! empty($operation['operation'])) { EstimateLineItem::create([ 'estimate_id' => $estimate->id, 'type' => 'labor', 'service_item_id' => $operation['service_item_id'] ?? null, 'description' => $operation['operation'], 'labor_hours' => $operation['estimated_hours'] ?? 0, 'labor_rate' => $operation['labor_rate'] ?? 85, 'total_price' => ($operation['estimated_hours'] ?? 0) * ($operation['labor_rate'] ?? 85), ]); } } return $estimate; } public function save() { $this->validate(); // End any active timesheet if ($this->currentTimesheet) { $this->endTimesheet(); } // Handle photo uploads $photoUrls = []; if ($this->photos) { foreach ($this->photos as $photo) { $photoUrls[] = $photo->store('diagnosis', 'public'); } } $diagnosis = Diagnosis::create([ 'job_card_id' => $this->jobCard->id, 'service_coordinator_id' => auth()->id(), 'customer_reported_issues' => $this->customer_reported_issues, 'diagnostic_findings' => $this->diagnostic_findings, 'root_cause_analysis' => $this->root_cause_analysis, 'recommended_repairs' => $this->recommended_repairs, 'additional_issues_found' => $this->additional_issues_found, 'priority_level' => $this->priority_level, 'estimated_repair_time' => $this->estimated_repair_time, 'parts_required' => array_filter($this->parts_required, function ($part) { return ! empty($part['part_name']); }), 'labor_operations' => array_filter($this->labor_operations, function ($operation) { return ! empty($operation['operation']); }), 'special_tools_required' => array_filter($this->special_tools_required, function ($tool) { return ! empty($tool['tool_name']); }), 'safety_concerns' => $this->safety_concerns, 'diagnostic_codes' => array_filter($this->diagnostic_codes, function ($code) { return ! empty($code['code']); }), 'test_results' => array_filter($this->test_results, function ($result) { return ! empty($result['test_name']); }), 'photos' => $photoUrls, 'notes' => $this->notes, 'environmental_impact' => $this->environmental_impact, 'customer_authorization_required' => $this->customer_authorization_required, 'diagnosis_status' => 'completed', 'diagnosis_date' => now(), ]); // Update job card status $this->jobCard->update(['status' => 'diagnosis_completed']); // Create estimate automatically $estimate = $this->createEstimateFromDiagnosis($diagnosis); // Clear session data after successful diagnosis creation session()->forget([ "diagnosis_parts_{$this->jobCard->id}", "diagnosis_labor_{$this->jobCard->id}", "diagnosis_codes_{$this->jobCard->id}", ]); session()->flash('message', 'Diagnosis completed successfully! Estimate #'.$estimate->estimate_number.' has been created automatically.'); return redirect()->route('estimates.show', $estimate); } public function render() { return view('livewire.diagnosis.create'); } }