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

276 lines
8.8 KiB
PHP

<?php
namespace App\Livewire\Invoices;
use App\Models\Branch;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\InvoiceLineItem;
use App\Models\Part;
use App\Models\ServiceItem;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Edit extends Component
{
public Invoice $invoice;
#[Validate('required')]
public $customer_id = '';
#[Validate('required')]
public $branch_id = '';
#[Validate('required|date')]
public $invoice_date = '';
#[Validate('required|date|after:invoice_date')]
public $due_date = '';
#[Validate('nullable|string')]
public $description = '';
#[Validate('nullable|string')]
public $notes = '';
#[Validate('nullable|string')]
public $terms_and_conditions = '';
#[Validate('numeric|min:0|max:100')]
public $tax_rate = 8.50;
#[Validate('numeric|min:0')]
public $discount_amount = 0;
public $line_items = [];
public $customers = [];
public $branches = [];
public $parts = [];
public $service_items = [];
public function mount(Invoice $invoice)
{
$this->invoice = $invoice;
// Check if invoice can be edited
if (in_array($invoice->status, ['paid', 'cancelled'])) {
session()->flash('error', 'This invoice cannot be edited as it is '.$invoice->status.'.');
return $this->redirect(route('invoices.show', $invoice));
}
// Populate form data
$this->customer_id = $invoice->customer_id;
$this->branch_id = $invoice->branch_id;
$this->invoice_date = $invoice->invoice_date->format('Y-m-d');
$this->due_date = $invoice->due_date->format('Y-m-d');
$this->description = $invoice->description;
$this->notes = $invoice->notes;
$this->terms_and_conditions = $invoice->terms_and_conditions;
$this->tax_rate = $invoice->tax_rate;
$this->discount_amount = $invoice->discount_amount;
// Load line items
$this->line_items = $invoice->lineItems->map(function ($item) {
return [
'id' => $item->id,
'type' => $item->type,
'description' => $item->description,
'quantity' => $item->quantity,
'unit_price' => $item->unit_price,
'part_id' => $item->part_id,
'part_number' => $item->part_number,
'technical_notes' => $item->technical_notes,
];
})->toArray();
// Load data
$this->customers = Customer::orderBy('first_name')->orderBy('last_name')->get();
$this->branches = Branch::orderBy('name')->get();
$this->parts = Part::where('status', 'active')->orderBy('name')->get();
$this->service_items = ServiceItem::where('status', 'active')->orderBy('service_name')->get();
// Add empty line item if none exist
if (empty($this->line_items)) {
$this->addLineItem();
}
}
public function addLineItem()
{
$this->line_items[] = [
'id' => null,
'type' => 'labour',
'description' => '',
'quantity' => 1,
'unit_price' => 0,
'part_id' => '',
'part_number' => '',
'technical_notes' => '',
];
}
public function removeLineItem($index)
{
unset($this->line_items[$index]);
$this->line_items = array_values($this->line_items);
}
public function updatedLineItems($value, $key)
{
// Auto-populate part details when part is selected
if (str_contains($key, 'part_id')) {
$index = explode('.', $key)[0];
$partId = $this->line_items[$index]['part_id'];
if ($partId) {
$part = Part::find($partId);
if ($part) {
$this->line_items[$index]['description'] = $part->name;
$this->line_items[$index]['unit_price'] = $part->sell_price ?? 0;
$this->line_items[$index]['part_number'] = $part->part_number;
}
}
}
// Reset part selection when type changes
if (str_contains($key, 'type')) {
$index = explode('.', $key)[0];
$this->line_items[$index]['part_id'] = '';
$this->line_items[$index]['part_number'] = '';
if ($this->line_items[$index]['type'] !== 'parts') {
$this->line_items[$index]['description'] = '';
$this->line_items[$index]['unit_price'] = 0;
}
}
// Auto-populate service item details
if (str_contains($key, 'service_item_id')) {
$index = explode('.', $key)[0];
$serviceItemId = $this->line_items[$index]['service_item_id'] ?? null;
if ($serviceItemId) {
$serviceItem = ServiceItem::find($serviceItemId);
if ($serviceItem) {
$this->line_items[$index]['description'] = $serviceItem->service_name;
$this->line_items[$index]['unit_price'] = $serviceItem->price ?? 0;
}
}
}
}
public function calculateSubtotal()
{
return collect($this->line_items)->sum(function ($item) {
return ($item['quantity'] ?? 0) * ($item['unit_price'] ?? 0);
});
}
public function calculateTax()
{
$subtotal = $this->calculateSubtotal() - $this->discount_amount;
return $subtotal * ($this->tax_rate / 100);
}
public function calculateTotal()
{
$subtotal = $this->calculateSubtotal();
$tax = $this->calculateTax();
return $subtotal + $tax - $this->discount_amount;
}
public function save()
{
$this->validate();
// Validate line items
if (empty($this->line_items)) {
$this->addError('line_items', 'At least one line item is required.');
return;
}
foreach ($this->line_items as $index => $item) {
if (empty($item['description'])) {
$this->addError("line_items.{$index}.description", 'Description is required.');
return;
}
if ($item['quantity'] <= 0) {
$this->addError("line_items.{$index}.quantity", 'Quantity must be greater than 0.');
return;
}
if ($item['unit_price'] < 0) {
$this->addError("line_items.{$index}.unit_price", 'Unit price cannot be negative.');
return;
}
}
// Update invoice
$this->invoice->update([
'customer_id' => $this->customer_id,
'branch_id' => $this->branch_id,
'invoice_date' => $this->invoice_date,
'due_date' => $this->due_date,
'description' => $this->description,
'notes' => $this->notes,
'terms_and_conditions' => $this->terms_and_conditions,
'tax_rate' => $this->tax_rate,
'discount_amount' => $this->discount_amount,
]);
// Get existing line item IDs
$existingIds = collect($this->line_items)->pluck('id')->filter()->toArray();
// Delete line items not in the current list
$this->invoice->lineItems()->whereNotIn('id', $existingIds)->delete();
// Update or create line items
foreach ($this->line_items as $item) {
if ($item['id']) {
// Update existing line item
InvoiceLineItem::where('id', $item['id'])->update([
'type' => $item['type'],
'description' => $item['description'],
'quantity' => $item['quantity'],
'unit_price' => $item['unit_price'],
'part_id' => $item['part_id'] ?: null,
'part_number' => $item['part_number'] ?: null,
'technical_notes' => $item['technical_notes'] ?: null,
]);
} else {
// Create new line item
InvoiceLineItem::create([
'invoice_id' => $this->invoice->id,
'type' => $item['type'],
'description' => $item['description'],
'quantity' => $item['quantity'],
'unit_price' => $item['unit_price'],
'part_id' => $item['part_id'] ?: null,
'part_number' => $item['part_number'] ?: null,
'technical_notes' => $item['technical_notes'] ?: null,
]);
}
}
session()->flash('success', 'Invoice updated successfully.');
return $this->redirect(route('invoices.show', $this->invoice));
}
#[Layout('components.layouts.app.sidebar')]
public function render()
{
return view('livewire.invoices.edit');
}
}