sackey e3b2b220d2
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
Enhance UI and functionality across various components
- Increased icon sizes in service items, service orders, users, and technician management for better visibility.
- Added custom loading indicators with appropriate icons in search fields for vehicles, work orders, and technicians.
- Introduced invoice management routes for better organization and access control.
- Created a new test for the estimate PDF functionality to ensure proper rendering and data integrity.
2025-08-16 14:36:58 +00:00

238 lines
8.4 KiB
PHP

<?php
namespace App\Livewire\Estimates;
use App\Models\Diagnosis;
use App\Models\Estimate;
use App\Models\EstimateLineItem;
use App\Models\Part;
use App\Notifications\EstimateNotification;
use Livewire\Component;
class Create extends Component
{
public Diagnosis $diagnosis;
public $terms_and_conditions = '';
public $validity_period_days = 30;
public $tax_rate = 8.25;
public $discount_amount = 0;
public $notes = '';
public $internal_notes = '';
public $lineItems = [];
public $subtotal = 0;
public $tax_amount = 0;
public $total_amount = 0;
protected $rules = [
'terms_and_conditions' => '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:labour,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' => 'labour',
'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' => 'labour',
'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' => 'labour',
'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', 'labour')->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');
}
}