'required|string', 'validity_period_days' => 'required|integer|min:1|max:365', 'tax_rate' => 'required|numeric|min:0|max:50', 'discount_amount' => 'nullable|numeric|min:0', 'lineItems.*.type' => 'required|in:labor,parts,miscellaneous', 'lineItems.*.description' => 'required|string', 'lineItems.*.quantity' => 'required|numeric|min:0.01', 'lineItems.*.unit_price' => 'required|numeric|min:0', ]; public function mount(Diagnosis $diagnosis) { $this->diagnosis = $diagnosis->load([ 'jobCard.customer', 'jobCard.vehicle', ]); // Pre-populate from diagnosis $this->initializeLineItems(); $this->terms_and_conditions = config('app.default_estimate_terms', 'This estimate is valid for 30 days. All work will be performed according to industry standards.' ); } public function initializeLineItems() { // Add labor operations from diagnosis if ($this->diagnosis->labor_operations) { foreach ($this->diagnosis->labor_operations as $labor) { $this->lineItems[] = [ 'type' => 'labor', 'description' => $labor['operation'] ?? 'Labor Operation', 'quantity' => $labor['estimated_hours'] ?? 1, 'unit_price' => $labor['labor_rate'] ?? 75, 'total_amount' => ($labor['estimated_hours'] ?? 1) * ($labor['labor_rate'] ?? 75), 'labor_hours' => $labor['estimated_hours'] ?? 1, 'labor_rate' => $labor['labor_rate'] ?? 75, 'required' => true, ]; } } // Add parts from diagnosis if ($this->diagnosis->parts_required) { foreach ($this->diagnosis->parts_required as $part) { $this->lineItems[] = [ 'type' => 'parts', 'part_id' => null, 'description' => ($part['part_name'] ?? 'Part').' ('.($part['part_number'] ?? 'N/A').')', 'quantity' => $part['quantity'] ?? 1, 'unit_price' => $part['estimated_cost'] ?? 0, 'total_amount' => ($part['quantity'] ?? 1) * ($part['estimated_cost'] ?? 0), 'markup_percentage' => 20, 'required' => true, ]; } } // If no line items from diagnosis, add a default labor item if (empty($this->lineItems)) { $this->lineItems[] = [ 'type' => 'labor', 'description' => 'Diagnostic and Repair Services', 'quantity' => $this->diagnosis->estimated_repair_time ?? 1, 'unit_price' => 75, 'total_amount' => ($this->diagnosis->estimated_repair_time ?? 1) * 75, 'labor_hours' => $this->diagnosis->estimated_repair_time ?? 1, 'labor_rate' => 75, 'required' => true, ]; } $this->calculateTotals(); } public function addLineItem() { $this->lineItems[] = [ 'type' => 'labor', 'description' => '', 'quantity' => 1, 'unit_price' => 0, 'total_amount' => 0, 'required' => true, ]; } public function removeLineItem($index) { unset($this->lineItems[$index]); $this->lineItems = array_values($this->lineItems); $this->calculateTotals(); } public function updatedLineItems() { $this->calculateTotals(); } public function calculateTotals() { $this->subtotal = collect($this->lineItems)->sum(function ($item) { return ($item['quantity'] ?? 0) * ($item['unit_price'] ?? 0); }); $this->tax_amount = ($this->subtotal - $this->discount_amount) * ($this->tax_rate / 100); $this->total_amount = $this->subtotal - $this->discount_amount + $this->tax_amount; // Update individual line item totals foreach ($this->lineItems as $index => &$item) { $item['total_amount'] = ($item['quantity'] ?? 0) * ($item['unit_price'] ?? 0); } } public function save() { $this->validate(); $this->calculateTotals(); // Generate estimate number $branchCode = $this->diagnosis->jobCard->branch_code; // Use database lock to prevent race conditions $maxNumber = \DB::transaction(function () use ($branchCode) { $lastEstimate = Estimate::where('estimate_number', 'like', $branchCode.'/EST%') ->whereYear('created_at', now()->year) ->orderByRaw('CAST(SUBSTRING(estimate_number, '.(strlen($branchCode) + 5).') AS UNSIGNED) DESC') ->lockForUpdate() ->first(); $nextNumber = 1; if ($lastEstimate) { // Extract the number part from the estimate number (e.g., MAIN/EST0001 -> 1) preg_match('/'.preg_quote($branchCode).'\/EST(\d+)/', $lastEstimate->estimate_number, $matches); $nextNumber = isset($matches[1]) ? intval($matches[1]) + 1 : 1; } return $nextNumber; }); $estimateNumber = $branchCode.'/EST'.str_pad($maxNumber, 4, '0', STR_PAD_LEFT); $estimate = Estimate::create([ 'estimate_number' => $estimateNumber, 'job_card_id' => $this->diagnosis->job_card_id, 'diagnosis_id' => $this->diagnosis->id, 'prepared_by_id' => auth()->id(), 'labor_cost' => collect($this->lineItems)->where('type', 'labor')->sum('total_amount'), 'parts_cost' => collect($this->lineItems)->where('type', 'parts')->sum('total_amount'), 'miscellaneous_cost' => collect($this->lineItems)->where('type', 'miscellaneous')->sum('total_amount'), 'subtotal' => $this->subtotal, 'tax_rate' => $this->tax_rate, 'tax_amount' => $this->tax_amount, 'discount_amount' => $this->discount_amount, 'total_amount' => $this->total_amount, 'validity_period_days' => $this->validity_period_days, 'terms_and_conditions' => $this->terms_and_conditions, 'notes' => $this->notes, 'internal_notes' => $this->internal_notes, 'status' => 'draft', ]); // Create line items foreach ($this->lineItems as $item) { EstimateLineItem::create([ 'estimate_id' => $estimate->id, 'type' => $item['type'], 'part_id' => $item['part_id'] ?? null, 'description' => $item['description'], 'quantity' => $item['quantity'], 'unit_price' => $item['unit_price'], 'total_amount' => $item['total_amount'], 'labor_hours' => $item['labor_hours'] ?? null, 'labor_rate' => $item['labor_rate'] ?? null, 'markup_percentage' => $item['markup_percentage'] ?? 0, 'required' => $item['required'] ?? true, ]); } // Update job card status $this->diagnosis->jobCard->update(['status' => 'estimate_prepared']); session()->flash('message', 'Estimate created successfully!'); return redirect()->route('estimates.show', $estimate); } public function sendToCustomer() { // This would be called after saving $customer = $this->diagnosis->jobCard->customer; $customer->notify(new EstimateNotification($estimate)); session()->flash('message', 'Estimate sent to customer successfully!'); } public function render() { return view('livewire.estimates.create'); } }