- 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.
238 lines
8.4 KiB
PHP
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: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');
|
|
}
|
|
}
|