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

223 lines
6.6 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 Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Create extends Component
{
#[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()
{
$this->invoice_date = now()->format('Y-m-d');
$this->due_date = now()->addDays(30)->format('Y-m-d');
$this->branch_id = Auth::user()->branch_id ?? '';
$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();
// Initialize with one empty line item
$this->addLineItem();
}
public function addLineItem()
{
$this->line_items[] = [
'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;
}
}
// Create invoice
$invoice = Invoice::create([
'customer_id' => $this->customer_id,
'branch_id' => $this->branch_id,
'created_by' => Auth::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,
'status' => 'draft',
]);
// Create line items
foreach ($this->line_items as $item) {
InvoiceLineItem::create([
'invoice_id' => $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 created successfully.');
return $this->redirect(route('invoices.show', $invoice));
}
#[Layout('components.layouts.app.sidebar')]
public function render()
{
return view('livewire.invoices.create');
}
}