Enhance UI and functionality across various components
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run

- 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.
This commit is contained in:
2025-08-16 14:36:58 +00:00
parent dbabed29d0
commit e3b2b220d2
91 changed files with 6350 additions and 1336 deletions

View File

@ -525,4 +525,25 @@ $delete = fn(Product $product) => $product->delete();
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter.
</laravel-boost-guidelines>
</laravel-boost-guidelines>
## Theme to use across components
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--color-zinc-50: var(--color-neutral-50);
--color-zinc-100: var(--color-neutral-100);
--color-zinc-200: var(--color-neutral-200);
--color-zinc-300: var(--color-neutral-300);
--color-zinc-400: var(--color-neutral-400);
--color-zinc-500: var(--color-neutral-500);
--color-zinc-600: var(--color-neutral-600);
--color-zinc-700: var(--color-neutral-700);
--color-zinc-800: var(--color-neutral-800);
--color-zinc-900: var(--color-neutral-900);
--color-zinc-950: var(--color-neutral-950);
--color-accent: var(--color-orange-500);
--color-accent-content: var(--color-orange-600);
--color-accent-foreground: var(--color-white);
}

View File

@ -38,7 +38,7 @@ class Create extends Component
'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.*.type' => 'required|in:labour,parts,miscellaneous',
'lineItems.*.description' => 'required|string',
'lineItems.*.quantity' => 'required|numeric|min:0.01',
'lineItems.*.unit_price' => 'required|numeric|min:0',
@ -64,7 +64,7 @@ class Create extends Component
if ($this->diagnosis->labor_operations) {
foreach ($this->diagnosis->labor_operations as $labor) {
$this->lineItems[] = [
'type' => 'labor',
'type' => 'labour',
'description' => $labor['operation'] ?? 'Labor Operation',
'quantity' => $labor['estimated_hours'] ?? 1,
'unit_price' => $labor['labor_rate'] ?? 75,
@ -95,7 +95,7 @@ class Create extends Component
// If no line items from diagnosis, add a default labor item
if (empty($this->lineItems)) {
$this->lineItems[] = [
'type' => 'labor',
'type' => 'labour',
'description' => 'Diagnostic and Repair Services',
'quantity' => $this->diagnosis->estimated_repair_time ?? 1,
'unit_price' => 75,
@ -112,7 +112,7 @@ class Create extends Component
public function addLineItem()
{
$this->lineItems[] = [
'type' => 'labor',
'type' => 'labour',
'description' => '',
'quantity' => 1,
'unit_price' => 0,
@ -181,7 +181,7 @@ class Create extends Component
'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'),
'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,

View File

@ -51,12 +51,12 @@ class CreateStandalone extends Component
protected $rules = [
'customerId' => 'required|exists:customers,id',
'vehicleId' => 'required|exists:vehicles,id',
'vehicleId' => 'nullable|exists:vehicles,id',
'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.*.type' => 'required|in:labour,parts,miscellaneous',
'lineItems.*.description' => 'required|string',
'lineItems.*.quantity' => 'required|numeric|min:0.01',
'lineItems.*.unit_price' => 'required|numeric|min:0',
@ -104,7 +104,7 @@ class CreateStandalone extends Component
'part_id' => null,
'part_number' => '',
'description' => '',
'quantity' => 1,
'quantity' => '', // Start empty to force user input
'unit_price' => 0,
'subtotal' => 0,
'type' => 'labour',
@ -155,10 +155,15 @@ class CreateStandalone extends Component
$this->subtotal = 0;
foreach ($this->lineItems as $index => $item) {
if (isset($item['quantity'], $item['unit_price'])) {
$lineSubtotal = $item['quantity'] * $item['unit_price'];
$quantity = is_numeric($item['quantity']) ? (float) $item['quantity'] : 0;
$unitPrice = is_numeric($item['unit_price']) ? (float) $item['unit_price'] : 0;
if ($quantity > 0 && $unitPrice >= 0) {
$lineSubtotal = $quantity * $unitPrice;
$this->lineItems[$index]['subtotal'] = $lineSubtotal;
$this->subtotal += $lineSubtotal;
} else {
$this->lineItems[$index]['subtotal'] = 0;
}
}
@ -201,6 +206,9 @@ class CreateStandalone extends Component
$this->lineItems[$lineIndex]['stock_available'] = $part->quantity_on_hand;
$this->lineItems[$lineIndex]['type'] = 'parts';
// DO NOT auto-set quantity - let user enter their desired quantity
// $this->lineItems[$lineIndex]['quantity'] stays as user entered
$this->calculateTotals();
}
@ -227,10 +235,19 @@ class CreateStandalone extends Component
return;
}
// Validate that all line items have quantities
foreach ($this->lineItems as $index => $item) {
if (empty($item['quantity']) || $item['quantity'] <= 0) {
$this->addError("lineItems.{$index}.quantity", 'Quantity is required and must be greater than 0.');
return;
}
}
$estimate = Estimate::create([
'estimate_number' => $this->generateEstimateNumber(),
'customer_id' => $this->customerId,
'vehicle_id' => $this->vehicleId,
'vehicle_id' => $this->vehicleId ?: null, // Allow null for vehicle_id
'job_card_id' => null, // No job card for standalone estimates
'diagnosis_id' => null, // No diagnosis for standalone estimates
'status' => 'draft',

View File

@ -56,14 +56,14 @@ class Edit extends Component
// Quick add presets
public $quickAddPresets = [
'oil_change' => ['type' => 'labor', 'description' => 'Oil Change Service', 'quantity' => 1, 'unit_price' => 75],
'brake_inspection' => ['type' => 'labor', 'description' => 'Brake System Inspection', 'quantity' => 1, 'unit_price' => 125],
'tire_rotation' => ['type' => 'labor', 'description' => 'Tire Rotation Service', 'quantity' => 1, 'unit_price' => 50],
'oil_change' => ['type' => 'labour', 'description' => 'Oil Change Service', 'quantity' => 1, 'unit_price' => 75],
'brake_inspection' => ['type' => 'labour', 'description' => 'Brake System Inspection', 'quantity' => 1, 'unit_price' => 125],
'tire_rotation' => ['type' => 'labour', 'description' => 'Tire Rotation Service', 'quantity' => 1, 'unit_price' => 50],
];
// Line item templates
public $newItem = [
'type' => 'labor',
'type' => 'labour',
'description' => '',
'quantity' => 1,
'unit_price' => 0,
@ -80,7 +80,7 @@ class Edit extends Component
'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.*.type' => 'required|in:labour,parts,miscellaneous',
'lineItems.*.description' => 'required|string',
'lineItems.*.quantity' => 'required|numeric|min:0.01',
'lineItems.*.unit_price' => 'required|numeric|min:0',

View File

@ -5,6 +5,7 @@ namespace App\Livewire\Estimates;
use App\Models\Customer;
use App\Models\Estimate;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Component;
@ -59,6 +60,9 @@ class Index extends Component
public $selectAll = false;
// Row selection for inline actions
public $selectedRow = null;
// Quick stats
public $stats = [];
@ -425,17 +429,76 @@ class Index extends Component
return;
}
// Update estimate status
$estimate->update([
'status' => 'sent',
'sent_to_customer_at' => now(),
]);
// TODO: Send email/SMS notification to customer
// Send notification to customer
$this->sendEstimateNotification($estimate);
session()->flash('success', 'Estimate sent to customer successfully.');
$this->loadStats();
}
public function resendEstimate($estimateId)
{
$estimate = Estimate::findOrFail($estimateId);
if (! Auth::user()->can('update', $estimate)) {
session()->flash('error', 'You are not authorized to resend this estimate.');
return;
}
if (! in_array($estimate->status, ['sent', 'approved', 'rejected'])) {
session()->flash('error', 'Only sent, approved, or rejected estimates can be resent.');
return;
}
// Update the sent timestamp
$estimate->update([
'sent_to_customer_at' => now(),
]);
// Send notification to customer
$this->sendEstimateNotification($estimate);
session()->flash('success', 'Estimate resent to customer successfully.');
$this->loadStats();
}
private function sendEstimateNotification(Estimate $estimate)
{
try {
// Get customer from estimate
$customer = $estimate->customer;
if (! $customer) {
// If no direct customer, get from job card
$customer = $estimate->jobCard?->customer;
}
if ($customer && $customer->email) {
// Send email notification using Notification facade
\Illuminate\Support\Facades\Notification::send($customer, new \App\Notifications\EstimateNotification($estimate));
// TODO: Send SMS notification if phone number is available
// if ($customer->phone) {
// // Implement SMS sending logic here
// }
} else {
session()->flash('warning', 'Estimate status updated, but customer email not found. Please contact customer manually.');
}
} catch (\Exception $e) {
// Log the error but don't fail the operation
\Illuminate\Support\Facades\Log::error('Failed to send estimate notification: '.$e->getMessage());
session()->flash('warning', 'Estimate status updated, but notification could not be sent. Please contact customer manually.');
}
}
public function confirmDelete($estimateId)
{
$estimate = Estimate::findOrFail($estimateId);
@ -453,6 +516,15 @@ class Index extends Component
$this->loadStats();
}
public function selectRow($estimateId)
{
if ($this->selectedRow === $estimateId) {
$this->selectedRow = null; // Deselect if clicking the same row
} else {
$this->selectedRow = $estimateId; // Select the clicked row
}
}
#[Layout('components.layouts.app')]
public function render()
{

View File

@ -217,28 +217,78 @@ class Show extends Component
public function downloadPDF()
{
try {
// Generate PDF using DomPDF
$pdf = Pdf::loadView('estimates.pdf', [
'estimate' => $this->estimate,
// Load the estimate with relationships for PDF generation
$estimate = $this->estimate->load([
'customer',
'jobCard.customer',
'jobCard.vehicle',
'vehicle',
'lineItems.part',
'preparedBy',
'diagnosis',
]);
// Log the download
Log::info('Estimate PDF downloaded', [
'estimate_id' => $this->estimate->id,
'downloaded_by' => auth()->id(),
]);
// For now, let's use a simple approach that should work
// First, verify HTML generation works
$html = view('estimates.pdf', compact('estimate'))->render();
return response()->streamDownload(function () use ($pdf) {
echo $pdf->output();
}, "estimate-{$this->estimate->estimate_number}.pdf");
if (empty($html)) {
throw new \Exception('Failed to generate HTML template');
}
// Try different approaches for PDF generation
try {
// Approach 1: Use Laravel's PDF facade if available
if (class_exists('\Barryvdh\DomPDF\Facade\Pdf')) {
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadHtml($html);
$pdf->setPaper('letter', 'portrait');
$output = $pdf->output();
} else {
throw new \Exception('PDF facade not available');
}
} catch (\Exception $e1) {
try {
// Approach 2: Direct DomPDF instantiation
$dompdf = new \Dompdf\Dompdf;
$dompdf->loadHtml($html);
$dompdf->setPaper('letter', 'portrait');
$dompdf->render();
$output = $dompdf->output();
} catch (\Exception $e2) {
// Approach 3: Return HTML for now
$filename = 'estimate-'.$estimate->estimate_number.'.html';
return response()->streamDownload(function () use ($html) {
echo $html;
}, $filename, [
'Content-Type' => 'text/html',
'Content-Disposition' => 'attachment; filename="'.$filename.'"',
]);
}
}
// Create filename with estimate number
$filename = 'estimate-'.$estimate->estimate_number.'.pdf';
// Return PDF for download
return response()->streamDownload(function () use ($output) {
echo $output;
}, $filename, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="'.$filename.'"',
]);
} catch (\Exception $e) {
Log::error('Failed to generate estimate PDF', [
'estimate_id' => $this->estimate->id,
// Log the error for debugging
Log::error('PDF generation failed for estimate '.$this->estimate->id, [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
session()->flash('error', 'Failed to generate PDF. Please try again.');
// Show user-friendly error message
session()->flash('error', 'Failed to generate PDF: '.$e->getMessage());
return null;
}
}
@ -265,6 +315,23 @@ class Show extends Component
return redirect()->route('work-orders.create', ['estimate' => $this->estimate->id]);
}
public function convertToInvoice()
{
if ($this->estimate->customer_approval_status !== 'approved') {
session()->flash('error', 'Estimate must be approved before converting to invoice.');
return;
}
if (! auth()->user()->can('create', \App\Models\Invoice::class)) {
session()->flash('error', 'You do not have permission to create invoices.');
return;
}
return redirect()->route('invoices.create-from-estimate', $this->estimate);
}
#[Layout('components.layouts.app')]
public function render()
{

View File

@ -0,0 +1,222 @@
<?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');
}
}

View File

@ -0,0 +1,196 @@
<?php
namespace App\Livewire\Invoices;
use App\Models\Branch;
use App\Models\Estimate;
use App\Models\Invoice;
use App\Models\Part;
use App\Models\ServiceItem;
use Livewire\Attributes\Validate;
use Livewire\Component;
class CreateFromEstimate extends Component
{
public Estimate $estimate;
#[Validate('required|string|max:255')]
public $subject = '';
#[Validate('nullable|string')]
public $description = '';
#[Validate('required|exists:branches,id')]
public $branch_id = '';
#[Validate('required|date')]
public $invoice_date = '';
#[Validate('required|date|after_or_equal:invoice_date')]
public $due_date = '';
#[Validate('required|numeric|min:0')]
public $tax_rate = 0;
public $lineItems = [];
public $branches = [];
public $serviceItems = [];
public $parts = [];
public function mount(Estimate $estimate)
{
$this->estimate = $estimate;
$this->authorize('create', Invoice::class);
// Pre-fill form with estimate data
$this->subject = $estimate->subject;
$this->description = $estimate->description;
$this->branch_id = $estimate->branch_id;
$this->invoice_date = now()->format('Y-m-d');
$this->due_date = now()->addDays(30)->format('Y-m-d');
$this->tax_rate = $estimate->tax_rate;
// Load estimate line items
$this->lineItems = $estimate->estimateLineItems->map(function ($item) {
return [
'type' => $item->type,
'service_item_id' => $item->service_item_id,
'part_id' => $item->part_id,
'description' => $item->description,
'quantity' => $item->quantity,
'unit_price' => $item->unit_price,
'total' => $item->total,
];
})->toArray();
$this->loadFormData();
}
public function loadFormData()
{
$this->branches = Branch::all();
$this->serviceItems = ServiceItem::all();
$this->parts = Part::all();
}
public function addLineItem()
{
$this->lineItems[] = [
'type' => 'service',
'service_item_id' => '',
'part_id' => '',
'description' => '',
'quantity' => 1,
'unit_price' => 0,
'total' => 0,
];
}
public function removeLineItem($index)
{
unset($this->lineItems[$index]);
$this->lineItems = array_values($this->lineItems);
}
public function updatedLineItems($value, $key)
{
$keyParts = explode('.', $key);
$index = $keyParts[0];
$field = $keyParts[1];
if ($field === 'type') {
// Reset related fields when type changes
$this->lineItems[$index]['service_item_id'] = '';
$this->lineItems[$index]['part_id'] = '';
$this->lineItems[$index]['description'] = '';
$this->lineItems[$index]['unit_price'] = 0;
}
if ($field === 'service_item_id' && $this->lineItems[$index]['type'] === 'service') {
$serviceItem = ServiceItem::find($value);
if ($serviceItem) {
$this->lineItems[$index]['description'] = $serviceItem->name;
$this->lineItems[$index]['unit_price'] = $serviceItem->price;
}
}
if ($field === 'part_id' && $this->lineItems[$index]['type'] === 'part') {
$part = Part::find($value);
if ($part) {
$this->lineItems[$index]['description'] = $part->name;
$this->lineItems[$index]['unit_price'] = $part->sell_price;
}
}
// Recalculate total for this line item
if (in_array($field, ['quantity', 'unit_price'])) {
$quantity = (float) $this->lineItems[$index]['quantity'];
$unitPrice = (float) $this->lineItems[$index]['unit_price'];
$this->lineItems[$index]['total'] = $quantity * $unitPrice;
}
}
public function getSubtotalProperty()
{
return collect($this->lineItems)->sum('total');
}
public function getTaxAmountProperty()
{
return $this->subtotal * ($this->tax_rate / 100);
}
public function getTotalProperty()
{
return $this->subtotal + $this->taxAmount;
}
public function save()
{
$this->validate();
$invoice = Invoice::create([
'job_card_id' => $this->estimate->job_card_id,
'estimate_id' => $this->estimate->id,
'customer_id' => $this->estimate->customer_id,
'branch_id' => $this->branch_id,
'created_by' => auth()->id(),
'invoice_number' => Invoice::generateInvoiceNumber($this->branch_id),
'subject' => $this->subject,
'description' => $this->description,
'invoice_date' => $this->invoice_date,
'due_date' => $this->due_date,
'subtotal' => $this->subtotal,
'tax_rate' => $this->tax_rate,
'tax_amount' => $this->taxAmount,
'total_amount' => $this->total,
'status' => 'draft',
]);
// Create line items
foreach ($this->lineItems as $item) {
$invoice->invoiceLineItems()->create([
'type' => $item['type'],
'service_item_id' => $item['service_item_id'] ?: null,
'part_id' => $item['part_id'] ?: null,
'description' => $item['description'],
'quantity' => $item['quantity'],
'unit_price' => $item['unit_price'],
'total' => $item['total'],
]);
}
session()->flash('success', 'Invoice created successfully from estimate.');
return redirect()->route('invoices.show', $invoice);
}
public function render()
{
return view('livewire.invoices.create-from-estimate')
->layout('layouts.app');
}
}

View File

@ -0,0 +1,275 @@
<?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');
}
}

View File

@ -0,0 +1,178 @@
<?php
namespace App\Livewire\Invoices;
use App\Models\Branch;
use App\Models\Invoice;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;
class Index extends Component
{
use WithPagination;
public $search = '';
public $filterStatus = '';
public $filterBranch = '';
public $filterDateFrom = '';
public $filterDateTo = '';
public $sortField = 'invoice_date';
public $sortDirection = 'desc';
protected $queryString = [
'search' => ['except' => ''],
'filterStatus' => ['except' => ''],
'filterBranch' => ['except' => ''],
'sortField' => ['except' => 'invoice_date'],
'sortDirection' => ['except' => 'desc'],
];
public function mount()
{
// Set default date filter to current month
$this->filterDateFrom = now()->startOfMonth()->format('Y-m-d');
$this->filterDateTo = now()->endOfMonth()->format('Y-m-d');
}
public function updatingSearch()
{
$this->resetPage();
}
public function updatingFilterStatus()
{
$this->resetPage();
}
public function updatingFilterBranch()
{
$this->resetPage();
}
public function sortBy($field)
{
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortField = $field;
$this->sortDirection = 'asc';
}
$this->resetPage();
}
public function clearFilters()
{
$this->reset(['search', 'filterStatus', 'filterBranch', 'filterDateFrom', 'filterDateTo']);
$this->resetPage();
}
public function deleteInvoice($invoiceId)
{
$invoice = Invoice::findOrFail($invoiceId);
// Check permissions
if (! auth()->user()->can('delete', $invoice)) {
session()->flash('error', 'You do not have permission to delete this invoice.');
return;
}
// Only allow deletion of draft invoices
if ($invoice->status !== 'draft') {
session()->flash('error', 'Only draft invoices can be deleted.');
return;
}
$invoice->delete();
session()->flash('success', 'Invoice deleted successfully.');
}
public function markAsPaid($invoiceId)
{
$invoice = Invoice::findOrFail($invoiceId);
if (! auth()->user()->can('update', $invoice)) {
session()->flash('error', 'You do not have permission to update this invoice.');
return;
}
$invoice->markAsPaid('manual', null, 'Marked as paid from invoice list');
session()->flash('success', 'Invoice marked as paid.');
}
public function sendInvoice($invoiceId)
{
$invoice = Invoice::findOrFail($invoiceId);
if (! auth()->user()->can('update', $invoice)) {
session()->flash('error', 'You do not have permission to send this invoice.');
return;
}
// This would typically integrate with email service
$invoice->markAsSent('email', $invoice->customer->email);
session()->flash('success', 'Invoice sent successfully.');
}
public function getInvoicesProperty()
{
return Invoice::query()
->with(['customer', 'branch', 'createdBy'])
->when($this->search, function (Builder $query) {
$query->where(function ($q) {
$q->where('invoice_number', 'like', '%'.$this->search.'%')
->orWhereHas('customer', function ($customerQuery) {
$customerQuery->where('first_name', 'like', '%'.$this->search.'%')
->orWhere('last_name', 'like', '%'.$this->search.'%')
->orWhere('email', 'like', '%'.$this->search.'%')
->orWhereRaw("CONCAT(first_name, ' ', last_name) LIKE ?", ['%'.$this->search.'%']);
});
});
})
->when($this->filterStatus, function (Builder $query) {
$query->where('status', $this->filterStatus);
})
->when($this->filterBranch, function (Builder $query) {
$query->where('branch_id', $this->filterBranch);
})
->when($this->filterDateFrom, function (Builder $query) {
$query->where('invoice_date', '>=', $this->filterDateFrom);
})
->when($this->filterDateTo, function (Builder $query) {
$query->where('invoice_date', '<=', $this->filterDateTo);
})
->orderBy($this->sortField, $this->sortDirection)
->paginate(15);
}
public function getBranchesProperty()
{
return Branch::orderBy('name')->get();
}
public function getStatusOptionsProperty()
{
return Invoice::getStatusOptions();
}
#[Layout('components.layouts.app.sidebar')]
public function render()
{
return view('livewire.invoices.index', [
'invoices' => $this->invoices,
'branches' => $this->branches,
'statusOptions' => $this->statusOptions,
]);
}
}

View File

@ -0,0 +1,171 @@
<?php
namespace App\Livewire\Invoices;
use App\Models\Invoice;
use Illuminate\Support\Facades\Log;
use Livewire\Attributes\Layout;
use Livewire\Component;
class Show extends Component
{
public Invoice $invoice;
public function mount(Invoice $invoice)
{
$this->invoice = $invoice->load([
'customer',
'branch',
'createdBy',
'serviceOrder.vehicle',
'jobCard.vehicle',
'estimate',
'lineItems.part',
'lineItems.serviceItem',
]);
}
public function downloadPDF()
{
try {
// Generate HTML from the template
$html = view('invoices.pdf', ['invoice' => $this->invoice])->render();
if (empty($html)) {
throw new \Exception('Failed to generate PDF template');
}
// Try different approaches for PDF generation
try {
// Approach 1: Use Laravel's PDF facade if available
if (class_exists('\Barryvdh\DomPDF\Facade\Pdf')) {
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadHtml($html);
$pdf->setPaper('letter', 'portrait');
$output = $pdf->output();
} else {
throw new \Exception('PDF facade not available');
}
} catch (\Exception $e1) {
try {
// Approach 2: Direct DomPDF instantiation
$dompdf = new \Dompdf\Dompdf;
$dompdf->loadHtml($html);
$dompdf->setPaper('letter', 'portrait');
$dompdf->render();
$output = $dompdf->output();
} catch (\Exception $e2) {
// Approach 3: Return HTML for now
$filename = 'invoice-'.$this->invoice->invoice_number.'.html';
return response()->streamDownload(function () use ($html) {
echo $html;
}, $filename, [
'Content-Type' => 'text/html',
'Content-Disposition' => 'attachment; filename="'.$filename.'"',
]);
}
}
// Create filename with invoice number
$filename = 'invoice-'.$this->invoice->invoice_number.'.pdf';
// Return PDF for download
return response()->streamDownload(function () use ($output) {
echo $output;
}, $filename, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="'.$filename.'"',
]);
} catch (\Exception $e) {
// Log the error for debugging
Log::error('PDF generation failed for invoice '.$this->invoice->id, [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// Show user-friendly error message
session()->flash('error', 'Failed to generate PDF: '.$e->getMessage());
return null;
}
}
public function markAsPaid()
{
if (! auth()->user()->can('update', $this->invoice)) {
session()->flash('error', 'You do not have permission to update this invoice.');
return;
}
$this->invoice->markAsPaid('manual', null, 'Marked as paid from invoice view');
$this->invoice->refresh();
session()->flash('success', 'Invoice marked as paid successfully.');
}
public function sendInvoice()
{
if (! auth()->user()->can('update', $this->invoice)) {
session()->flash('error', 'You do not have permission to send this invoice.');
return;
}
// This would typically integrate with email service
$this->invoice->markAsSent('email', $this->invoice->customer->email);
$this->invoice->refresh();
session()->flash('success', 'Invoice sent successfully.');
}
public function duplicateInvoice()
{
if (! auth()->user()->can('create', Invoice::class)) {
session()->flash('error', 'You do not have permission to create invoices.');
return;
}
try {
$newInvoice = $this->invoice->replicate();
$newInvoice->invoice_number = Invoice::generateInvoiceNumber($this->invoice->branch->code);
$newInvoice->status = 'draft';
$newInvoice->invoice_date = now()->toDateString();
$newInvoice->due_date = now()->addDays(30)->toDateString();
$newInvoice->paid_at = null;
$newInvoice->payment_method = null;
$newInvoice->payment_reference = null;
$newInvoice->payment_notes = null;
$newInvoice->sent_at = null;
$newInvoice->sent_method = null;
$newInvoice->sent_to = null;
$newInvoice->created_by = auth()->id();
$newInvoice->save();
// Duplicate line items
foreach ($this->invoice->lineItems as $lineItem) {
$newLineItem = $lineItem->replicate();
$newLineItem->invoice_id = $newInvoice->id;
$newLineItem->save();
}
// Recalculate totals
$newInvoice->recalculateTotals();
return redirect()->route('invoices.edit', $newInvoice);
} catch (\Exception $e) {
Log::error('Failed to duplicate invoice', [
'invoice_id' => $this->invoice->id,
'error' => $e->getMessage(),
]);
session()->flash('error', 'Failed to duplicate invoice. Please try again.');
}
}
#[Layout('components.layouts.app.sidebar')]
public function render()
{
return view('livewire.invoices.show');
}
}

View File

@ -6,11 +6,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Notifications\Notifiable;
class Customer extends Model
{
/** @use HasFactory<\Database\Factories\CustomerFactory> */
use HasFactory;
use HasFactory, Notifiable;
protected $fillable = [
'user_id',

View File

@ -1,158 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Estimate extends Model
{
use HasFactory;
protected $fillable = [
'job_card_id',
'diagnosis_id',
'customer_id',
'vehicle_id',
'estimate_number',
'prepared_by_id',
'labor_cost',
'parts_cost',
'miscellaneous_cost',
'subtotal',
'tax_rate',
'tax_amount',
'discount_amount',
'total_amount',
'validity_period_days',
'terms_and_conditions',
'status',
'customer_approval_status',
'customer_approved_at',
'customer_approval_method',
'sent_to_customer_at',
'sms_sent_at',
'email_sent_at',
'notes',
'internal_notes',
'revision_number',
'original_estimate_id',
];
protected $casts = [
'labor_cost' => 'decimal:2',
'parts_cost' => 'decimal:2',
'miscellaneous_cost' => 'decimal:2',
'subtotal' => 'decimal:2',
'tax_rate' => 'decimal:4',
'tax_amount' => 'decimal:2',
'discount_amount' => 'decimal:2',
'total_amount' => 'decimal:2',
'customer_approved_at' => 'datetime',
'sent_to_customer_at' => 'datetime',
'sms_sent_at' => 'datetime',
'email_sent_at' => 'datetime',
];
protected static function boot()
{
parent::boot();
static::creating(function ($estimate) {
if (empty($estimate->estimate_number)) {
$estimate->estimate_number = 'EST-'.date('Y').'-'.str_pad(
static::whereYear('created_at', now()->year)->count() + 1,
6,
'0',
STR_PAD_LEFT
);
}
});
}
public function jobCard(): BelongsTo
{
return $this->belongsTo(JobCard::class);
}
public function diagnosis(): BelongsTo
{
return $this->belongsTo(Diagnosis::class);
}
public function preparedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'prepared_by_id');
}
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function vehicle(): BelongsTo
{
return $this->belongsTo(Vehicle::class);
}
public function lineItems(): HasMany
{
return $this->hasMany(EstimateLineItem::class);
}
public function originalEstimate(): BelongsTo
{
return $this->belongsTo(Estimate::class, 'original_estimate_id');
}
public function revisions(): HasMany
{
return $this->hasMany(Estimate::class, 'original_estimate_id');
}
public function workOrders(): HasMany
{
return $this->hasMany(WorkOrder::class);
}
public function getValidUntilAttribute()
{
return $this->created_at->addDays($this->validity_period_days);
}
public function getIsExpiredAttribute()
{
return $this->valid_until < now() && $this->status !== 'approved';
}
public function calculateTotals(): void
{
$this->labor_cost = $this->lineItems()->where('type', 'labor')->sum('total_amount');
$this->parts_cost = $this->lineItems()->where('type', 'parts')->sum('total_amount');
$this->miscellaneous_cost = $this->lineItems()->where('type', 'miscellaneous')->sum('total_amount');
$this->subtotal = $this->labor_cost + $this->parts_cost + $this->miscellaneous_cost - $this->discount_amount;
$this->tax_amount = $this->subtotal * ($this->tax_rate / 100);
$this->total_amount = $this->subtotal + $this->tax_amount;
$this->save();
}
/**
* Get the customer for this estimate (either direct or through job card)
*/
public function getEstimateCustomerAttribute()
{
return $this->customer_id ? $this->customer : $this->jobCard?->customer;
}
/**
* Get the vehicle for this estimate (either direct or through job card)
*/
public function getEstimateVehicleAttribute()
{
return $this->vehicle_id ? $this->vehicle : $this->jobCard?->vehicle;
}
}

View File

@ -1,159 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Estimate extends Model
{
use HasFactory;
protected $fillable = [
'job_card_id',
'diagnosis_id',
'customer_id',
'vehicle_id',
'estimate_number',
'prepared_by_id',
'labor_cost',
'parts_cost',
'miscellaneous_cost',
'subtotal',
'tax_rate',
'tax_amount',
'discount_amount',
'total_amount',
'validity_period_days',
'terms_and_conditions',
'status',
'customer_approval_status',
'customer_approved_at',
'customer_approval_method',
'sent_to_customer_at',
'sms_sent_at',
'email_sent_at',
'notes',
'internal_notes',
'revision_number',
'original_estimate_id',
];
protected $casts = [
'labor_cost' => 'decimal:2',
'parts_cost' => 'decimal:2',
'miscellaneous_cost' => 'decimal:2',
'subtotal' => 'decimal:2',
'tax_rate' => 'decimal:4',
'tax_amount' => 'decimal:2',
'discount_amount' => 'decimal:2',
'total_amount' => 'decimal:2',
'customer_approved_at' => 'datetime',
'sent_to_customer_at' => 'datetime',
'sms_sent_at' => 'datetime',
'email_sent_at' => 'datetime',
];
protected static function boot()
{
parent::boot();
static::creating(function ($estimate) {
if (empty($estimate->estimate_number)) {
$estimate->estimate_number = 'EST-'.date('Y').'-'.str_pad(
static::whereYear('created_at', now()->year)->count() + 1,
6,
'0',
STR_PAD_LEFT
);
}
});
}
public function jobCard(): BelongsTo
{
return $this->belongsTo(JobCard::class);
}
public function diagnosis(): BelongsTo
{
return $this->belongsTo(Diagnosis::class);
}
public function preparedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'prepared_by_id');
}
// Core relationships for standalone estimates
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function vehicle(): BelongsTo
{
return $this->belongsTo(Vehicle::class);
}
public function lineItems(): HasMany
{
return $this->hasMany(EstimateLineItem::class);
}
public function originalEstimate(): BelongsTo
{
return $this->belongsTo(Estimate::class, 'original_estimate_id');
}
public function revisions(): HasMany
{
return $this->hasMany(Estimate::class, 'original_estimate_id');
}
public function workOrders(): HasMany
{
return $this->hasMany(WorkOrder::class);
}
public function getValidUntilAttribute()
{
return $this->created_at->addDays($this->validity_period_days);
}
public function getIsExpiredAttribute()
{
return $this->valid_until < now() && $this->status !== 'approved';
}
public function calculateTotals(): void
{
$this->labor_cost = $this->lineItems()->where('type', 'labor')->sum('total_amount');
$this->parts_cost = $this->lineItems()->where('type', 'parts')->sum('total_amount');
$this->miscellaneous_cost = $this->lineItems()->where('type', 'miscellaneous')->sum('total_amount');
$this->subtotal = $this->labor_cost + $this->parts_cost + $this->miscellaneous_cost - $this->discount_amount;
$this->tax_amount = $this->subtotal * ($this->tax_rate / 100);
$this->total_amount = $this->subtotal + $this->tax_amount;
$this->save();
}
/**
* Get the customer for this estimate (either direct or through job card)
*/
public function getEstimateCustomerAttribute()
{
return $this->customer_id ? $this->customer : $this->jobCard?->customer;
}
/**
* Get the vehicle for this estimate (either direct or through job card)
*/
public function getEstimateVehicleAttribute()
{
return $this->vehicle_id ? $this->vehicle : $this->jobCard?->vehicle;
}
}

View File

@ -12,7 +12,7 @@ class EstimateLineItem extends Model
protected $fillable = [
'estimate_id',
'type', // 'labor', 'parts', 'miscellaneous'
'type', // 'labour', 'parts', 'miscellaneous'
'part_id',
'description',
'quantity',

268
app/Models/Invoice.php Normal file
View File

@ -0,0 +1,268 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Invoice extends Model
{
use HasFactory;
protected $fillable = [
'invoice_number',
'status',
'customer_id',
'service_order_id',
'job_card_id',
'estimate_id',
'branch_id',
'created_by',
'invoice_date',
'due_date',
'description',
'notes',
'terms_and_conditions',
'subtotal',
'tax_rate',
'tax_amount',
'discount_amount',
'total_amount',
'paid_at',
'payment_method',
'payment_reference',
'payment_notes',
'sent_at',
'sent_method',
'sent_to',
];
protected $casts = [
'invoice_date' => 'date',
'due_date' => 'date',
'paid_at' => 'datetime',
'sent_at' => 'datetime',
'subtotal' => 'decimal:2',
'tax_rate' => 'decimal:2',
'tax_amount' => 'decimal:2',
'discount_amount' => 'decimal:2',
'total_amount' => 'decimal:2',
];
/**
* Generate the next invoice number for a branch
*/
public static function generateInvoiceNumber(string $branchCode = 'MAIN'): string
{
$year = now()->year;
$prefix = strtoupper($branchCode).'-INV-'.$year.'-';
$lastInvoice = static::where('invoice_number', 'like', $prefix.'%')
->orderBy('invoice_number', 'desc')
->first();
if ($lastInvoice) {
$lastNumber = (int) substr($lastInvoice->invoice_number, strlen($prefix));
$nextNumber = $lastNumber + 1;
} else {
$nextNumber = 1;
}
return $prefix.str_pad($nextNumber, 5, '0', STR_PAD_LEFT);
}
/**
* Status options for invoices
*/
public static function getStatusOptions(): array
{
return [
'draft' => 'Draft',
'sent' => 'Sent',
'paid' => 'Paid',
'overdue' => 'Overdue',
'cancelled' => 'Cancelled',
];
}
/**
* Payment method options
*/
public static function getPaymentMethodOptions(): array
{
return [
'cash' => 'Cash',
'card' => 'Credit/Debit Card',
'check' => 'Check',
'bank_transfer' => 'Bank Transfer',
'other' => 'Other',
];
}
/**
* Check if invoice is overdue
*/
public function isOverdue(): bool
{
return $this->status !== 'paid' &&
$this->status !== 'cancelled' &&
$this->due_date < now()->startOfDay();
}
/**
* Check if invoice is paid
*/
public function isPaid(): bool
{
return $this->status === 'paid' && ! is_null($this->paid_at);
}
/**
* Mark invoice as paid
*/
public function markAsPaid(?string $paymentMethod = null, ?string $reference = null, ?string $notes = null): void
{
$this->update([
'status' => 'paid',
'paid_at' => now(),
'payment_method' => $paymentMethod,
'payment_reference' => $reference,
'payment_notes' => $notes,
]);
}
/**
* Mark invoice as sent
*/
public function markAsSent(string $method = 'email', ?string $sentTo = null): void
{
$this->update([
'status' => 'sent',
'sent_at' => now(),
'sent_method' => $method,
'sent_to' => $sentTo,
]);
}
/**
* Calculate totals from line items
*/
public function recalculateTotals(): void
{
$subtotal = $this->lineItems()->sum('total_amount');
$taxAmount = $subtotal * ($this->tax_rate / 100);
$total = $subtotal + $taxAmount - $this->discount_amount;
$this->update([
'subtotal' => $subtotal,
'tax_amount' => $taxAmount,
'total_amount' => $total,
]);
}
// Relationships
/**
* Invoice belongs to a customer
*/
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
/**
* Invoice belongs to a branch
*/
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class);
}
/**
* Invoice belongs to a user (creator)
*/
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
/**
* Invoice may belong to a service order
*/
public function serviceOrder(): BelongsTo
{
return $this->belongsTo(ServiceOrder::class);
}
/**
* Invoice may belong to a job card
*/
public function jobCard(): BelongsTo
{
return $this->belongsTo(JobCard::class);
}
/**
* Invoice may belong to an estimate
*/
public function estimate(): BelongsTo
{
return $this->belongsTo(Estimate::class);
}
/**
* Invoice has many line items
*/
public function lineItems(): HasMany
{
return $this->hasMany(InvoiceLineItem::class);
}
// Scopes
/**
* Scope to filter by branch
*/
public function scopeByBranch($query, string $branchCode)
{
return $query->whereHas('branch', function ($q) use ($branchCode) {
$q->where('code', $branchCode);
});
}
/**
* Scope to filter by status
*/
public function scopeByStatus($query, string $status)
{
return $query->where('status', $status);
}
/**
* Scope to get overdue invoices
*/
public function scopeOverdue($query)
{
return $query->where('status', '!=', 'paid')
->where('status', '!=', 'cancelled')
->where('due_date', '<', now()->startOfDay());
}
/**
* Scope to get paid invoices
*/
public function scopePaid($query)
{
return $query->where('status', 'paid');
}
/**
* Scope to get unpaid invoices
*/
public function scopeUnpaid($query)
{
return $query->whereIn('status', ['draft', 'sent', 'overdue']);
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class InvoiceLineItem extends Model
{
use HasFactory;
protected $fillable = [
'invoice_id',
'type',
'description',
'quantity',
'unit_price',
'total_amount',
'part_id',
'service_item_id',
'part_number',
'technical_notes',
];
protected $casts = [
'quantity' => 'decimal:2',
'unit_price' => 'decimal:2',
'total_amount' => 'decimal:2',
];
/**
* Type options for line items
*/
public static function getTypeOptions(): array
{
return [
'labour' => 'Labour',
'parts' => 'Parts',
'miscellaneous' => 'Miscellaneous',
];
}
/**
* Calculate total amount when quantity or unit price changes
*/
public function calculateTotal(): float
{
return $this->quantity * $this->unit_price;
}
protected static function boot()
{
parent::boot();
static::saving(function ($lineItem) {
$lineItem->total_amount = $lineItem->calculateTotal();
});
static::saved(function ($lineItem) {
// Recalculate invoice totals when line item changes
$lineItem->invoice->recalculateTotals();
});
static::deleted(function ($lineItem) {
// Recalculate invoice totals when line item is deleted
$lineItem->invoice->recalculateTotals();
});
}
// Relationships
/**
* Line item belongs to an invoice
*/
public function invoice(): BelongsTo
{
return $this->belongsTo(Invoice::class);
}
/**
* Line item may belong to a part
*/
public function part(): BelongsTo
{
return $this->belongsTo(Part::class);
}
/**
* Line item may belong to a service item
*/
public function serviceItem(): BelongsTo
{
return $this->belongsTo(ServiceItem::class);
}
}

View File

@ -40,13 +40,21 @@ class PartHistory extends Model
// Event types
const EVENT_CREATED = 'created';
const EVENT_UPDATED = 'updated';
const EVENT_STOCK_IN = 'stock_in';
const EVENT_STOCK_OUT = 'stock_out';
const EVENT_ADJUSTMENT = 'adjustment';
const EVENT_PRICE_CHANGE = 'price_change';
const EVENT_SUPPLIER_CHANGE = 'supplier_change';
const EVENT_DELETED = 'deleted';
const EVENT_RESTORED = 'restored';
public function part(): BelongsTo
@ -61,7 +69,7 @@ class PartHistory extends Model
public function getEventColorAttribute(): string
{
return match($this->event_type) {
return match ($this->event_type) {
self::EVENT_CREATED => 'green',
self::EVENT_UPDATED => 'blue',
self::EVENT_STOCK_IN => 'green',
@ -77,7 +85,7 @@ class PartHistory extends Model
public function getEventIconAttribute(): string
{
return match($this->event_type) {
return match ($this->event_type) {
self::EVENT_CREATED => 'plus-circle',
self::EVENT_UPDATED => 'pencil',
self::EVENT_STOCK_IN => 'arrow-down',
@ -98,7 +106,8 @@ class PartHistory extends Model
}
$prefix = $this->quantity_change > 0 ? '+' : '';
return $prefix . number_format($this->quantity_change);
return $prefix.number_format($this->quantity_change);
}
public static function logEvent(
@ -121,7 +130,7 @@ class PartHistory extends Model
'notes' => $options['notes'] ?? null,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'created_by' => auth()->id() ?? 1,
'created_by' => auth()->id() ?? \App\Models\User::first()?->id ?? null,
]);
}
}

View File

@ -26,7 +26,15 @@ class EstimatePolicy
*/
public function view(User $user, Estimate $estimate): bool
{
return false;
// Super admin has global access
if ($user->hasRole('super_admin')) {
return true;
}
// Service coordinators, supervisors, and admins can view estimates in their branch
// Or if they created the estimate
return $user->hasAnyRole(['service_coordinator', 'service_supervisor', 'admin'], $user->branch_code) ||
$estimate->prepared_by_id === $user->id;
}
/**
@ -48,7 +56,19 @@ class EstimatePolicy
*/
public function update(User $user, Estimate $estimate): bool
{
return false;
// Super admin has global access
if ($user->hasRole('super_admin')) {
return true;
}
// Service coordinators, supervisors, and admins can update estimates in their branch
// Or if they created the estimate (and it's still in draft status)
if ($user->hasAnyRole(['service_coordinator', 'service_supervisor', 'admin'], $user->branch_code)) {
return true;
}
// Creator can edit their own draft estimates
return $estimate->prepared_by_id === $user->id && $estimate->status === 'draft';
}
/**
@ -56,7 +76,18 @@ class EstimatePolicy
*/
public function delete(User $user, Estimate $estimate): bool
{
return false;
// Super admin has global access
if ($user->hasRole('super_admin')) {
return true;
}
// Service supervisors and admins can delete estimates in their branch
if ($user->hasAnyRole(['service_supervisor', 'admin'], $user->branch_code)) {
return true;
}
// Creator can delete their own draft estimates
return $estimate->prepared_by_id === $user->id && $estimate->status === 'draft';
}
/**

View File

@ -10,6 +10,7 @@
"license": "MIT",
"require": {
"php": "^8.2",
"barryvdh/laravel-dompdf": "^3.1",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"livewire/flux": "^2.1.1",

367
composer.lock generated
View File

@ -4,8 +4,85 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "86ce39c32f1ba24deda1b62e36de786b",
"content-hash": "7f82283c737928e3e0f4cf0046dee15c",
"packages": [
{
"name": "barryvdh/laravel-dompdf",
"version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-dompdf.git",
"reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d",
"reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d",
"shasum": ""
},
"require": {
"dompdf/dompdf": "^3.0",
"illuminate/support": "^9|^10|^11|^12",
"php": "^8.1"
},
"require-dev": {
"larastan/larastan": "^2.7|^3.0",
"orchestra/testbench": "^7|^8|^9|^10",
"phpro/grumphp": "^2.5",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"PDF": "Barryvdh\\DomPDF\\Facade\\Pdf",
"Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf"
},
"providers": [
"Barryvdh\\DomPDF\\ServiceProvider"
]
},
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"Barryvdh\\DomPDF\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "A DOMPDF Wrapper for Laravel",
"keywords": [
"dompdf",
"laravel",
"pdf"
],
"support": {
"issues": "https://github.com/barryvdh/laravel-dompdf/issues",
"source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.1"
},
"funding": [
{
"url": "https://fruitcake.nl",
"type": "custom"
},
{
"url": "https://github.com/barryvdh",
"type": "github"
}
],
"time": "2025-02-13T15:07:54+00:00"
},
{
"name": "brick/math",
"version": "0.13.1",
@ -426,6 +503,161 @@
],
"time": "2024-02-05T11:56:58+00:00"
},
{
"name": "dompdf/dompdf",
"version": "v3.1.0",
"source": {
"type": "git",
"url": "https://github.com/dompdf/dompdf.git",
"reference": "a51bd7a063a65499446919286fb18b518177155a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/a51bd7a063a65499446919286fb18b518177155a",
"reference": "a51bd7a063a65499446919286fb18b518177155a",
"shasum": ""
},
"require": {
"dompdf/php-font-lib": "^1.0.0",
"dompdf/php-svg-lib": "^1.0.0",
"ext-dom": "*",
"ext-mbstring": "*",
"masterminds/html5": "^2.0",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"ext-gd": "*",
"ext-json": "*",
"ext-zip": "*",
"mockery/mockery": "^1.3",
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "^3.5",
"symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
},
"suggest": {
"ext-gd": "Needed to process images",
"ext-gmagick": "Improves image processing performance",
"ext-imagick": "Improves image processing performance",
"ext-zlib": "Needed for pdf stream compression"
},
"type": "library",
"autoload": {
"psr-4": {
"Dompdf\\": "src/"
},
"classmap": [
"lib/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1"
],
"authors": [
{
"name": "The Dompdf Community",
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
}
],
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
"homepage": "https://github.com/dompdf/dompdf",
"support": {
"issues": "https://github.com/dompdf/dompdf/issues",
"source": "https://github.com/dompdf/dompdf/tree/v3.1.0"
},
"time": "2025-01-15T14:09:04+00:00"
},
{
"name": "dompdf/php-font-lib",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-font-lib.git",
"reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
"reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6"
},
"type": "library",
"autoload": {
"psr-4": {
"FontLib\\": "src/FontLib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "The FontLib Community",
"homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
}
],
"description": "A library to read, parse, export and make subsets of different types of font files.",
"homepage": "https://github.com/dompdf/php-font-lib",
"support": {
"issues": "https://github.com/dompdf/php-font-lib/issues",
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.1"
},
"time": "2024-12-02T14:37:59+00:00"
},
{
"name": "dompdf/php-svg-lib",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-svg-lib.git",
"reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/eb045e518185298eb6ff8d80d0d0c6b17aecd9af",
"reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0",
"sabberworm/php-css-parser": "^8.4"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Svg\\": "src/Svg"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0-or-later"
],
"authors": [
{
"name": "The SvgLib Community",
"homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
}
],
"description": "A library to read, parse and export to PDF SVG files.",
"homepage": "https://github.com/dompdf/php-svg-lib",
"support": {
"issues": "https://github.com/dompdf/php-svg-lib/issues",
"source": "https://github.com/dompdf/php-svg-lib/tree/1.0.0"
},
"time": "2024-04-29T13:26:35+00:00"
},
{
"name": "dragonmantank/cron-expression",
"version": "v3.4.0",
@ -2265,6 +2497,73 @@
},
"time": "2025-04-08T15:13:36+00:00"
},
{
"name": "masterminds/html5",
"version": "2.10.0",
"source": {
"type": "git",
"url": "https://github.com/Masterminds/html5-php.git",
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
"shasum": ""
},
"require": {
"ext-dom": "*",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
},
"autoload": {
"psr-4": {
"Masterminds\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Matt Butcher",
"email": "technosophos@gmail.com"
},
{
"name": "Matt Farina",
"email": "matt@mattfarina.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "An HTML5 parser and serializer.",
"homepage": "http://masterminds.github.io/html5-php",
"keywords": [
"HTML5",
"dom",
"html",
"parser",
"querypath",
"serializer",
"xml"
],
"support": {
"issues": "https://github.com/Masterminds/html5-php/issues",
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
},
"time": "2025-07-25T09:04:22+00:00"
},
{
"name": "monolog/monolog",
"version": "3.9.0",
@ -3688,6 +3987,72 @@
},
"time": "2025-06-25T14:20:11+00:00"
},
{
"name": "sabberworm/php-css-parser",
"version": "v8.9.0",
"source": {
"type": "git",
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9",
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
},
"require-dev": {
"phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41",
"rawr/cross-data-providers": "^2.0.0"
},
"suggest": {
"ext-mbstring": "for parsing UTF-8 CSS"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "9.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Sabberworm\\CSS\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Raphael Schweikert"
},
{
"name": "Oliver Klee",
"email": "github@oliverklee.de"
},
{
"name": "Jake Hotson",
"email": "jake.github@qzdesign.co.uk"
}
],
"description": "Parser for CSS Files written in PHP",
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"keywords": [
"css",
"parser",
"stylesheet"
],
"support": {
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0"
},
"time": "2025-07-11T13:20:48+00:00"
},
{
"name": "spatie/laravel-activitylog",
"version": "4.10.2",

301
config/dompdf.php Normal file
View File

@ -0,0 +1,301 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Settings
|--------------------------------------------------------------------------
|
| Set some default values. It is possible to add all defines that can be set
| in dompdf_config.inc.php. You can also override the entire config file.
|
*/
'show_warnings' => false, // Throw an Exception on warnings from dompdf
'public_path' => null, // Override the public path if needed
/*
* Dejavu Sans font is missing glyphs for converted entities, turn it off if you need to show and £.
*/
'convert_entities' => true,
'options' => [
/**
* The location of the DOMPDF font directory
*
* The location of the directory where DOMPDF will store fonts and font metrics
* Note: This directory must exist and be writable by the webserver process.
* *Please note the trailing slash.*
*
* Notes regarding fonts:
* Additional .afm font metrics can be added by executing load_font.php from command line.
*
* Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must
* be embedded in the pdf file or the PDF may not display correctly. This can significantly
* increase file size unless font subsetting is enabled. Before embedding a font please
* review your rights under the font license.
*
* Any font specification in the source HTML is translated to the closest font available
* in the font directory.
*
* The pdf standard "Base 14 fonts" are:
* Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
* Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
* Symbol, ZapfDingbats.
*/
'font_dir' => storage_path('fonts'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
/**
* The location of the DOMPDF font cache directory
*
* This directory contains the cached font metrics for the fonts used by DOMPDF.
* This directory can be the same as DOMPDF_FONT_DIR
*
* Note: This directory must exist and be writable by the webserver process.
*/
'font_cache' => storage_path('fonts'),
/**
* The location of a temporary directory.
*
* The directory specified must be writeable by the webserver process.
* The temporary directory is required to download remote images and when
* using the PDFLib back end.
*/
'temp_dir' => sys_get_temp_dir(),
/**
* ==== IMPORTANT ====
*
* dompdf's "chroot": Prevents dompdf from accessing system files or other
* files on the webserver. All local files opened by dompdf must be in a
* subdirectory of this directory. DO NOT set it to '/' since this could
* allow an attacker to use dompdf to read any files on the server. This
* should be an absolute path.
* This is only checked on command line call by dompdf.php, but not by
* direct class use like:
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
*/
'chroot' => realpath(base_path()),
/**
* Protocol whitelist
*
* Protocols and PHP wrappers allowed in URIs, and the validation rules
* that determine if a resouce may be loaded. Full support is not guaranteed
* for the protocols/wrappers specified
* by this array.
*
* @var array
*/
'allowed_protocols' => [
'data://' => ['rules' => []],
'file://' => ['rules' => []],
'http://' => ['rules' => []],
'https://' => ['rules' => []],
],
/**
* Operational artifact (log files, temporary files) path validation
*/
'artifactPathValidation' => null,
/**
* @var string
*/
'log_output_file' => null,
/**
* Whether to enable font subsetting or not.
*/
'enable_font_subsetting' => false,
/**
* The PDF rendering backend to use
*
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
* fall back on CPDF. 'GD' renders PDFs to graphic files.
* {@link * Canvas_Factory} ultimately determines which rendering class to
* instantiate based on this setting.
*
* Both PDFLib & CPDF rendering backends provide sufficient rendering
* capabilities for dompdf, however additional features (e.g. object,
* image and font support, etc.) differ between backends. Please see
* {@link PDFLib_Adapter} for more information on the PDFLib backend
* and {@link CPDF_Adapter} and lib/class.pdf.php for more information
* on CPDF. Also see the documentation for each backend at the links
* below.
*
* The GD rendering backend is a little different than PDFLib and
* CPDF. Several features of CPDF and PDFLib are not supported or do
* not make any sense when creating image files. For example,
* multiple pages are not supported, nor are PDF 'objects'. Have a
* look at {@link GD_Adapter} for more information. GD support is
* experimental, so use it at your own risk.
*
* @link http://www.pdflib.com
* @link http://www.ros.co.nz/pdf
* @link http://www.php.net/image
*/
'pdf_backend' => 'CPDF',
/**
* html target media view which should be rendered into pdf.
* List of types and parsing rules for future extensions:
* http://www.w3.org/TR/REC-html40/types.html
* screen, tty, tv, projection, handheld, print, braille, aural, all
* Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.
* Note, even though the generated pdf file is intended for print output,
* the desired content might be different (e.g. screen or projection view of html file).
* Therefore allow specification of content here.
*/
'default_media_type' => 'screen',
/**
* The default paper size.
*
* North America standard is "letter"; other countries generally "a4"
*
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
*/
'default_paper_size' => 'a4',
/**
* The default paper orientation.
*
* The orientation of the page (portrait or landscape).
*
* @var string
*/
'default_paper_orientation' => 'portrait',
/**
* The default font family
*
* Used if no suitable fonts can be found. This must exist in the font folder.
*
* @var string
*/
'default_font' => 'serif',
/**
* Image DPI setting
*
* This setting determines the default DPI setting for images and fonts. The
* DPI may be overridden for inline images by explictly setting the
* image's width & height style attributes (i.e. if the image's native
* width is 600 pixels and you specify the image's width as 72 points,
* the image will have a DPI of 600 in the rendered PDF. The DPI of
* background images can not be overridden and is controlled entirely
* via this parameter.
*
* For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
* If a size in html is given as px (or without unit as image size),
* this tells the corresponding size in pt.
* This adjusts the relative sizes to be similar to the rendering of the
* html page in a reference browser.
*
* In pdf, always 1 pt = 1/72 inch
*
* Rendering resolution of various browsers in px per inch:
* Windows Firefox and Internet Explorer:
* SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?
* Linux Firefox:
* about:config *resolution: Default:96
* (xorg screen dimension in mm and Desktop font dpi settings are ignored)
*
* Take care about extra font/image zoom factor of browser.
*
* In images, <img> size in pixel attribute, img css style, are overriding
* the real image dimension in px for rendering.
*
* @var int
*/
'dpi' => 96,
/**
* Enable embedded PHP
*
* If this setting is set to true then DOMPDF will automatically evaluate embedded PHP contained
* within <script type="text/php"> ... </script> tags.
*
* ==== IMPORTANT ==== Enabling this for documents you do not trust (e.g. arbitrary remote html pages)
* is a security risk.
* Embedded scripts are run with the same level of system access available to dompdf.
* Set this option to false (recommended) if you wish to process untrusted documents.
* This setting may increase the risk of system exploit.
* Do not change this settings without understanding the consequences.
* Additional documentation is available on the dompdf wiki at:
* https://github.com/dompdf/dompdf/wiki
*
* @var bool
*/
'enable_php' => false,
/**
* Rnable inline JavaScript
*
* If this setting is set to true then DOMPDF will automatically insert JavaScript code contained
* within <script type="text/javascript"> ... </script> tags as written into the PDF.
* NOTE: This is PDF-based JavaScript to be executed by the PDF viewer,
* not browser-based JavaScript executed by Dompdf.
*
* @var bool
*/
'enable_javascript' => true,
/**
* Enable remote file access
*
* If this setting is set to true, DOMPDF will access remote sites for
* images and CSS files as required.
*
* ==== IMPORTANT ====
* This can be a security risk, in particular in combination with isPhpEnabled and
* allowing remote html code to be passed to $dompdf = new DOMPDF(); $dompdf->load_html(...);
* This allows anonymous users to download legally doubtful internet content which on
* tracing back appears to being downloaded by your server, or allows malicious php code
* in remote html pages to be executed by your server with your account privileges.
*
* This setting may increase the risk of system exploit. Do not change
* this settings without understanding the consequences. Additional
* documentation is available on the dompdf wiki at:
* https://github.com/dompdf/dompdf/wiki
*
* @var bool
*/
'enable_remote' => false,
/**
* List of allowed remote hosts
*
* Each value of the array must be a valid hostname.
*
* This will be used to filter which resources can be loaded in combination with
* isRemoteEnabled. If enable_remote is FALSE, then this will have no effect.
*
* Leave to NULL to allow any remote host.
*
* @var array|null
*/
'allowed_remote_hosts' => null,
/**
* A ratio applied to the fonts height to be more like browsers' line height
*/
'font_height_ratio' => 1.1,
/**
* Use the HTML5 Lib parser
*
* @deprecated This feature is now always on in dompdf 2.x
*
* @var bool
*/
'enable_html5_parser' => true,
],
];

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Update any existing 'labor' values to 'labour' first
DB::statement("UPDATE estimate_line_items SET type = 'labour' WHERE type = 'labor'");
// Modify the enum to use British spelling
DB::statement("ALTER TABLE estimate_line_items MODIFY COLUMN type ENUM('labour', 'parts', 'miscellaneous') NOT NULL");
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Update any existing 'labour' values back to 'labor'
DB::statement("UPDATE estimate_line_items SET type = 'labor' WHERE type = 'labour'");
// Revert the enum to use American spelling
DB::statement("ALTER TABLE estimate_line_items MODIFY COLUMN type ENUM('labor', 'parts', 'miscellaneous') NOT NULL");
}
};

View File

@ -0,0 +1,68 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('invoices', function (Blueprint $table) {
$table->id();
$table->string('invoice_number')->unique();
$table->string('status')->default('draft'); // draft, sent, paid, overdue, cancelled
// Relationships
$table->foreignId('customer_id')->constrained()->onDelete('cascade');
$table->foreignId('service_order_id')->nullable()->constrained()->onDelete('set null');
$table->foreignId('job_card_id')->nullable()->constrained()->onDelete('set null');
$table->foreignId('estimate_id')->nullable()->constrained()->onDelete('set null');
$table->foreignId('branch_id')->constrained()->onDelete('cascade');
$table->foreignId('created_by')->constrained('users')->onDelete('cascade');
// Invoice Details
$table->date('invoice_date');
$table->date('due_date');
$table->text('description')->nullable();
$table->text('notes')->nullable();
$table->text('terms_and_conditions')->nullable();
// Financial Information
$table->decimal('subtotal', 10, 2)->default(0);
$table->decimal('tax_rate', 5, 2)->default(0);
$table->decimal('tax_amount', 10, 2)->default(0);
$table->decimal('discount_amount', 10, 2)->default(0);
$table->decimal('total_amount', 10, 2)->default(0);
// Payment Information
$table->timestamp('paid_at')->nullable();
$table->string('payment_method')->nullable(); // cash, card, check, bank_transfer
$table->string('payment_reference')->nullable();
$table->text('payment_notes')->nullable();
// Delivery Information
$table->timestamp('sent_at')->nullable();
$table->string('sent_method')->nullable(); // email, print, portal
$table->string('sent_to')->nullable(); // email address or delivery method
$table->timestamps();
// Indexes
$table->index(['customer_id', 'status']);
$table->index(['branch_id', 'invoice_date']);
$table->index(['status', 'due_date']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('invoices');
}
};

View File

@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('invoice_line_items', function (Blueprint $table) {
$table->id();
$table->foreignId('invoice_id')->constrained()->onDelete('cascade');
// Line Item Details
$table->string('type')->default('labour'); // labour, parts, miscellaneous
$table->string('description');
$table->decimal('quantity', 8, 2)->default(1);
$table->decimal('unit_price', 10, 2);
$table->decimal('total_amount', 10, 2);
// Optional References
$table->foreignId('part_id')->nullable()->constrained()->onDelete('set null');
$table->foreignId('service_item_id')->nullable()->constrained()->onDelete('set null');
$table->string('part_number')->nullable();
$table->text('technical_notes')->nullable();
$table->timestamps();
// Indexes
$table->index(['invoice_id', 'type']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('invoice_line_items');
}
};

View File

@ -0,0 +1,111 @@
<?php
namespace Database\Seeders;
use App\Models\Branch;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\InvoiceLineItem;
use App\Models\Part;
use App\Models\User;
use Illuminate\Database\Seeder;
class InvoiceSeeder extends Seeder
{
/**
* Run the database seeder.
*/
public function run(): void
{
// Get necessary data
$customers = Customer::take(10)->get();
$branch = Branch::first();
$user = User::first();
$parts = Part::take(5)->get();
if ($customers->isEmpty() || ! $branch || ! $user) {
$this->command->warn('Skipping invoice seeder - missing required data (customers, branch, or user)');
return;
}
// Create sample invoices
foreach ($customers as $index => $customer) {
$invoice = Invoice::create([
'invoice_number' => Invoice::generateInvoiceNumber($branch->code),
'status' => collect(['draft', 'sent', 'paid', 'overdue'])->random(),
'customer_id' => $customer->id,
'branch_id' => $branch->id,
'created_by' => $user->id,
'invoice_date' => now()->subDays(rand(0, 30)),
'due_date' => now()->addDays(rand(15, 45)),
'description' => 'Vehicle maintenance and repair services',
'notes' => $index % 3 === 0 ? 'Customer requested expedited service. All work completed as specified.' : null,
'terms_and_conditions' => 'Payment due within 30 days. Late payments subject to 1.5% monthly service charge.',
'tax_rate' => 8.50,
'discount_amount' => $index % 4 === 0 ? rand(10, 50) : 0,
]);
// Add line items
$numItems = rand(2, 5);
for ($i = 0; $i < $numItems; $i++) {
$type = collect(['labour', 'parts', 'miscellaneous'])->random();
$quantity = rand(1, 3);
$unitPrice = match ($type) {
'labour' => rand(75, 150),
'parts' => rand(25, 300),
'miscellaneous' => rand(10, 75),
};
$part = $parts->random();
InvoiceLineItem::create([
'invoice_id' => $invoice->id,
'type' => $type,
'description' => match ($type) {
'labour' => collect([
'Brake system inspection and repair',
'Engine diagnostic and tune-up',
'Transmission service and fluid change',
'Air conditioning system service',
'Tire rotation and alignment',
'Oil change and filter replacement',
])->random(),
'parts' => $part->name ?? 'Replacement part',
'miscellaneous' => collect([
'Shop supplies and consumables',
'Environmental disposal fee',
'Vehicle inspection fee',
'Diagnostic software fee',
])->random(),
},
'quantity' => $quantity,
'unit_price' => $unitPrice,
'total_amount' => $quantity * $unitPrice,
'part_id' => $type === 'parts' ? $part->id : null,
'part_number' => $type === 'parts' ? $part->part_number : null,
'technical_notes' => $type === 'labour' && $i === 0 ? 'Required specialized diagnostic equipment' : null,
]);
}
// Recalculate totals
$invoice->recalculateTotals();
// Mark some invoices as paid
if ($invoice->status === 'paid') {
$invoice->markAsPaid(
collect(['cash', 'card', 'check', 'bank_transfer'])->random(),
'REF-'.strtoupper(\Illuminate\Support\Str::random(6)),
'Payment processed successfully'
);
}
// Mark some invoices as sent
if (in_array($invoice->status, ['sent', 'paid', 'overdue'])) {
$invoice->markAsSent('email', $customer->email);
}
}
$this->command->info('Created '.$customers->count().' sample invoices with line items');
}
}

View File

@ -2,8 +2,8 @@
namespace Database\Seeders;
use App\Models\Role;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
@ -122,6 +122,15 @@ class RolesAndPermissionsSeeder extends Seeder
['name' => 'diagnosis.create', 'display_name' => 'Create Diagnosis', 'description' => 'Can create diagnosis reports'],
],
// Invoices
'invoices' => [
['name' => 'invoices.view', 'display_name' => 'View Invoices', 'description' => 'Can view invoices'],
['name' => 'invoices.create', 'display_name' => 'Create Invoices', 'description' => 'Can create new invoices'],
['name' => 'invoices.update', 'display_name' => 'Edit Invoices', 'description' => 'Can edit invoices'],
['name' => 'invoices.send', 'display_name' => 'Send Invoices', 'description' => 'Can send invoices to customers'],
['name' => 'invoices.payment', 'display_name' => 'Record Payments', 'description' => 'Can record invoice payments'],
],
// Reports & Analytics
'reports' => [
['name' => 'reports.view', 'display_name' => 'View Reports', 'description' => 'Can view business reports'],
@ -160,7 +169,7 @@ class RolesAndPermissionsSeeder extends Seeder
'name' => 'super_admin',
'display_name' => 'Super Administrator',
'description' => 'Full system access with all permissions',
'permissions' => 'all' // Special case - gets all permissions
'permissions' => 'all', // Special case - gets all permissions
],
[
'name' => 'manager',
@ -178,9 +187,10 @@ class RolesAndPermissionsSeeder extends Seeder
'technicians.view', 'technicians.create', 'technicians.update', 'technicians.view-performance', 'technicians.schedules',
'inspections.view', 'inspections.create', 'inspections.update', 'inspections.approve', 'inspections.reschedule',
'estimates.view', 'estimates.create', 'estimates.update', 'estimates.approve', 'diagnosis.view', 'diagnosis.create',
'invoices.view', 'invoices.create', 'invoices.update', 'invoices.send', 'invoices.payment',
'reports.view', 'reports.create', 'reports.export', 'reports.financial',
'timesheets.view', 'timesheets.approve',
]
],
],
[
'name' => 'service_advisor',
@ -193,9 +203,10 @@ class RolesAndPermissionsSeeder extends Seeder
'service-orders.view', 'service-orders.create', 'service-orders.update',
'appointments.view', 'appointments.create', 'appointments.update', 'appointments.confirm',
'estimates.view', 'estimates.create', 'diagnosis.view',
'invoices.view', 'invoices.create',
'inventory.view', 'inventory.stock-movements',
'inspections.view', 'inspections.create',
]
],
],
[
'name' => 'technician',
@ -212,7 +223,7 @@ class RolesAndPermissionsSeeder extends Seeder
'inspections.view', 'inspections.create', 'inspections.update',
'diagnosis.view', 'diagnosis.create',
'timesheets.view', 'timesheets.create', 'timesheets.update',
]
],
],
[
'name' => 'inventory_manager',
@ -224,7 +235,7 @@ class RolesAndPermissionsSeeder extends Seeder
'inventory.purchase-orders', 'inventory.purchase-orders-approve',
'service-orders.view', 'work-orders.view',
'reports.view', 'reports.create', 'reports.export',
]
],
],
[
'name' => 'customer_portal',
@ -235,7 +246,7 @@ class RolesAndPermissionsSeeder extends Seeder
'vehicles.view', 'vehicles.history',
'service-orders.view',
'estimates.view',
]
],
],
];
@ -261,7 +272,7 @@ class RolesAndPermissionsSeeder extends Seeder
->where('is_active', true)
->pluck('id')
->toArray();
$role->permissions()->sync($permissionIds);
}
}

View File

@ -62,6 +62,12 @@
</flux:menu.item>
@endif
@if(auth()->user()->hasPermission('invoices.create'))
<flux:menu.item icon="receipt-percent" href="{{ route('invoices.create') }}" wire:navigate>
New Invoice
</flux:menu.item>
@endif
@if(auth()->user()->hasPermission('work-orders.create'))
<flux:menu.item icon="wrench-screwdriver" href="{{ route('work-orders.index') }}" wire:navigate>
New Work Order
@ -255,8 +261,8 @@
</flux:navlist.item>
@endif
@if(auth()->user()->hasPermission('service-orders.view'))
<flux:navlist.item icon="receipt-percent" href="#" :current="request()->is('invoices*')" wire:navigate>
@if(auth()->user()->hasPermission('invoices.view'))
<flux:navlist.item icon="receipt-percent" href="{{ route('invoices.index') }}" :current="request()->is('invoices*')" wire:navigate>
Invoices
</flux:navlist.item>
@endif
@ -455,11 +461,22 @@
document.addEventListener('alpine:init', () => {
Alpine.data('sidebarState', () => ({
collapsed: JSON.parse(localStorage.getItem('sidebarCollapsed') || '{}'),
screenLg: window.innerWidth >= 1024,
init() {
this.$watch('collapsed', (value) => {
localStorage.setItem('sidebarCollapsed', JSON.stringify(value));
});
// Handle screen size changes
this.$watch('screenLg', (value) => {
// Optional: do something when screen size changes
});
// Listen for resize events
window.addEventListener('resize', () => {
this.screenLg = window.innerWidth >= 1024;
});
}
}));
});

View File

@ -0,0 +1,419 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Estimate #{{ $estimate->estimate_number }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'DejaVu Sans', Arial, sans-serif;
font-size: 11px;
line-height: 1.4;
color: #333;
}
.header {
border-bottom: 3px solid #f97316;
padding-bottom: 20px;
margin-bottom: 30px;
}
.header-content {
display: table;
width: 100%;
}
.company-info {
display: table-cell;
width: 50%;
vertical-align: top;
}
.estimate-info {
display: table-cell;
width: 50%;
vertical-align: top;
text-align: right;
}
.company-name {
font-size: 20px;
font-weight: bold;
color: #f97316;
margin-bottom: 5px;
}
.estimate-title {
font-size: 24px;
font-weight: bold;
color: #1f2937;
margin-bottom: 10px;
}
.estimate-number {
font-size: 14px;
font-weight: bold;
margin-bottom: 5px;
}
.section {
margin-bottom: 25px;
}
.section-title {
font-size: 14px;
font-weight: bold;
color: #1f2937;
margin-bottom: 10px;
border-bottom: 1px solid #e5e7eb;
padding-bottom: 5px;
}
.info-grid {
display: table;
width: 100%;
margin-bottom: 20px;
}
.info-column {
display: table-cell;
width: 50%;
vertical-align: top;
padding-right: 20px;
}
.info-row {
margin-bottom: 8px;
}
.info-label {
font-weight: bold;
display: inline-block;
width: 100px;
}
.status-badge {
padding: 3px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: bold;
text-transform: uppercase;
}
.status-draft {
background-color: #f3f4f6;
color: #374151;
}
.status-sent {
background-color: #dbeafe;
color: #1e40af;
}
.status-approved {
background-color: #d1fae5;
color: #065f46;
}
.status-rejected {
background-color: #fee2e2;
color: #991b1b;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.items-table th,
.items-table td {
border: 1px solid #e5e7eb;
padding: 8px;
text-align: left;
}
.items-table th {
background-color: #f9fafb;
font-weight: bold;
font-size: 10px;
text-transform: uppercase;
}
.items-table td {
font-size: 11px;
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
.type-badge {
padding: 2px 6px;
border-radius: 3px;
font-size: 9px;
font-weight: bold;
text-transform: uppercase;
}
.type-labour {
background-color: #dbeafe;
color: #1e40af;
}
.type-parts {
background-color: #d1fae5;
color: #065f46;
}
.type-miscellaneous {
background-color: #f3f4f6;
color: #374151;
}
.summary-table {
width: 300px;
margin-left: auto;
margin-top: 20px;
}
.summary-table td {
padding: 5px 10px;
border: none;
}
.summary-row {
border-bottom: 1px solid #e5e7eb;
}
.total-row {
font-weight: bold;
font-size: 14px;
border-top: 2px solid #f97316;
background-color: #fff7ed;
}
.notes-section {
margin-top: 30px;
padding: 15px;
background-color: #f9fafb;
border-radius: 5px;
}
.terms-section {
margin-top: 20px;
font-size: 10px;
color: #6b7280;
border-top: 1px solid #e5e7eb;
padding-top: 15px;
}
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
text-align: center;
font-size: 9px;
color: #9ca3af;
border-top: 1px solid #e5e7eb;
padding: 10px 0;
background-color: white;
}
.page-break {
page-break-before: always;
}
</style>
</head>
<body>
<!-- Header -->
<div class="header">
<div class="header-content">
<div class="company-info">
<div class="company-name">{{ app(\App\Settings\GeneralSettings::class)->shop_name ?? 'Car Repairs Shop' }}</div>
<div>{{ app(\App\Settings\GeneralSettings::class)->shop_address ?? 'Shop Address' }}</div>
<div>{{ app(\App\Settings\GeneralSettings::class)->shop_phone ?? 'Phone Number' }}</div>
<div>{{ app(\App\Settings\GeneralSettings::class)->shop_email ?? 'Email Address' }}</div>
</div>
<div class="estimate-info">
<div class="estimate-title">ESTIMATE</div>
<div class="estimate-number">#{{ $estimate->estimate_number }}</div>
<div>Date: {{ $estimate->created_at->format('M j, Y') }}</div>
@if($estimate->validity_period_days)
<div>Valid Until: {{ $estimate->valid_until->format('M j, Y') }}</div>
@endif
<div style="margin-top: 10px;">
<span class="status-badge status-{{ $estimate->status }}">{{ ucfirst($estimate->status) }}</span>
</div>
</div>
</div>
</div>
<!-- Customer Information -->
<div class="section">
<div class="section-title">Customer Information</div>
<div class="info-grid">
<div class="info-column">
@php
$customer = $estimate->customer ?? $estimate->jobCard?->customer;
@endphp
@if($customer)
<div class="info-row">
<span class="info-label">Name:</span>
{{ $customer->name }}
</div>
<div class="info-row">
<span class="info-label">Email:</span>
{{ $customer->email }}
</div>
<div class="info-row">
<span class="info-label">Phone:</span>
{{ $customer->phone }}
</div>
@else
<div class="info-row">No customer information available</div>
@endif
</div>
<div class="info-column">
@php
$vehicle = $estimate->vehicle ?? $estimate->jobCard?->vehicle;
@endphp
@if($vehicle)
<div class="info-row">
<span class="info-label">Vehicle:</span>
{{ $vehicle->year }} {{ $vehicle->make }} {{ $vehicle->model }}
</div>
<div class="info-row">
<span class="info-label">License:</span>
{{ $vehicle->license_plate }}
</div>
@if($vehicle->vin)
<div class="info-row">
<span class="info-label">VIN:</span>
{{ $vehicle->vin }}
</div>
@endif
@else
<div class="info-row">No vehicle specified</div>
@endif
</div>
</div>
</div>
<!-- Job Card Information (if applicable) -->
@if($estimate->jobCard)
<div class="section">
<div class="section-title">Job Information</div>
<div class="info-row">
<span class="info-label">Job Card:</span>
#{{ $estimate->jobCard->job_number }}
</div>
@if($estimate->diagnosis)
<div class="info-row">
<span class="info-label">Diagnosis:</span>
{{ Str::limit($estimate->diagnosis->findings, 100) }}
</div>
@endif
</div>
@endif
<!-- Line Items -->
<div class="section">
<div class="section-title">Services & Parts</div>
@if($estimate->lineItems->count() > 0)
<table class="items-table">
<thead>
<tr>
<th>Type</th>
<th>Description</th>
<th class="text-center">Qty</th>
<th class="text-right">Unit Price</th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody>
@foreach($estimate->lineItems as $item)
<tr>
<td>
<span class="type-badge type-{{ $item->type }}">
{{ ucfirst($item->type) }}
</span>
</td>
<td>
{{ $item->description }}
@if($item->part)
<br><small style="color: #6b7280;">Part #: {{ $item->part->part_number }}</small>
@endif
</td>
<td class="text-center">{{ $item->quantity }}</td>
<td class="text-right">${{ number_format($item->unit_price, 2) }}</td>
<td class="text-right">${{ number_format($item->total_amount, 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
@else
<div style="text-align: center; padding: 20px; color: #6b7280;">
No line items available
</div>
@endif
</div>
<!-- Summary -->
<table class="summary-table">
<tr class="summary-row">
<td>Subtotal:</td>
<td class="text-right">${{ number_format($estimate->subtotal, 2) }}</td>
</tr>
@if($estimate->discount_amount > 0)
<tr class="summary-row">
<td>Discount:</td>
<td class="text-right" style="color: #dc2626;">-${{ number_format($estimate->discount_amount, 2) }}</td>
</tr>
@endif
<tr class="summary-row">
<td>Tax ({{ $estimate->tax_rate }}%):</td>
<td class="text-right">${{ number_format($estimate->tax_amount, 2) }}</td>
</tr>
<tr class="total-row">
<td><strong>Total:</strong></td>
<td class="text-right"><strong>${{ number_format($estimate->total_amount, 2) }}</strong></td>
</tr>
</table>
<!-- Customer Notes -->
@if($estimate->notes)
<div class="notes-section">
<div class="section-title">Notes</div>
<div style="white-space: pre-wrap;">{{ $estimate->notes }}</div>
</div>
@endif
<!-- Terms & Conditions -->
@if($estimate->terms_and_conditions)
<div class="terms-section">
<div style="font-weight: bold; margin-bottom: 10px;">Terms & Conditions</div>
<div style="white-space: pre-wrap;">{{ $estimate->terms_and_conditions }}</div>
</div>
@endif
<!-- Footer -->
<div class="footer">
<div>Estimate generated on {{ now()->format('M j, Y g:i A') }}</div>
@if($estimate->preparedBy)
<div>Prepared by: {{ $estimate->preparedBy->name }}</div>
@endif
</div>
</body>
</html>

View File

@ -19,19 +19,25 @@ if ($stashable) {
$attributes = $attributes->merge([
'x-bind:data-stashed' => '! screenLg',
'x-resize.document' => 'screenLg = window.innerWidth >= 1024',
'x-init' => '$el.classList.add(\'-translate-x-full\', \'rtl:translate-x-full\'); $el.removeAttribute(\'data-mobile-cloak\'); $el.classList.add(\'transition-transform\')',
'x-init' => 'screenLg = window.innerWidth >= 1024; $el.classList.add(\'-translate-x-full\', \'rtl:translate-x-full\'); $el.removeAttribute(\'data-mobile-cloak\'); $el.classList.add(\'transition-transform\')',
])->class([
'max-lg:data-mobile-cloak:hidden',
'[[data-show-stashed-sidebar]_&]:translate-x-0! lg:translate-x-0!',
'z-20! data-stashed:start-0! data-stashed:fixed! data-stashed:top-0! data-stashed:min-h-dvh! data-stashed:max-h-dvh!'
]);
}
// Check if x-data already exists, if not add default
$existingXData = $attributes->get('x-data');
if (!$existingXData) {
$attributes = $attributes->merge(['x-data' => '{ screenLg: window.innerWidth >= 1024 }']);
}
@endphp
@if ($stashable)
<flux:sidebar.backdrop />
@endif
<div {{ $attributes->class($classes) }} x-data="{ screenLg: window.innerWidth >= 1024 }" data-mobile-cloak data-flux-sidebar>
<div {{ $attributes->class($classes) }} data-mobile-cloak data-flux-sidebar>
{{ $slot }}
</div>

View File

@ -0,0 +1,512 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice #{{ $invoice->invoice_number }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'DejaVu Sans', Arial, sans-serif;
font-size: 11px;
line-height: 1.4;
color: #333;
}
.header {
border-bottom: 3px solid #2563eb;
padding-bottom: 20px;
margin-bottom: 30px;
}
.header-content {
display: table;
width: 100%;
}
.company-info {
display: table-cell;
width: 50%;
vertical-align: top;
}
.invoice-info {
display: table-cell;
width: 50%;
vertical-align: top;
text-align: right;
}
.company-name {
font-size: 20px;
font-weight: bold;
color: #2563eb;
margin-bottom: 5px;
}
.invoice-title {
font-size: 24px;
font-weight: bold;
color: #1f2937;
margin-bottom: 10px;
}
.invoice-number {
font-size: 14px;
font-weight: bold;
margin-bottom: 5px;
}
.section {
margin-bottom: 25px;
}
.section-title {
font-size: 14px;
font-weight: bold;
color: #1f2937;
margin-bottom: 10px;
border-bottom: 1px solid #e5e7eb;
padding-bottom: 5px;
}
.info-grid {
display: table;
width: 100%;
margin-bottom: 20px;
}
.info-column {
display: table-cell;
width: 50%;
vertical-align: top;
padding-right: 20px;
}
.info-row {
margin-bottom: 8px;
}
.info-label {
font-weight: bold;
display: inline-block;
width: 100px;
}
.status-badge {
padding: 3px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: bold;
text-transform: uppercase;
}
.status-draft {
background-color: #f3f4f6;
color: #374151;
}
.status-sent {
background-color: #dbeafe;
color: #1e40af;
}
.status-paid {
background-color: #d1fae5;
color: #065f46;
}
.status-overdue {
background-color: #fee2e2;
color: #991b1b;
}
.status-cancelled {
background-color: #f3f4f6;
color: #6b7280;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.items-table th,
.items-table td {
border: 1px solid #e5e7eb;
padding: 8px;
text-align: left;
}
.items-table th {
background-color: #f9fafb;
font-weight: bold;
font-size: 10px;
text-transform: uppercase;
}
.items-table td {
font-size: 11px;
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
.type-badge {
padding: 2px 6px;
border-radius: 3px;
font-size: 9px;
font-weight: bold;
text-transform: uppercase;
}
.type-labour {
background-color: #dbeafe;
color: #1e40af;
}
.type-parts {
background-color: #d1fae5;
color: #065f46;
}
.type-miscellaneous {
background-color: #f3f4f6;
color: #374151;
}
.summary-table {
width: 300px;
margin-left: auto;
margin-top: 20px;
}
.summary-table td {
padding: 5px 10px;
border: none;
}
.summary-row {
border-bottom: 1px solid #e5e7eb;
}
.total-row {
font-weight: bold;
font-size: 14px;
border-top: 2px solid #2563eb;
background-color: #eff6ff;
}
.payment-section {
margin-top: 30px;
padding: 15px;
background-color: #f9fafb;
border-radius: 5px;
}
.notes-section {
margin-top: 20px;
padding: 15px;
background-color: #f9fafb;
border-radius: 5px;
}
.terms-section {
margin-top: 20px;
font-size: 10px;
color: #6b7280;
border-top: 1px solid #e5e7eb;
padding-top: 15px;
}
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
text-align: center;
font-size: 9px;
color: #9ca3af;
border-top: 1px solid #e5e7eb;
padding: 10px 0;
background-color: white;
}
.page-break {
page-break-before: always;
}
.paid-watermark {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(-45deg);
font-size: 72px;
font-weight: bold;
color: rgba(34, 197, 94, 0.1);
z-index: -1;
pointer-events: none;
}
</style>
</head>
<body>
@if($invoice->status === 'paid')
<div class="paid-watermark">PAID</div>
@endif
<!-- Header -->
<div class="header">
<div class="header-content">
<div class="company-info">
<div class="company-name">{{ app(\App\Settings\GeneralSettings::class)->shop_name ?? 'Car Repairs Shop' }}</div>
<div>{{ app(\App\Settings\GeneralSettings::class)->shop_address ?? 'Shop Address' }}</div>
<div>{{ app(\App\Settings\GeneralSettings::class)->shop_phone ?? 'Phone Number' }}</div>
<div>{{ app(\App\Settings\GeneralSettings::class)->shop_email ?? 'Email Address' }}</div>
</div>
<div class="invoice-info">
<div class="invoice-title">INVOICE</div>
<div class="invoice-number">#{{ $invoice->invoice_number }}</div>
<div>Date: {{ $invoice->invoice_date->format('M j, Y') }}</div>
<div>Due: {{ $invoice->due_date->format('M j, Y') }}</div>
@if($invoice->isPaid())
<div>Paid: {{ $invoice->paid_at->format('M j, Y') }}</div>
@endif
<div style="margin-top: 10px;">
<span class="status-badge status-{{ $invoice->status }}">{{ ucfirst($invoice->status) }}</span>
</div>
</div>
</div>
</div>
<!-- Customer Information -->
<div class="section">
<div class="section-title">Bill To</div>
<div class="info-grid">
<div class="info-column">
<div class="info-row">
<span class="info-label">Name:</span>
{{ $invoice->customer->name }}
</div>
<div class="info-row">
<span class="info-label">Email:</span>
{{ $invoice->customer->email }}
</div>
<div class="info-row">
<span class="info-label">Phone:</span>
{{ $invoice->customer->phone }}
</div>
</div>
<div class="info-column">
@if($invoice->customer->address)
<div class="info-row">
<span class="info-label">Address:</span>
{{ $invoice->customer->address }}
</div>
@endif
@if($invoice->customer->city)
<div class="info-row">
<span class="info-label">City:</span>
{{ $invoice->customer->city }}, {{ $invoice->customer->state ?? '' }} {{ $invoice->customer->zip_code ?? '' }}
</div>
@endif
</div>
</div>
</div>
<!-- Vehicle Information (if applicable) -->
@php
$vehicle = $invoice->serviceOrder?->vehicle ?? $invoice->jobCard?->vehicle;
@endphp
@if($vehicle)
<div class="section">
<div class="section-title">Vehicle Information</div>
<div class="info-grid">
<div class="info-column">
<div class="info-row">
<span class="info-label">Vehicle:</span>
{{ $vehicle->year }} {{ $vehicle->make }} {{ $vehicle->model }}
</div>
<div class="info-row">
<span class="info-label">License:</span>
{{ $vehicle->license_plate }}
</div>
</div>
<div class="info-column">
@if($vehicle->vin)
<div class="info-row">
<span class="info-label">VIN:</span>
{{ $vehicle->vin }}
</div>
@endif
@if($vehicle->mileage)
<div class="info-row">
<span class="info-label">Mileage:</span>
{{ number_format($vehicle->mileage) }} miles
</div>
@endif
</div>
</div>
</div>
@endif
<!-- Service Information (if applicable) -->
@if($invoice->serviceOrder || $invoice->jobCard || $invoice->estimate)
<div class="section">
<div class="section-title">Service Information</div>
@if($invoice->serviceOrder)
<div class="info-row">
<span class="info-label">Service Order:</span>
#{{ $invoice->serviceOrder->order_number }}
</div>
@endif
@if($invoice->jobCard)
<div class="info-row">
<span class="info-label">Job Card:</span>
#{{ $invoice->jobCard->job_number }}
</div>
@endif
@if($invoice->estimate)
<div class="info-row">
<span class="info-label">Estimate:</span>
#{{ $invoice->estimate->estimate_number }}
</div>
@endif
</div>
@endif
<!-- Line Items -->
<div class="section">
<div class="section-title">Services & Parts</div>
@if($invoice->lineItems->count() > 0)
<table class="items-table">
<thead>
<tr>
<th>Type</th>
<th>Description</th>
<th class="text-center">Qty</th>
<th class="text-right">Unit Price</th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody>
@foreach($invoice->lineItems as $item)
<tr>
<td>
<span class="type-badge type-{{ $item->type }}">
{{ ucfirst($item->type) }}
</span>
</td>
<td>
{{ $item->description }}
@if($item->part)
<br><small style="color: #6b7280;">Part #: {{ $item->part->part_number }}</small>
@endif
@if($item->part_number)
<br><small style="color: #6b7280;">Part #: {{ $item->part_number }}</small>
@endif
@if($item->technical_notes)
<br><small style="color: #6b7280;">{{ $item->technical_notes }}</small>
@endif
</td>
<td class="text-center">{{ $item->quantity }}</td>
<td class="text-right">${{ number_format($item->unit_price, 2) }}</td>
<td class="text-right">${{ number_format($item->total_amount, 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
@else
<div style="text-align: center; padding: 20px; color: #6b7280;">
No line items available
</div>
@endif
</div>
<!-- Summary -->
<table class="summary-table">
<tr class="summary-row">
<td>Subtotal:</td>
<td class="text-right">${{ number_format($invoice->subtotal, 2) }}</td>
</tr>
@if($invoice->discount_amount > 0)
<tr class="summary-row">
<td>Discount:</td>
<td class="text-right" style="color: #dc2626;">-${{ number_format($invoice->discount_amount, 2) }}</td>
</tr>
@endif
<tr class="summary-row">
<td>Tax ({{ $invoice->tax_rate }}%):</td>
<td class="text-right">${{ number_format($invoice->tax_amount, 2) }}</td>
</tr>
<tr class="total-row">
<td><strong>Total:</strong></td>
<td class="text-right"><strong>${{ number_format($invoice->total_amount, 2) }}</strong></td>
</tr>
</table>
<!-- Payment Information -->
@if($invoice->isPaid())
<div class="payment-section">
<div class="section-title">Payment Information</div>
<div>
<p><strong>Payment Date:</strong> {{ $invoice->paid_at->format('M j, Y g:i A') }}</p>
@if($invoice->payment_method)
<p><strong>Payment Method:</strong> {{ ucfirst(str_replace('_', ' ', $invoice->payment_method)) }}</p>
@endif
@if($invoice->payment_reference)
<p><strong>Reference:</strong> {{ $invoice->payment_reference }}</p>
@endif
@if($invoice->payment_notes)
<p><strong>Notes:</strong> {{ $invoice->payment_notes }}</p>
@endif
</div>
</div>
@else
<div class="payment-section">
<div class="section-title">Payment Information</div>
<div>
<p>Payment is due by {{ $invoice->due_date->format('M j, Y') }}.</p>
<p>Please include invoice number {{ $invoice->invoice_number }} with your payment.</p>
<p class="mt-2">
<strong>Payment Methods:</strong> Cash, Credit Card, Check, Bank Transfer
</p>
</div>
</div>
@endif
<!-- Customer Notes -->
@if($invoice->notes)
<div class="notes-section">
<div class="section-title">Notes</div>
<div style="white-space: pre-wrap;">{{ $invoice->notes }}</div>
</div>
@endif
<!-- Terms & Conditions -->
@if($invoice->terms_and_conditions)
<div class="terms-section">
<div style="font-weight: bold; margin-bottom: 10px;">Terms & Conditions</div>
<div style="white-space: pre-wrap;">{{ $invoice->terms_and_conditions }}</div>
</div>
@endif
<!-- Footer -->
<div class="footer">
<div>Invoice generated on {{ now()->format('M j, Y g:i A') }}</div>
@if($invoice->createdBy)
<div>Prepared by: {{ $invoice->createdBy->name }}</div>
@endif
</div>
</body>
</html>

View File

@ -14,6 +14,7 @@
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
@fluxAppearance
</head>
<body class="font-sans antialiased bg-zinc-50 dark:bg-zinc-900 min-h-full">
<div class="min-h-full">
@ -53,5 +54,6 @@
</div>
@livewireScripts
@fluxScripts
</body>
</html>

View File

@ -60,7 +60,7 @@
class="p-2 text-zinc-500 dark:text-zinc-400 hover:text-gray-700 hover:bg-zinc-100 rounded-l-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-indigo-500"
title="Previous {{ $viewType }}"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
@ -69,7 +69,7 @@
class="p-2 text-zinc-500 dark:text-zinc-400 hover:text-gray-700 hover:bg-zinc-100 rounded-r-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-indigo-500"
title="Next {{ $viewType }}"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>

View File

@ -7,7 +7,7 @@
<p class="text-zinc-600 dark:text-zinc-400">Create a new appointment for a customer</p>
</div>
<a href="{{ route('appointments.index') }}" class="inline-flex items-center px-4 py-2 border border-zinc-300 dark:border-zinc-600 hover:bg-zinc-50 dark:hover:bg-zinc-700 text-zinc-700 dark:text-zinc-300 font-medium rounded-lg transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
Back to Appointments
@ -173,7 +173,7 @@
Cancel
</a>
<button type="submit" class="inline-flex items-center justify-center px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
Schedule Appointment

View File

@ -22,14 +22,14 @@
<!-- Quick Actions -->
<div class="flex space-x-2">
<button wire:click="createAppointment" class="inline-flex items-center px-4 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-lg border border-zinc-200 dark:border-zinc-700 hover:shadow-xl transition-all duration-200 transform hover:scale-105">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
New Appointment
</button>
<button class="inline-flex items-center px-4 py-2.5 bg-white dark:bg-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-700 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-lg border border-zinc-300 dark:border-zinc-600 dark:border-gray-600 border border-zinc-200 dark:border-zinc-700 hover:shadow-md transition-all duration-200">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
Export
@ -116,7 +116,11 @@
<input wire:model.live="search"
type="text"
placeholder="Search by customer, vehicle, or service..."
class="block w-full pl-10 pr-4 py-3 border border-zinc-300 dark:border-zinc-600 dark:border-gray-600 rounded-lg bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent border border-zinc-200 dark:border-zinc-700 hover:shadow-md transition-all duration-200" />
class="block w-full pl-10 pr-10 py-3 border border-zinc-300 dark:border-zinc-600 dark:border-gray-600 rounded-lg bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent border border-zinc-200 dark:border-zinc-700 hover:shadow-md transition-all duration-200" />
<!-- Custom loading indicator with calendar icon -->
<div wire:loading wire:target="search" class="absolute right-3 top-1/2 transform -translate-y-1/2">
<flux:icon.calendar class="w-6 h-6 text-zinc-400 animate-bounce" />
</div>
</div>
</div>
@ -207,7 +211,7 @@
</span>
</div>
<div class="flex items-center space-x-2 text-sm text-zinc-500 dark:text-zinc-400 dark:text-gray-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>Showing {{ $appointments->firstItem() }}-{{ $appointments->lastItem() }} of {{ $appointments->total() }}</span>
@ -222,7 +226,7 @@
<th class="group px-6 py-4 text-left text-xs font-semibold text-zinc-500 dark:text-zinc-400 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-600 transition-colors">
<div class="flex items-center space-x-1">
<span>Date & Time</span>
<svg class="w-4 h-4 opacity-0 group-hover:opacity-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 opacity-0 group-hover:opacity-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"></path>
</svg>
</div>
@ -320,7 +324,7 @@
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 rounded-full bg-green-100 dark:bg-green-900/50 flex items-center justify-center">
<svg class="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
@ -333,7 +337,7 @@
</div>
@else
<div class="flex items-center text-gray-400 dark:text-zinc-500 dark:text-zinc-400">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"></path>
</svg>
<span class="text-sm">Unassigned</span>
@ -375,7 +379,7 @@
<button wire:click="confirmAppointment({{ $appointment->id }})"
title="Confirm"
class="text-gray-400 hover:text-green-600 transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</button>
@ -385,7 +389,7 @@
<button wire:click="checkInAppointment({{ $appointment->id }})"
title="Check In"
class="text-gray-400 hover:text-blue-600 transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
</svg>
</button>
@ -395,7 +399,7 @@
<button wire:click="completeAppointment({{ $appointment->id }})"
title="Complete"
class="text-gray-400 hover:text-green-600 transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</button>
@ -405,7 +409,7 @@
<button wire:click="editAppointment({{ $appointment->id }})"
title="Edit"
class="text-gray-400 hover:text-indigo-600 transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
@ -413,7 +417,7 @@
wire:confirm="Are you sure you want to cancel this appointment?"
title="Cancel"
class="text-gray-400 hover:text-red-600 transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
@ -424,7 +428,7 @@
wire:confirm="Mark this appointment as no-show?"
title="No Show"
class="text-gray-400 hover:text-orange-600 transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7a4 4 0 11-8 0 4 4 0 018 0zM9 14a6 6 0 00-6 6v1h12v-1a6 6 0 00-6-6z"></path>
</svg>
</button>
@ -455,7 +459,7 @@
@endif
</p>
<button wire:click="createAppointment" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md border border-zinc-200 dark:border-zinc-700 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors duration-200">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
Schedule Appointment

View File

@ -89,19 +89,19 @@
{{-- Legend --}}
<div class="flex items-center space-x-6 mb-6 text-sm">
<div class="flex items-center space-x-2">
<div class="w-4 h-4 bg-green-100 border border-green-300 rounded"></div>
<div class="w-6 h-6 bg-green-100 border border-green-300 rounded"></div>
<span class="text-zinc-600 dark:text-zinc-400 dark:text-gray-400">Available</span>
</div>
<div class="flex items-center space-x-2">
<div class="w-4 h-4 bg-red-100 border border-red-300 rounded"></div>
<div class="w-6 h-6 bg-red-100 border border-red-300 rounded"></div>
<span class="text-zinc-600 dark:text-zinc-400 dark:text-gray-400">Booked</span>
</div>
<div class="flex items-center space-x-2">
<div class="w-4 h-4 bg-gray-100 border border-zinc-300 dark:border-zinc-600 rounded"></div>
<div class="w-6 h-6 bg-gray-100 border border-zinc-300 dark:border-zinc-600 rounded"></div>
<span class="text-zinc-600 dark:text-zinc-400 dark:text-gray-400">Unavailable</span>
</div>
<div class="flex items-center space-x-2">
<div class="w-4 h-4 bg-blue-100 border border-blue-300 rounded"></div>
<div class="w-6 h-6 bg-blue-100 border border-blue-300 rounded"></div>
<span class="text-zinc-600 dark:text-zinc-400 dark:text-gray-400">Selected</span>
</div>
</div>

View File

@ -9,7 +9,7 @@
<a href="{{ route('branches.index') }}"
class="inline-flex items-center px-4 py-2 bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600 text-zinc-700 dark:text-zinc-300 text-sm font-medium rounded-md transition-colors"
wire:navigate>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Back to Branches
@ -200,7 +200,7 @@
</a>
<button type="submit"
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Create Branch

View File

@ -9,7 +9,7 @@
<a href="{{ route('branches.index') }}"
class="inline-flex items-center px-4 py-2 bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-700 dark:hover:bg-zinc-600 text-zinc-700 dark:text-zinc-300 text-sm font-medium rounded-md transition-colors"
wire:navigate>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Back to Branches
@ -218,7 +218,7 @@
</a>
<button type="submit"
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Update Branch

View File

@ -10,7 +10,7 @@
<a href="{{ route('branches.create') }}"
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors"
wire:navigate>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Add Branch
@ -88,7 +88,7 @@
<th wire:click="sortBy('code')" class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-800">
Code
@if($sortField === 'code')
<svg class="inline w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="inline w-6 h-6 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@if($sortDirection === 'asc')
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
@else
@ -100,7 +100,7 @@
<th wire:click="sortBy('name')" class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-800">
Name
@if($sortField === 'name')
<svg class="inline w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="inline w-6 h-6 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@if($sortDirection === 'asc')
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
@else
@ -112,7 +112,7 @@
<th wire:click="sortBy('city')" class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-800">
Location
@if($sortField === 'city')
<svg class="inline w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="inline w-6 h-6 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@if($sortDirection === 'asc')
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
@else
@ -124,7 +124,7 @@
<th wire:click="sortBy('manager_name')" class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-800">
Manager
@if($sortField === 'manager_name')
<svg class="inline w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="inline w-6 h-6 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@if($sortDirection === 'asc')
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
@else
@ -183,7 +183,7 @@
class="text-zinc-600 dark:text-zinc-400 hover:text-zinc-800 dark:hover:text-zinc-200"
wire:navigate
title="Edit">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</a>
@ -194,7 +194,7 @@
wire:confirm="Are you sure you want to {{ $branch->is_active ? 'deactivate' : 'activate' }} this branch?"
class="text-yellow-600 dark:text-yellow-400 hover:text-yellow-800 dark:hover:text-yellow-200"
title="{{ $branch->is_active ? 'Deactivate' : 'Activate' }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</button>
@ -205,7 +205,7 @@
wire:confirm="Are you sure you want to delete this branch? This action cannot be undone."
class="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200"
title="Delete">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
@ -226,7 +226,7 @@
<a href="{{ route('branches.create') }}"
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors"
wire:navigate>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Add Branch

View File

@ -87,7 +87,7 @@
<div class="mt-3 text-sm text-gray-500">
@foreach($step['details'] as $detail)
<div class="flex items-center mt-1">
<svg class="w-4 h-4 text-gray-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 text-gray-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
</svg>
{{ $detail }}
@ -124,14 +124,14 @@
<p>Contact your service advisor for real-time updates:</p>
@if($jobCard->assignedTo)
<div class="mt-2 flex items-center">
<svg class="w-4 h-4 text-gray-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 text-gray-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3z"/>
</svg>
<span class="font-medium">{{ $jobCard->assignedTo->name }}</span>
</div>
@endif
<div class="mt-1 flex items-center">
<svg class="w-4 h-4 text-gray-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 text-gray-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z"/>
</svg>
<span>{{ app(\App\Settings\GeneralSettings::class)->shop_phone }}</span>

View File

@ -34,11 +34,14 @@
<!-- Filters -->
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 p-4">
<flux:input
wire:model.live="search"
placeholder="Search customers..."
icon="magnifying-glass"
/>
<div class="relative">
<flux:input
wire:model.live="search"
placeholder="Search customers..."
icon="magnifying-glass"
:loading="false"
/>
</div>
<select wire:model.live="status" class="rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-2 focus:ring-blue-500">
<option value="">All Statuses</option>

View File

@ -79,15 +79,15 @@
<div class="flex items-center">
@for($i = 1; $i <= 5; $i++)
@if($i <= floor($metrics['this_week']['customer_satisfaction']))
<svg class="w-4 h-4 text-yellow-400" fill="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 text-yellow-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
@elseif($i <= $metrics['this_week']['customer_satisfaction'])
<svg class="w-4 h-4 text-yellow-200" fill="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 text-yellow-200" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
@else
<svg class="w-4 h-4 text-zinc-300" fill="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 text-zinc-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
@endif

View File

@ -121,7 +121,7 @@
<div class="mt-6 pt-4 border-t border-zinc-200 dark:border-zinc-700">
<a href="{{ route('job-cards.index') }}" class="inline-flex items-center text-sm text-zinc-600 dark:text-zinc-400 hover:text-zinc-800 dark:hover:text-zinc-200">
View all job cards
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>

View File

@ -135,7 +135,7 @@
@if(!$currentTimesheet)
<button type="button" wire:click="startTimesheet"
class="w-full px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-7 4h12l-2 5H9l-2-5z"/>
</svg>
Start Diagnosis
@ -143,7 +143,7 @@
@else
<button type="button" wire:click="endTimesheet"
class="w-full px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"/>
</svg>

View File

@ -10,7 +10,7 @@
</div>
<div class="flex items-center space-x-3">
<button wire:click="refreshList" class="inline-flex items-center px-4 py-2 border border-zinc-300 dark:border-zinc-600 hover:bg-zinc-50 dark:hover:bg-zinc-700 text-zinc-700 dark:text-zinc-300 font-medium rounded-lg transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
Refresh
@ -23,10 +23,14 @@
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 shadow-sm mb-8">
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<div class="relative">
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Search</label>
<input type="text" wire:model.live="search" placeholder="Search by job card number, customer..."
class="w-full rounded-lg border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
class="w-full rounded-lg border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 pr-10">
<!-- Custom loading indicator with medical bag icon -->
<div wire:loading wire:target="search" class="absolute right-3 top-10 transform -translate-y-1/2">
<flux:icon.clipboard-document-check class="w-6 h-6 text-zinc-400 animate-pulse" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Status</label>

View File

@ -10,13 +10,13 @@
</div>
<div class="flex items-center space-x-3">
<a href="{{ route('diagnosis.edit', $diagnosis) }}" class="inline-flex items-center px-4 py-2 border border-zinc-300 dark:border-zinc-600 hover:bg-zinc-50 dark:hover:bg-zinc-700 text-zinc-700 dark:text-zinc-300 font-medium rounded-lg transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
Edit Diagnosis
</a>
<a href="{{ route('job-cards.show', $diagnosis->jobCard) }}" class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors shadow-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
Back to Job Card

View File

@ -47,9 +47,9 @@
<div>
<flux:field>
<flux:label>Vehicle</flux:label>
<flux:label>Vehicle <span class="text-gray-500 text-sm">(Optional)</span></flux:label>
<flux:select wire:model.live="vehicleId" placeholder="Select a vehicle..." :disabled="!$customerId">
<option value="">Select a vehicle...</option>
<option value="">Select a vehicle (optional)...</option>
@foreach($customerVehicles as $vehicle)
<option value="{{ $vehicle->id }}">
{{ $vehicle->year }} {{ $vehicle->make }} {{ $vehicle->model }} - {{ $vehicle->license_plate }}
@ -57,6 +57,15 @@
@endforeach
</flux:select>
<flux:error name="vehicleId" />
@if(!$customerId)
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
Select a customer first to see their vehicles
</div>
@elseif(empty($customerVehicles) && $customerId)
<div class="text-xs text-amber-600 dark:text-amber-400 mt-1">
No vehicles found for this customer. Vehicle selection is optional for estimates.
</div>
@endif
</flux:field>
@if($selectedVehicle)
@ -77,7 +86,7 @@
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Line Items</h3>
<button wire:click="addLineItem" type="button" class="inline-flex items-center px-3 py-2 bg-accent hover:bg-accent-content text-accent-foreground text-sm font-medium rounded-lg transition-colors">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"></path>
</svg>
Add Item
@ -117,7 +126,7 @@
@endif
</div>
<button wire:click="clearPartSelection({{ $index }})" type="button" class="text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
@ -175,25 +184,26 @@
<div class="col-span-2">
<flux:field>
<flux:label>Quantity</flux:label>
<flux:label>Quantity <span class="text-accent">*</span></flux:label>
<flux:input
wire:model.live="lineItems.{{ $index }}.quantity"
type="number"
step="0.01"
min="0.01"
@if($item['type'] === 'parts' && isset($item['stock_available']))
max="{{ $item['stock_available'] }}"
@endif
placeholder="Enter quantity..."
class="font-medium"
autocomplete="off"
/>
@if($item['type'] === 'parts' && isset($item['stock_available']))
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
@if($item['stock_available'] <= 5)
<span class="text-red-600 dark:text-red-400">⚠️ Low stock: {{ $item['stock_available'] }} available</span>
@else
<span class="text-green-600 dark:text-green-400"> {{ $item['stock_available'] }} available</span>
@endif
</div>
@endif
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
@if($item['type'] === 'parts')
Enter number of parts needed
@elseif($item['type'] === 'labour')
Enter hours (e.g., 2.5 for 2.5 hours)
@else
Enter quantity for this item
@endif
</div>
<flux:error name="lineItems.{{ $index }}.quantity" />
</flux:field>
</div>
@ -221,6 +231,20 @@
</div>
</div>
<!-- Stock Validation Warning (only shown when there's an issue) -->
@if($item['type'] === 'parts' && isset($item['stock_available']) && $item['quantity'] && $item['quantity'] > $item['stock_available'])
<div class="mt-2 p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 rounded-lg">
<div class="flex items-center space-x-2">
<svg class="w-5 h-5 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
<div class="text-sm text-red-800 dark:text-red-200">
<strong>Insufficient Stock:</strong> You requested {{ $item['quantity'] }} but only {{ $item['stock_available'] }} are available in inventory.
</div>
</div>
</div>
@endif
@error("lineItems.{$index}.type")
<p class="text-red-600 text-sm">{{ $message }}</p>
@enderror

View File

@ -77,7 +77,7 @@
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Line Items</h3>
<flux:button wire:click="addLineItem" variant="outline" size="sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Add Item
@ -102,7 +102,7 @@
<tr wire:key="line-item-{{ $index }}">
<td class="px-6 py-4 whitespace-nowrap">
<flux:select wire:model.live="lineItems.{{ $index }}.type">
<option value="labor">Labor</option>
<option value="labour">Labour</option>
<option value="parts">Parts</option>
<option value="miscellaneous">Miscellaneous</option>
</flux:select>
@ -124,7 +124,7 @@
<td class="px-6 py-4 whitespace-nowrap">
@if(!($item['required'] ?? false))
<flux:button wire:click="removeLineItem({{ $index }})" variant="ghost" size="sm" class="text-red-600 hover:text-red-800">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</flux:button>
@ -208,7 +208,7 @@
Cancel
</flux:button>
<flux:button wire:click="save" variant="primary">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Create Estimate

View File

@ -0,0 +1,265 @@
<div class="space-y-6">
<!-- Header -->
<div class="bg-gradient-to-r from-accent/10 to-accent/5 rounded-xl p-6 border border-accent/20 dark:border-accent/30">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">Edit Estimate</h1>
<p class="text-gray-600 dark:text-gray-400">
{{ $estimate->estimate_number }}
{{ $estimate->customer_id ? $estimate->customer?->name : $estimate->jobCard?->customer?->name ?? 'Unknown Customer' }}
@if($estimate->customer_id && $estimate->vehicle)
{{ $estimate->vehicle->year }} {{ $estimate->vehicle->make }} {{ $estimate->vehicle->model }}
@elseif($estimate->jobCard?->vehicle)
{{ $estimate->jobCard->vehicle->year }} {{ $estimate->jobCard->vehicle->make }} {{ $estimate->jobCard->vehicle->model }}
@endif
</p>
@if($lastSaved)
<p class="text-sm text-green-600 dark:text-green-400 mt-2">
<span class="inline-flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
Auto-saved at {{ $lastSaved }}
</span>
</p>
@endif
</div>
<div class="flex space-x-3 mt-4 lg:mt-0">
<flux:button wire:click="toggleAutoSave" variant="ghost" size="sm">
@if($autoSave)
<svg class="w-4 h-4 mr-2 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
Auto-save ON
@else
<svg class="w-4 h-4 mr-2 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z" clip-rule="evenodd"></path>
</svg>
Auto-save OFF
@endif
</flux:button>
<flux:button href="{{ route('estimates.show', $estimate) }}" variant="ghost">
View Estimate
</flux:button>
<flux:button wire:click="save" variant="primary">
Save Changes
</flux:button>
</div>
</div>
</div>
<!-- Quick Add Presets -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Quick Add Service Items</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
@foreach($quickAddPresets as $key => $preset)
<button
wire:click="addQuickPreset('{{ $key }}')"
class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-accent hover:bg-accent/5 dark:hover:bg-accent/10 transition-colors text-left group"
>
<div class="font-medium text-gray-900 dark:text-gray-100 group-hover:text-accent">{{ $preset['description'] }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">${{ number_format($preset['unit_price'], 2) }}</div>
<div class="text-xs text-accent mt-2 opacity-0 group-hover:opacity-100 transition-opacity">Click to add</div>
</button>
@endforeach
</div>
</div>
<!-- Main Content Grid -->
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
<!-- Line Items (2/3 width) -->
<div class="xl:col-span-2">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Line Items</h3>
<div class="flex space-x-3">
@if(!$bulkOperationMode)
<flux:button wire:click="toggleBulkMode" variant="outline" size="sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Bulk Operations
</flux:button>
@else
<flux:button wire:click="bulkDelete" variant="danger" size="sm" :disabled="empty($selectedItems)">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
Delete Selected
</flux:button>
<flux:button wire:click="toggleBulkMode" variant="ghost" size="sm">
Cancel
</flux:button>
@endif
<flux:button wire:click="addLineItem" variant="primary" size="sm">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"></path>
</svg>
Add Item
</flux:button>
</div>
</div>
</div>
<div class="overflow-x-auto">
@if(count($lineItems) > 0)
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
@if($bulkOperationMode)
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
<flux:checkbox wire:model.live="selectAll" />
</th>
@endif
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Description</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Qty</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Unit Price</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Total</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@foreach($lineItems as $index => $item)
<tr wire:key="item-{{ $index }}" class="hover:bg-gray-50 dark:hover:bg-gray-700">
@if($bulkOperationMode)
<td class="px-6 py-4 whitespace-nowrap">
<flux:checkbox wire:model.live="selectedItems" value="{{ $index }}" />
</td>
@endif
<td class="px-6 py-4 whitespace-nowrap">
<flux:select wire:model.live="lineItems.{{ $index }}.type" class="w-full">
<option value="labour">Labour</option>
<option value="parts">Parts</option>
<option value="miscellaneous">Miscellaneous</option>
</flux:select>
<flux:error name="lineItems.{{ $index }}.type" />
</td>
<td class="px-6 py-4">
<flux:input wire:model.live="lineItems.{{ $index }}.description" placeholder="Item description" class="w-full" />
<flux:error name="lineItems.{{ $index }}.description" />
</td>
<td class="px-6 py-4 whitespace-nowrap">
<flux:input wire:model.live="lineItems.{{ $index }}.quantity" type="number" step="0.01" min="0.01" class="w-20" />
<flux:error name="lineItems.{{ $index }}.quantity" />
</td>
<td class="px-6 py-4 whitespace-nowrap">
<flux:input wire:model.live="lineItems.{{ $index }}.unit_price" type="number" step="0.01" min="0" class="w-24" />
<flux:error name="lineItems.{{ $index }}.unit_price" />
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
${{ number_format(($item['quantity'] ?? 0) * ($item['unit_price'] ?? 0), 2) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<flux:button wire:click="removeLineItem({{ $index }})" variant="danger" size="sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</flux:button>
</td>
</tr>
@endforeach
</tbody>
</table>
@else
<div class="p-12 text-center">
<svg class="w-12 h-12 mx-auto text-gray-400 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">No line items yet</h3>
<p class="text-gray-500 dark:text-gray-400 mb-4">Get started by adding your first service or part.</p>
<flux:button wire:click="addLineItem" variant="primary">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"></path>
</svg>
Add First Item
</flux:button>
</div>
@endif
</div>
</div>
</div>
<!-- Settings & Summary (1/3 width) -->
<div class="space-y-6">
<!-- Estimate Settings -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Settings</h3>
<div class="space-y-4">
<flux:field>
<flux:label>Validity Period (Days)</flux:label>
<flux:input wire:model.live="validity_period_days" type="number" min="1" max="365" />
<flux:error name="validity_period_days" />
</flux:field>
<flux:field>
<flux:label>Tax Rate (%)</flux:label>
<flux:input wire:model.live="tax_rate" type="number" step="0.01" min="0" max="100" />
<flux:error name="tax_rate" />
</flux:field>
<flux:field>
<flux:label>Discount Amount ($)</flux:label>
<flux:input wire:model.live="discount_amount" type="number" step="0.01" min="0" />
<flux:error name="discount_amount" />
</flux:field>
</div>
</div>
<!-- Summary -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Summary</h3>
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">Subtotal:</span>
<span class="font-medium text-gray-900 dark:text-gray-100">${{ number_format($subtotal, 2) }}</span>
</div>
@if($discount_amount > 0)
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">Discount:</span>
<span class="font-medium text-red-600 dark:text-red-400">-${{ number_format($discount_amount, 2) }}</span>
</div>
@endif
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">Tax ({{ $tax_rate }}%):</span>
<span class="font-medium text-gray-900 dark:text-gray-100">${{ number_format($tax_amount, 2) }}</span>
</div>
<div class="border-t border-gray-200 dark:border-gray-700 pt-3">
<div class="flex justify-between">
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">Total:</span>
<span class="text-lg font-bold text-accent">${{ number_format($total_amount, 2) }}</span>
</div>
</div>
</div>
</div>
<!-- Notes -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Notes</h3>
<div class="space-y-4">
<flux:field>
<flux:label>Customer Notes</flux:label>
<flux:textarea wire:model.live="notes" rows="3" placeholder="Notes visible to customer..." />
<flux:error name="notes" />
</flux:field>
<flux:field>
<flux:label>Internal Notes</flux:label>
<flux:textarea wire:model.live="internal_notes" rows="3" placeholder="Internal notes (not shown to customer)..." />
<flux:error name="internal_notes" />
</flux:field>
</div>
</div>
<!-- Terms & Conditions -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<flux:field>
<flux:label>Terms & Conditions</flux:label>
<flux:textarea wire:model.live="terms_and_conditions" rows="4" placeholder="Terms and conditions..." />
<flux:error name="terms_and_conditions" />
</flux:field>
</div>
</div>
</div>
</div>

View File

@ -1,20 +1,20 @@
<div class="space-y-8">
<!-- Header Section -->
<div class="bg-gradient-to-r from-orange-50 to-amber-50 rounded-xl p-6 border border-orange-100">
<div class="space-y-6">
<!-- Header -->
<div class="bg-gradient-to-r from-accent/10 to-accent/5 rounded-xl p-6 border border-accent/20 dark:border-accent/30">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900 mb-2">Edit Estimate</h1>
<p class="text-gray-600">
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">Edit Estimate</h1>
<p class="text-gray-600 dark:text-gray-400">
{{ $estimate->estimate_number }}
{{ $estimate->customer_id ? $estimate->customer?->name : $estimate->jobCard?->customer?->name ?? 'Unknown Customer' }}
@if($estimate->customer_id)
{{ $estimate->vehicle?->year }} {{ $estimate->vehicle?->make }} {{ $estimate->vehicle?->model }}
@else
{{ $estimate->jobCard?->vehicle?->year }} {{ $estimate->jobCard?->vehicle?->make }} {{ $estimate->jobCard?->vehicle?->model }}
{{ $estimate->customer_id ? $estimate->customer?->name : $estimate->jobCard?->customer?->name ?? 'Unknown Customer' }}
@if($estimate->customer_id && $estimate->vehicle)
{{ $estimate->vehicle->year }} {{ $estimate->vehicle->make }} {{ $estimate->vehicle->model }}
@elseif($estimate->jobCard?->vehicle)
{{ $estimate->jobCard->vehicle->year }} {{ $estimate->jobCard->vehicle->make }} {{ $estimate->jobCard->vehicle->model }}
@endif
</p>
@if($lastSaved)
<p class="text-sm text-green-600 mt-2">
<p class="text-sm text-green-600 dark:text-green-400 mt-2">
<span class="inline-flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
@ -25,7 +25,7 @@
@endif
</div>
<div class="flex space-x-3 mt-4 lg:mt-0">
<button wire:click="toggleAutoSave" class="inline-flex items-center px-4 py-2 border border-orange-300 rounded-lg text-sm font-medium text-orange-700 bg-white hover:bg-orange-50 transition-colors">
<flux:button wire:click="toggleAutoSave" variant="ghost" size="sm">
@if($autoSave)
<svg class="w-4 h-4 mr-2 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
@ -37,374 +37,229 @@
</svg>
Auto-save OFF
@endif
</button>
<button wire:click="toggleAdvancedOptions" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
Advanced Options
</button>
</flux:button>
<flux:button href="{{ route('estimates.show', $estimate) }}" variant="ghost">
View Estimate
</flux:button>
<flux:button wire:click="save" variant="primary">
Save Changes
</flux:button>
</div>
</div>
</div>
<!-- Quick Add Presets -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Quick Add Service Items</h3>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Quick Add Service Items</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
@foreach($quickAddPresets as $key => $preset)
<button
wire:click="addQuickPreset('{{ $key }}')"
class="p-4 border border-gray-200 rounded-lg hover:border-orange-300 hover:bg-orange-50 transition-colors text-left group"
class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-accent hover:bg-accent/5 dark:hover:bg-accent/10 transition-colors text-left group"
>
<div class="font-medium text-gray-900 group-hover:text-orange-700">{{ $preset['description'] }}</div>
<div class="text-sm text-gray-500 mt-1">${{ number_format($preset['unit_price'], 2) }}</div>
<div class="text-xs text-orange-600 mt-2 opacity-0 group-hover:opacity-100 transition-opacity">Click to add</div>
<div class="font-medium text-gray-900 dark:text-gray-100 group-hover:text-accent">{{ $preset['description'] }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">${{ number_format($preset['unit_price'], 2) }}</div>
<div class="text-xs text-accent mt-2 opacity-0 group-hover:opacity-100 transition-opacity">Click to add</div>
</button>
@endforeach
</div>
</div>
<!-- Main Form Grid -->
<div class="grid grid-cols-1 xl:grid-cols-3 gap-8">
<!-- Line Items Section (2/3 width) -->
<div class="xl:col-span-2 space-y-6">
<!-- Line Items Header -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="p-6 border-b border-gray-200">
<!-- Main Content Grid -->
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
<!-- Line Items (2/3 width) -->
<div class="xl:col-span-2">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900">Line Items</h3>
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Line Items</h3>
<div class="flex space-x-3">
@if(!$bulkOperationMode)
<button wire:click="toggleBulkMode" class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors">
<flux:button wire:click="toggleBulkMode" variant="outline" size="sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Bulk Operations
</button>
</flux:button>
@else
<div class="flex space-x-2">
<button wire:click="bulkDelete" class="inline-flex items-center px-3 py-2 border border-red-300 rounded-lg text-sm font-medium text-red-700 bg-white hover:bg-red-50 transition-colors"
@if(empty($selectedItems)) disabled @endif>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
Delete Selected
</button>
<button wire:click="toggleBulkMode" class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors">
Cancel
</button>
</div>
<flux:button wire:click="bulkDelete" variant="danger" size="sm" :disabled="empty($selectedItems)">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
Delete Selected
</flux:button>
<flux:button wire:click="toggleBulkMode" variant="ghost" size="sm">
Cancel
</flux:button>
@endif
</div>
</div>
</div>
<!-- Existing Line Items -->
<div class="divide-y divide-gray-200">
@forelse($lineItems as $index => $item)
<div class="p-6 {{ $bulkOperationMode ? 'bg-gray-50' : '' }}">
@if($bulkOperationMode)
<div class="flex items-start space-x-4">
<div class="flex items-center h-5 mt-4">
<input wire:model="selectedItems" value="{{ $index }}" type="checkbox" class="h-4 w-4 text-orange-600 focus:ring-orange-500 border-gray-300 rounded">
</div>
<div class="flex-1">
@endif
@if($item['is_editing'])
<!-- Edit Mode -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-4">
<div class="md:col-span-2">
<flux:select wire:model="lineItems.{{ $index }}.type" size="sm">
<option value="labor">Labor</option>
<option value="parts">Parts</option>
<option value="miscellaneous">Misc</option>
</flux:select>
</div>
<div class="md:col-span-4">
<flux:input wire:model="lineItems.{{ $index }}.description" placeholder="Description" size="sm" />
</div>
<div class="md:col-span-2">
<flux:input wire:model="lineItems.{{ $index }}.quantity" type="number" step="0.01" min="0" placeholder="Qty" size="sm" />
</div>
<div class="md:col-span-2">
<flux:input wire:model="lineItems.{{ $index }}.unit_price" type="number" step="0.01" min="0" placeholder="Price" size="sm" />
</div>
<div class="md:col-span-2 flex space-x-2">
<button wire:click="saveLineItem({{ $index }})" class="flex-1 inline-flex items-center justify-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</button>
<button wire:click="cancelEditLineItem({{ $index }})" class="flex-1 inline-flex items-center justify-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
</div>
</div>
@if($showAdvancedOptions)
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4 pt-4 border-t border-gray-200">
<div>
<flux:input wire:model="lineItems.{{ $index }}.markup_percentage" type="number" step="0.01" min="0" placeholder="Markup %" size="sm" />
</div>
<div>
<flux:select wire:model="lineItems.{{ $index }}.discount_type" size="sm">
<option value="none">No Discount</option>
<option value="percentage">Percentage</option>
<option value="fixed">Fixed Amount</option>
</flux:select>
</div>
<div>
<flux:input wire:model="lineItems.{{ $index }}.discount_value" type="number" step="0.01" min="0" placeholder="Discount" size="sm" />
</div>
<div>
<label class="inline-flex items-center">
<input wire:model="lineItems.{{ $index }}.is_taxable" type="checkbox" class="rounded border-gray-300 text-orange-600 shadow-sm focus:border-orange-300 focus:ring focus:ring-orange-200 focus:ring-opacity-50">
<span class="ml-2 text-sm text-gray-600">Taxable</span>
</label>
</div>
</div>
@endif
@else
<!-- Display Mode -->
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="flex items-center space-x-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ $item['type'] === 'labor' ? 'bg-blue-100 text-blue-800' :
($item['type'] === 'parts' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800') }}">
{{ ucfirst($item['type']) }}
</span>
<span class="font-medium text-gray-900">{{ $item['description'] }}</span>
@if($item['part_name'])
<span class="text-sm text-gray-500">({{ $item['part_name'] }})</span>
@endif
</div>
<div class="mt-1 text-sm text-gray-500">
Qty: {{ $item['quantity'] }} × ${{ number_format($item['unit_price'], 2) }}
@if($item['markup_percentage'] > 0)
<span class="text-orange-600">+ {{ $item['markup_percentage'] }}% markup</span>
@endif
@if($item['discount_type'] !== 'none')
<span class="text-red-600">
- {{ $item['discount_type'] === 'percentage' ? $item['discount_value'].'%' : '$'.number_format($item['discount_value'], 2) }} discount
</span>
@endif
</div>
@if($item['notes'])
<div class="mt-2 text-sm text-gray-600 italic">{{ $item['notes'] }}</div>
@endif
</div>
<div class="flex items-center space-x-4">
<div class="text-right">
<div class="font-semibold text-gray-900">${{ number_format($item['total_amount'], 2) }}</div>
@if(!$item['is_taxable'])
<div class="text-xs text-gray-500">Tax exempt</div>
@endif
</div>
@if(!$bulkOperationMode)
<div class="flex space-x-2">
<button wire:click="editLineItem({{ $index }})" class="p-2 text-gray-400 hover:text-orange-600 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<button wire:click="duplicateLineItem({{ $index }})" class="p-2 text-gray-400 hover:text-blue-600 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
<button wire:click="removeLineItem({{ $index }})" class="p-2 text-gray-400 hover:text-red-600 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
@endif
</div>
</div>
@endif
@if($bulkOperationMode)
</div>
</div>
@endif
</div>
@empty
<div class="p-8 text-center text-gray-500">
<svg class="mx-auto h-12 w-12 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 48 48">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v1a2 2 0 002 2h2m9 0h2a2 2 0 002-2V7a2 2 0 00-2-2h-2m-9 4h9m5 0a2 2 0 012 2v3.11a1 1 0 01-.3.71l-7 7a1 1 0 01-1.4 0l-7-7a1 1 0 01-.3-.71V11a2 2 0 012-2z"></path>
</svg>
<p>No line items added yet. Add service items above to get started.</p>
</div>
@endforelse
</div>
<!-- Add New Line Item Form -->
<div class="p-6 bg-gray-50 border-t border-gray-200">
<h4 class="text-sm font-semibold text-gray-900 mb-4">Add New Line Item</h4>
<div class="grid grid-cols-1 md:grid-cols-12 gap-4">
<div class="md:col-span-2">
<flux:select wire:model="newItem.type" size="sm">
<option value="labor">Labor</option>
<option value="parts">Parts</option>
<option value="miscellaneous">Misc</option>
</flux:select>
</div>
<div class="md:col-span-4">
<flux:input wire:model="newItem.description" placeholder="Description" size="sm" />
</div>
<div class="md:col-span-2">
<flux:input wire:model="newItem.quantity" type="number" step="0.01" min="0" placeholder="Quantity" size="sm" />
</div>
<div class="md:col-span-2">
<flux:input wire:model="newItem.unit_price" type="number" step="0.01" min="0" placeholder="Unit Price" size="sm" />
</div>
<div class="md:col-span-2">
<button wire:click="addLineItem" class="w-full inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-orange-600 hover:bg-orange-700 transition-colors">
<flux:button wire:click="addLineItem" variant="primary" size="sm">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"></path>
</svg>
Add Item
</button>
</flux:button>
</div>
</div>
</div>
@if($showAdvancedOptions)
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4 pt-4 border-t border-gray-200">
<div>
<flux:input wire:model="newItem.markup_percentage" type="number" step="0.01" min="0" placeholder="Markup %" size="sm" />
</div>
<div>
<flux:select wire:model="newItem.discount_type" size="sm">
<option value="none">No Discount</option>
<option value="percentage">Percentage</option>
<option value="fixed">Fixed Amount</option>
</flux:select>
</div>
<div>
<flux:input wire:model="newItem.discount_value" type="number" step="0.01" min="0" placeholder="Discount Value" size="sm" />
</div>
<div>
<label class="inline-flex items-center">
<input wire:model="newItem.is_taxable" type="checkbox" class="rounded border-gray-300 text-orange-600 shadow-sm focus:border-orange-300 focus:ring focus:ring-orange-200 focus:ring-opacity-50">
<span class="ml-2 text-sm text-gray-600">Taxable</span>
</label>
</div>
</div>
<div class="mt-4">
<flux:input wire:model="newItem.notes" placeholder="Item notes (optional)" size="sm" />
<div class="overflow-x-auto">
@if(count($lineItems) > 0)
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
@if($bulkOperationMode)
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
<flux:checkbox wire:model.live="selectAll" />
</th>
@endif
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Description</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Qty</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Unit Price</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Total</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@foreach($lineItems as $index => $item)
<tr wire:key="item-{{ $index }}" class="hover:bg-gray-50 dark:hover:bg-gray-700">
@if($bulkOperationMode)
<td class="px-6 py-4 whitespace-nowrap">
<flux:checkbox wire:model.live="selectedItems" value="{{ $index }}" />
</td>
@endif
<td class="px-6 py-4 whitespace-nowrap">
<flux:select wire:model.live="lineItems.{{ $index }}.type" class="w-full">
<option value="labour">Labour</option>
<option value="parts">Parts</option>
<option value="miscellaneous">Miscellaneous</option>
</flux:select>
<flux:error name="lineItems.{{ $index }}.type" />
</td>
<td class="px-6 py-4">
<flux:input wire:model.live="lineItems.{{ $index }}.description" placeholder="Item description" class="w-full" />
<flux:error name="lineItems.{{ $index }}.description" />
</td>
<td class="px-6 py-4 whitespace-nowrap">
<flux:input wire:model.live="lineItems.{{ $index }}.quantity" type="number" step="0.01" min="0.01" class="w-20" />
<flux:error name="lineItems.{{ $index }}.quantity" />
</td>
<td class="px-6 py-4 whitespace-nowrap">
<flux:input wire:model.live="lineItems.{{ $index }}.unit_price" type="number" step="0.01" min="0" class="w-24" />
<flux:error name="lineItems.{{ $index }}.unit_price" />
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
${{ number_format(($item['quantity'] ?? 0) * ($item['unit_price'] ?? 0), 2) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<flux:button wire:click="removeLineItem({{ $index }})" variant="danger" size="sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</flux:button>
</td>
</tr>
@endforeach
</tbody>
</table>
@else
<div class="p-12 text-center">
<svg class="w-12 h-12 mx-auto text-gray-400 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">No line items yet</h3>
<p class="text-gray-500 dark:text-gray-400 mb-4">Get started by adding your first service or part.</p>
<flux:button wire:click="addLineItem" variant="primary">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"></path>
</svg>
Add First Item
</flux:button>
</div>
@endif
</div>
</div>
</div>
<!-- Settings & Summary Sidebar (1/3 width) -->
<!-- Settings & Summary (1/3 width) -->
<div class="space-y-6">
<!-- Financial Summary -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Financial Summary</h3>
<!-- Estimate Settings -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Settings</h3>
<div class="space-y-4">
<flux:field>
<flux:label>Validity Period (Days)</flux:label>
<flux:input wire:model.live="validity_period_days" type="number" min="1" max="365" />
<flux:error name="validity_period_days" />
</flux:field>
<flux:field>
<flux:label>Tax Rate (%)</flux:label>
<flux:input wire:model.live="tax_rate" type="number" step="0.01" min="0" max="100" />
<flux:error name="tax_rate" />
</flux:field>
<flux:field>
<flux:label>Discount Amount ($)</flux:label>
<flux:input wire:model.live="discount_amount" type="number" step="0.01" min="0" />
<flux:error name="discount_amount" />
</flux:field>
</div>
</div>
<!-- Summary -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Summary</h3>
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-gray-600">Subtotal:</span>
<span class="font-medium">${{ number_format($subtotal, 2) }}</span>
<span class="text-gray-600 dark:text-gray-400">Subtotal:</span>
<span class="font-medium text-gray-900 dark:text-gray-100">${{ number_format($subtotal, 2) }}</span>
</div>
@if($discount_amount > 0)
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">Discount:</span>
<span class="font-medium text-red-600 dark:text-red-400">-${{ number_format($discount_amount, 2) }}</span>
</div>
@endif
<div class="flex justify-between text-sm">
<span class="text-gray-600">Discount:</span>
<span class="font-medium text-red-600">-${{ number_format($discount_amount, 2) }}</span>
<span class="text-gray-600 dark:text-gray-400">Tax ({{ $tax_rate }}%):</span>
<span class="font-medium text-gray-900 dark:text-gray-100">${{ number_format($tax_amount, 2) }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Tax ({{ $tax_rate }}%):</span>
<span class="font-medium">${{ number_format($tax_amount, 2) }}</span>
</div>
<div class="border-t border-gray-200 pt-3">
<div class="border-t border-gray-200 dark:border-gray-700 pt-3">
<div class="flex justify-between">
<span class="text-lg font-semibold text-gray-900">Total:</span>
<span class="text-lg font-bold text-orange-600">${{ number_format($total_amount, 2) }}</span>
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">Total:</span>
<span class="text-lg font-bold text-accent">${{ number_format($total_amount, 2) }}</span>
</div>
</div>
</div>
</div>
<!-- Estimate Settings -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Estimate Settings</h3>
<!-- Notes -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Notes</h3>
<div class="space-y-4">
<div>
<flux:field>
<flux:label>Tax Rate (%)</flux:label>
<flux:input wire:model.live="tax_rate" type="number" step="0.01" min="0" max="50" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Discount Amount ($)</flux:label>
<flux:input wire:model.live="discount_amount" type="number" step="0.01" min="0" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Valid for (days)</flux:label>
<flux:input wire:model="validity_period_days" type="number" min="1" max="365" />
</flux:field>
</div>
</div>
</div>
<flux:field>
<flux:label>Customer Notes</flux:label>
<flux:textarea wire:model.live="notes" rows="3" placeholder="Notes visible to customer..." />
<flux:error name="notes" />
</flux:field>
<!-- Notes Section -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Notes</h3>
<div class="space-y-4">
<div>
<flux:field>
<flux:label>Customer Notes</flux:label>
<flux:textarea wire:model="notes" rows="3" placeholder="Notes visible to customer..." />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Internal Notes</flux:label>
<flux:textarea wire:model="internal_notes" rows="3" placeholder="Internal notes for staff..." />
</flux:field>
</div>
<flux:field>
<flux:label>Internal Notes</flux:label>
<flux:textarea wire:model.live="internal_notes" rows="3" placeholder="Internal notes (not shown to customer)..." />
<flux:error name="internal_notes" />
</flux:field>
</div>
</div>
<!-- Terms & Conditions -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Terms & Conditions</h3>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<flux:field>
<flux:textarea wire:model="terms_and_conditions" rows="6" placeholder="Enter terms and conditions..." />
<flux:label>Terms & Conditions</flux:label>
<flux:textarea wire:model.live="terms_and_conditions" rows="4" placeholder="Terms and conditions..." />
<flux:error name="terms_and_conditions" />
</flux:field>
</div>
<!-- Action Buttons -->
<div class="space-y-3">
<button wire:click="save" class="w-full inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-white bg-gradient-to-r from-orange-600 to-orange-700 hover:from-orange-700 hover:to-orange-800 transition-all duration-200 shadow-lg hover:shadow-xl">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
Save Estimate
</button>
<a href="{{ route('estimates.show', $estimate) }}" class="w-full inline-flex items-center justify-center px-6 py-3 border border-gray-300 text-base font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 transition-colors">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
Cancel
</a>
</div>
</div>
</div>
</div>

View File

@ -20,7 +20,7 @@
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
From Diagnosis
<svg class="w-4 h-4 ml-2" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 ml-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
@ -177,7 +177,7 @@
@if($search || $statusFilter || $approvalStatusFilter || $customerFilter || $dateFrom || $dateTo)
<div class="flex items-center space-x-2">
<button wire:click="clearFilters" class="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
Clear Filters
@ -298,19 +298,19 @@
</div>
<div class="flex space-x-2">
<button wire:click="bulkAction('mark_sent')" class="inline-flex items-center px-3 py-2 border border-blue-300 dark:border-blue-600 rounded-lg text-sm font-medium text-blue-700 dark:text-blue-300 bg-white dark:bg-gray-800 hover:bg-blue-50 dark:hover:bg-blue-900/50 transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"></path>
</svg>
Mark as Sent
</button>
<button wire:click="bulkAction('export')" class="inline-flex items-center px-3 py-2 border border-green-300 dark:border-green-600 rounded-lg text-sm font-medium text-green-700 dark:text-green-300 bg-white dark:bg-gray-800 hover:bg-green-50 dark:hover:bg-green-900/50 transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
Export
</button>
<button wire:click="bulkAction('delete')" class="inline-flex items-center px-3 py-2 border border-red-300 dark:border-red-600 rounded-lg text-sm font-medium text-red-700 dark:text-red-300 bg-white dark:bg-gray-800 hover:bg-red-50 dark:hover:bg-red-900/50 transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
Delete
@ -335,7 +335,7 @@
<div class="flex items-center space-x-1">
<span>Estimate #</span>
@if($sortBy === 'estimate_number')
<svg class="w-4 h-4 text-accent" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 text-accent" fill="currentColor" viewBox="0 0 20 20">
@if($sortDirection === 'asc')
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"></path>
@else
@ -349,7 +349,7 @@
<div class="flex items-center space-x-1">
<span>Date</span>
@if($sortBy === 'created_at')
<svg class="w-4 h-4 text-accent" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 text-accent" fill="currentColor" viewBox="0 0 20 20">
@if($sortDirection === 'asc')
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"></path>
@else
@ -367,7 +367,7 @@
<div class="flex items-center space-x-1">
<span>Total</span>
@if($sortBy === 'total_amount')
<svg class="w-4 h-4 text-accent" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 text-accent" fill="currentColor" viewBox="0 0 20 20">
@if($sortDirection === 'asc')
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"></path>
@else
@ -377,21 +377,54 @@
@endif
</div>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-900 dark:text-gray-100 uppercase tracking-wider">Valid Until</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-900 dark:text-gray-100 uppercase tracking-wider rounded-tr-lg">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@forelse($estimates as $estimate)
<tr class="hover:bg-accent/10 dark:hover:bg-accent/20 transition-colors" wire:key="estimate-{{ $estimate->id }}">
<tr class="cursor-pointer transition-colors {{ $selectedRow === $estimate->id ? 'bg-accent/20 border-l-4 border-accent' : 'hover:bg-accent/10 dark:hover:bg-accent/20' }}"
wire:key="estimate-{{ $estimate->id }}"
wire:click="selectRow({{ $estimate->id }})">
@if($bulkMode)
<td class="px-6 py-4 whitespace-nowrap">
<td class="px-6 py-4 whitespace-nowrap" onclick="event.stopPropagation()">
<flux:checkbox wire:model.live="selectedEstimates" value="{{ $estimate->id }}" />
</td>
@endif
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex flex-col">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $estimate->estimate_number }}</div>
<!-- Inline Actions - Only show when this row is selected -->
@if($selectedRow === $estimate->id)
<div class="flex items-center space-x-1 mt-2">
@php
$actions = [];
// Edit (first) - temporarily bypass policy for super admin
if(auth()->user()->hasRole('super_admin') || auth()->user()->can('update', $estimate)) {
$actions[] = '<a href="' . route('estimates.edit', $estimate) . '" class="text-xs text-amber-600 hover:text-amber-800 dark:text-amber-400 dark:hover:text-amber-300 font-medium" onclick="event.stopPropagation()">edit</a>';
}
// View (second) - temporarily bypass policy for super admin
if(auth()->user()->hasRole('super_admin') || auth()->user()->can('view', $estimate)) {
$actions[] = '<a href="' . route('estimates.show', $estimate) . '" class="text-xs text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 font-medium" onclick="event.stopPropagation()">view</a>';
}
// Delete (third) - temporarily bypass policy for super admin
if(auth()->user()->hasRole('super_admin') || auth()->user()->can('delete', $estimate)) {
$actions[] = '<button onclick="event.stopPropagation(); confirmDeleteEstimate(' . $estimate->id . ')" class="text-xs text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 font-medium">delete</button>';
}
// Send/Resend (fourth)
if($estimate->status === 'draft') {
$actions[] = '<button wire:click="sendEstimate(' . $estimate->id . ')" class="text-xs text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300 font-medium" onclick="event.stopPropagation()">send</button>';
} elseif(in_array($estimate->status, ['sent', 'approved', 'rejected'])) {
$actions[] = '<button wire:click="resendEstimate(' . $estimate->id . ')" class="text-xs text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300 font-medium" onclick="event.stopPropagation()">resend</button>';
}
@endphp
{!! implode(' <span class="text-gray-400 text-xs">|</span> ', $actions) !!}
</div>
@endif
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@ -454,60 +487,10 @@
${{ number_format($estimate->total_amount, 2) }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@if($estimate->validity_period_days)
@php
$validUntil = $estimate->created_at->addDays($estimate->validity_period_days);
$isExpired = $validUntil->isPast();
$isExpiringSoon = $validUntil->diffInDays(now()) <= 7 && !$isExpired;
@endphp
<div class="text-sm {{ $isExpired ? 'text-red-600' : ($isExpiringSoon ? 'text-accent' : 'text-gray-900 dark:text-gray-100') }}">
{{ $validUntil->format('M j, Y') }}
</div>
@if($isExpired)
<div class="text-xs text-red-500">Expired</div>
@elseif($isExpiringSoon)
<div class="text-xs text-accent">Expires soon</div>
@endif
@else
<span class="text-sm text-gray-400">No expiry</span>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex items-center space-x-2">
<a href="{{ route('estimates.show', $estimate) }}" class="text-accent hover:text-accent-foreground transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</a>
@can('update', $estimate)
<a href="{{ route('estimates.edit', $estimate) }}" class="text-accent hover:text-accent-foreground transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</a>
@endcan
@if($estimate->status === 'draft')
<button wire:click="sendEstimate({{ $estimate->id }})" class="text-blue-600 hover:text-blue-800 transition-colors" title="Send to Customer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
</svg>
</button>
@endif
@can('delete', $estimate)
<button wire:click="confirmDelete({{ $estimate->id }})" class="text-red-600 hover:text-red-800 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
@endcan
</div>
</td>
</tr>
@empty
<tr>
<td colspan="{{ $bulkMode ? '11' : '10' }}" class="px-6 py-12 text-center">
<td colspan="{{ $bulkMode ? '9' : '8' }}" class="px-6 py-12 text-center">
<div class="flex flex-col items-center justify-center space-y-3">
<svg class="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
@ -523,7 +506,7 @@
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"></path>
</svg>
Create New Estimate
<svg class="w-4 h-4 ml-2" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 ml-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
@ -566,9 +549,20 @@
</div>
<!-- Pagination -->
@if($estimates->hasPages())
<div class="mt-6">
<div class="mt-6 flex justify-between items-center">
<div class="text-sm text-gray-700 dark:text-gray-300">
Showing {{ $estimates->firstItem() ?? 0 }} to {{ $estimates->lastItem() ?? 0 }} of {{ $estimates->total() }} estimates
</div>
<div>
{{ $estimates->links() }}
</div>
@endif
</div>
</div>
<script>
function confirmDeleteEstimate(estimateId) {
if (confirm('Are you sure you want to delete this estimate? This action cannot be undone.')) {
@this.call('confirmDelete', estimateId);
}
}
</script>

View File

@ -1,3 +0,0 @@
<div>
{{-- Nothing in the world is as soft and yielding as water. --}}
</div>

View File

@ -49,7 +49,7 @@
<div class="flex items-center space-x-3">
@if($estimate->status === 'draft')
<button wire:click="sendToCustomer" class="inline-flex items-center px-4 py-2 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-medium rounded-lg transition-all duration-200 shadow-lg transform hover:scale-105">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
</svg>
Send to Customer
@ -58,7 +58,7 @@
<div class="relative" x-data="{ open: false }">
<button @click="open = !open" class="inline-flex items-center px-4 py-2 border border-zinc-300 dark:border-zinc-600 hover:bg-zinc-50 dark:hover:bg-zinc-700 text-zinc-700 dark:text-zinc-300 font-medium rounded-lg transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"/>
</svg>
Actions
@ -67,26 +67,26 @@
<div x-show="open" @click.away="open = false" x-transition class="absolute right-0 mt-2 w-48 bg-white dark:bg-zinc-800 rounded-lg shadow-lg border border-zinc-200 dark:border-zinc-700 z-50">
<div class="py-1">
<a href="{{ route('estimates.edit', $estimate) }}" class="flex items-center px-4 py-2 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
Edit Estimate
</a>
<button wire:click="duplicateEstimate" class="w-full flex items-center px-4 py-2 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
Duplicate Estimate
</button>
<button wire:click="downloadPDF" class="w-full flex items-center px-4 py-2 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Download PDF
</button>
@if($estimate->status === 'approved')
<a href="{{ route('work-orders.create', $estimate) }}" class="flex items-center px-4 py-2 text-sm text-green-600 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/20">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
Create Work Order
@ -97,7 +97,7 @@
</div>
<a href="{{ route('job-cards.show', $estimate->jobCard) }}" class="inline-flex items-center px-4 py-2 bg-zinc-600 hover:bg-zinc-700 text-white font-medium rounded-lg transition-colors shadow-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
View Job Card

View File

@ -0,0 +1,317 @@
<div class="space-y-6">
<!-- Header -->
<div class="bg-gradient-to-r from-accent/10 to-accent/5 rounded-xl p-6 border border-accent/20 dark:border-accent/30">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<div class="flex items-center space-x-4">
<div class="h-16 w-16 bg-gradient-to-br from-accent to-accent-content rounded-xl flex items-center justify-center shadow-lg">
<svg class="h-8 w-8 text-accent-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
Estimate #{{ $estimate->estimate_number }}
</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">
@if($estimate->jobCard)
Job Card #{{ $estimate->jobCard->job_number }} • {{ $estimate->created_at->format('M j, Y') }}
@else
Standalone Estimate {{ $estimate->created_at->format('M j, Y') }}
@endif
</p>
<div class="flex items-center space-x-3 mt-2">
<flux:badge
:color="match($estimate->status) {
'draft' => 'zinc',
'sent' => 'blue',
'approved' => 'green',
'rejected' => 'red',
'expired' => 'orange',
default => 'zinc'
}"
size="sm"
>
{{ ucfirst($estimate->status) }}
</flux:badge>
<flux:badge
:color="match($estimate->customer_approval_status) {
'pending' => 'yellow',
'approved' => 'green',
'rejected' => 'red',
default => 'zinc'
}"
size="sm"
>
Customer: {{ ucfirst($estimate->customer_approval_status) }}
</flux:badge>
@if($estimate->validity_period_days)
<span class="text-xs text-gray-500 dark:text-gray-400">
Valid until {{ $estimate->valid_until->format('M j, Y') }}
</span>
@endif
</div>
</div>
</div>
<!-- Actions -->
<div class="flex items-center space-x-3 mt-4 lg:mt-0">
@if($estimate->status === 'draft')
<flux:button wire:click="sendToCustomer" variant="primary">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
</svg>
Send to Customer
</flux:button>
@endif
<flux:dropdown align="end">
<flux:button variant="ghost" icon="ellipsis-horizontal" />
<flux:menu>
<flux:menu.item icon="pencil" href="{{ route('estimates.edit', $estimate) }}">
Edit Estimate
</flux:menu.item>
<flux:menu.item icon="document-duplicate" wire:click="duplicateEstimate">
Duplicate Estimate
</flux:menu.item>
<flux:menu.item icon="arrow-down-tray" wire:click="downloadPDF">
Download PDF
</flux:menu.item>
@if($estimate->status === 'approved')
<flux:menu.item icon="plus" href="{{ route('work-orders.create', $estimate) }}">
Create Work Order
</flux:menu.item>
@endif
@if($estimate->customer_approval_status === 'pending' && auth()->user()->can('approve', $estimate))
<flux:menu.separator />
<flux:menu.item icon="check" wire:click="approveEstimate">
Approve Estimate
</flux:menu.item>
<flux:menu.item icon="x-mark" wire:click="rejectEstimate">
Reject Estimate
</flux:menu.item>
@endif
</flux:menu>
</flux:dropdown>
</div>
</div>
</div>
<!-- Customer & Vehicle Information -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Customer Information -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Customer Information</h3>
@php
$customer = $estimate->customer ?? $estimate->jobCard?->customer;
@endphp
@if($customer)
<div class="space-y-3">
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Name:</span>
<p class="text-gray-900 dark:text-gray-100">{{ $customer->name }}</p>
</div>
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Email:</span>
<p class="text-gray-900 dark:text-gray-100">{{ $customer->email }}</p>
</div>
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Phone:</span>
<p class="text-gray-900 dark:text-gray-100">{{ $customer->phone }}</p>
</div>
</div>
@else
<p class="text-gray-500 dark:text-gray-400">No customer information available</p>
@endif
</div>
<!-- Vehicle Information -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Vehicle Information</h3>
@php
$vehicle = $estimate->vehicle ?? $estimate->jobCard?->vehicle;
@endphp
@if($vehicle)
<div class="space-y-3">
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Vehicle:</span>
<p class="text-gray-900 dark:text-gray-100">{{ $vehicle->year }} {{ $vehicle->make }} {{ $vehicle->model }}</p>
</div>
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">License Plate:</span>
<p class="text-gray-900 dark:text-gray-100">{{ $vehicle->license_plate }}</p>
</div>
@if($vehicle->vin)
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">VIN:</span>
<p class="text-gray-900 dark:text-gray-100 font-mono text-sm">{{ $vehicle->vin }}</p>
</div>
@endif
</div>
@else
<p class="text-gray-500 dark:text-gray-400">No vehicle specified</p>
@endif
</div>
</div>
<!-- Line Items -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Line Items</h3>
<flux:button wire:click="$toggle('showItemDetails')" variant="ghost" size="sm">
{{ $showItemDetails ? 'Hide Details' : 'Show Details' }}
</flux:button>
</div>
</div>
<div class="overflow-x-auto">
@if($estimate->lineItems->count() > 0)
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Description</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Qty</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Unit Price</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Total</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@foreach($estimate->lineItems as $item)
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<flux:badge
:color="match($item->type) {
'labour' => 'blue',
'parts' => 'green',
'miscellaneous' => 'gray',
default => 'gray'
}"
size="sm"
>
{{ ucfirst($item->type) }}
</flux:badge>
</td>
<td class="px-6 py-4">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $item->description }}</div>
@if($showItemDetails && $item->part)
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
Part #: {{ $item->part->part_number }}
</div>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{{ $item->quantity }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
${{ number_format($item->unit_price, 2) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
${{ number_format($item->total_amount, 2) }}
</td>
</tr>
@endforeach
</tbody>
</table>
@else
<div class="p-12 text-center">
<svg class="w-12 h-12 mx-auto text-gray-400 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">No line items</h3>
<p class="text-gray-500 dark:text-gray-400">This estimate doesn't have any line items yet.</p>
</div>
@endif
</div>
</div>
<!-- Summary & Details -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Summary -->
<div class="lg:col-span-1">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Summary</h3>
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">Subtotal:</span>
<span class="font-medium text-gray-900 dark:text-gray-100">${{ number_format($estimate->subtotal, 2) }}</span>
</div>
@if($estimate->discount_amount > 0)
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">Discount:</span>
<span class="font-medium text-red-600 dark:text-red-400">-${{ number_format($estimate->discount_amount, 2) }}</span>
</div>
@endif
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">Tax ({{ $estimate->tax_rate }}%):</span>
<span class="font-medium text-gray-900 dark:text-gray-100">${{ number_format($estimate->tax_amount, 2) }}</span>
</div>
<div class="border-t border-gray-200 dark:border-gray-700 pt-3">
<div class="flex justify-between">
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">Total:</span>
<span class="text-lg font-bold text-accent">${{ number_format($estimate->total_amount, 2) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Notes & Terms -->
<div class="lg:col-span-2 space-y-6">
@if($estimate->notes)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Customer Notes</h3>
<p class="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{ $estimate->notes }}</p>
</div>
@endif
@if($estimate->internal_notes && auth()->user()->can('view', $estimate))
<div class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800 p-6">
<h3 class="text-lg font-semibold text-yellow-800 dark:text-yellow-200 mb-4">Internal Notes</h3>
<p class="text-yellow-700 dark:text-yellow-300 whitespace-pre-wrap">{{ $estimate->internal_notes }}</p>
</div>
@endif
@if($estimate->terms_and_conditions)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Terms & Conditions</h3>
<p class="text-gray-700 dark:text-gray-300 whitespace-pre-wrap text-sm">{{ $estimate->terms_and_conditions }}</p>
</div>
@endif
</div>
</div>
<!-- Audit Trail -->
@if($estimate->created_at)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Estimate History</h3>
<div class="space-y-3 text-sm">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Created:</span>
<span class="text-gray-900 dark:text-gray-100">{{ $estimate->created_at->format('M j, Y g:i A') }}</span>
</div>
@if($estimate->sent_at)
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Sent to Customer:</span>
<span class="text-gray-900 dark:text-gray-100">{{ $estimate->sent_at->format('M j, Y g:i A') }}</span>
</div>
@endif
@if($estimate->customer_approved_at)
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Customer Response:</span>
<span class="text-gray-900 dark:text-gray-100">
{{ ucfirst($estimate->customer_approval_status) }} on {{ $estimate->customer_approved_at->format('M j, Y g:i A') }}
</span>
</div>
@endif
@if($estimate->preparedBy)
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Prepared By:</span>
<span class="text-gray-900 dark:text-gray-100">{{ $estimate->preparedBy->name }}</span>
</div>
@endif
</div>
</div>
@endif
</div>

View File

@ -1,47 +1,51 @@
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<!-- Advanced Header with Status & Actions -->
<div class="bg-gradient-to-r from-purple-50 to-blue-50 dark:from-purple-900/10 dark:to-blue-900/10 rounded-2xl border border-purple-200 dark:border-purple-800 p-6 mb-8">
<div class="flex items-center justify-between">
<div class="space-y-6">
<!-- Header -->
<div class="bg-gradient-to-r from-accent/10 to-accent/5 rounded-xl p-6 border border-accent/20 dark:border-accent/30">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<div class="flex items-center space-x-4">
<div class="h-16 w-16 bg-gradient-to-br from-purple-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
<svg class="h-8 w-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="h-16 w-16 bg-gradient-to-br from-accent to-accent-content rounded-xl flex items-center justify-center shadow-lg">
<svg class="h-8 w-8 text-accent-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<div>
<h1 class="text-3xl font-bold tracking-tight bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
Estimate #{{ $estimate->estimate_number }}
</h1>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mt-1">
<p class="text-gray-600 dark:text-gray-400 mt-1">
@if($estimate->jobCard)
Job Card #{{ $estimate->jobCard->job_card_number }} • {{ $estimate->created_at->format('M j, Y') }}
Job Card #{{ $estimate->jobCard->job_number }} • {{ $estimate->created_at->format('M j, Y') }}
@else
Standalone Estimate {{ $estimate->created_at->format('M j, Y') }}
@endif
</p>
<div class="flex items-center space-x-3 mt-2">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold
@switch($estimate->status)
@case('draft') bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200 @break
@case('sent') bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 @break
@case('approved') bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 @break
@case('rejected') bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 @break
@case('expired') bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200 @break
@endswitch
">
<flux:badge
:color="match($estimate->status) {
'draft' => 'zinc',
'sent' => 'blue',
'approved' => 'green',
'rejected' => 'red',
'expired' => 'orange',
default => 'zinc'
}"
size="sm"
>
{{ ucfirst($estimate->status) }}
</span>
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold
@switch($estimate->customer_approval_status)
@case('pending') bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 @break
@case('approved') bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 @break
@case('rejected') bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 @break
@endswitch
">
</flux:badge>
<flux:badge
:color="match($estimate->customer_approval_status) {
'pending' => 'yellow',
'approved' => 'green',
'rejected' => 'red',
default => 'zinc'
}"
size="sm"
>
Customer: {{ ucfirst($estimate->customer_approval_status) }}
</span>
</flux:badge>
@if($estimate->validity_period_days)
<span class="text-xs text-zinc-500 dark:text-zinc-400">
<span class="text-xs text-gray-500 dark:text-gray-400">
Valid until {{ $estimate->valid_until->format('M j, Y') }}
</span>
@endif
@ -49,373 +53,270 @@
</div>
</div>
<!-- Advanced Action Menu -->
<div class="flex items-center space-x-3">
<!-- Actions -->
<div class="flex items-center space-x-3 mt-4 lg:mt-0">
@if($estimate->status === 'draft')
<button wire:click="sendToCustomer" class="inline-flex items-center px-4 py-2 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-medium rounded-lg transition-all duration-200 shadow-lg transform hover:scale-105">
<flux:button wire:click="sendToCustomer" variant="primary">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
</svg>
Send to Customer
</button>
</flux:button>
@endif
<div class="relative" x-data="{ open: false }">
<button @click="open = !open" class="inline-flex items-center px-4 py-2 border border-zinc-300 dark:border-zinc-600 hover:bg-zinc-50 dark:hover:bg-zinc-700 text-zinc-700 dark:text-zinc-300 font-medium rounded-lg transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"/>
</svg>
Actions
</button>
<flux:dropdown align="end">
<flux:button variant="ghost" icon="ellipsis-horizontal" />
<div x-show="open" @click.away="open = false" x-transition class="absolute right-0 mt-2 w-48 bg-white dark:bg-zinc-800 rounded-lg shadow-lg border border-zinc-200 dark:border-zinc-700 z-50">
<div class="py-1">
<a href="{{ route('estimates.edit', $estimate) }}" class="flex items-center px-4 py-2 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
Edit Estimate
</a>
<button wire:click="duplicateEstimate" class="w-full flex items-center px-4 py-2 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
Duplicate Estimate
</button>
<button wire:click="downloadPDF" class="w-full flex items-center px-4 py-2 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Download PDF
</button>
@if($estimate->status === 'approved')
<a href="{{ route('work-orders.create', $estimate) }}" class="flex items-center px-4 py-2 text-sm text-green-600 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/20">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
Create Work Order
</a>
@endif
</div>
</div>
</div>
@if($estimate->jobCard)
<a href="{{ route('job-cards.show', $estimate->jobCard) }}" class="inline-flex items-center px-4 py-2 bg-zinc-600 hover:bg-zinc-700 text-white font-medium rounded-lg transition-colors shadow-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
View Job Card
</a>
@else
<a href="{{ route('estimates.index') }}" class="inline-flex items-center px-4 py-2 bg-zinc-600 hover:bg-zinc-700 text-white font-medium rounded-lg transition-colors shadow-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
Back to Estimates
</a>
@endif
<flux:menu>
<flux:menu.item icon="pencil" href="{{ route('estimates.edit', $estimate) }}">
Edit Estimate
</flux:menu.item>
<flux:menu.item icon="document-duplicate" wire:click="duplicateEstimate">
Duplicate Estimate
</flux:menu.item>
<flux:menu.item icon="arrow-down-tray" wire:click="downloadPDF">
Download PDF
</flux:menu.item>
@if($estimate->status === 'approved')
<flux:menu.item icon="plus" href="{{ route('work-orders.create', $estimate) }}">
Create Work Order
</flux:menu.item>
@can('create', \App\Models\Invoice::class)
<flux:menu.item icon="document-text" wire:click="convertToInvoice">
Convert to Invoice
</flux:menu.item>
@endcan
@endif
@if($estimate->customer_approval_status === 'pending' && auth()->user()->can('approve', $estimate))
<flux:menu.separator />
<flux:menu.item icon="check" wire:click="approveEstimate">
Approve Estimate
</flux:menu.item>
<flux:menu.item icon="x-mark" wire:click="rejectEstimate">
Reject Estimate
</flux:menu.item>
@endif
</flux:menu>
</flux:dropdown>
</div>
</div>
</div>
<!-- Advanced Two-Column Layout -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Main Content Column -->
<div class="lg:col-span-2 space-y-8">
<!-- Customer & Vehicle Information Card -->
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 shadow-sm">
<div class="px-6 py-4 border-b border-zinc-200 dark:border-zinc-700 bg-gradient-to-r from-zinc-50 to-zinc-100 dark:from-zinc-800 dark:to-zinc-700 rounded-t-xl">
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 flex items-center">
<svg class="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
Customer & Vehicle Details
</h2>
</div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<div class="flex items-center space-x-3">
<div class="h-10 w-10 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
<svg class="h-5 w-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
<div>
@if($estimate->customer_id)
{{-- Standalone estimate --}}
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $estimate->customer->name }}</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $estimate->customer->phone }}</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $estimate->customer->email }}</p>
@elseif($estimate->jobCard?->customer)
{{-- Job card-based estimate --}}
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $estimate->jobCard->customer->name }}</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $estimate->jobCard->customer->phone }}</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $estimate->jobCard->customer->email }}</p>
@else
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">Unknown Customer</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">No contact information</p>
@endif
</div>
</div>
</div>
<div class="space-y-4">
<div class="flex items-center space-x-3">
<div class="h-10 w-10 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
<svg class="h-5 w-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div>
@if($estimate->vehicle_id)
{{-- Standalone estimate --}}
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{{ $estimate->vehicle->year }} {{ $estimate->vehicle->make }} {{ $estimate->vehicle->model }}
</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $estimate->vehicle->license_plate }}</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">VIN: {{ $estimate->vehicle->vin }}</p>
@elseif($estimate->jobCard?->vehicle)
{{-- Job card-based estimate --}}
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{{ $estimate->jobCard->vehicle->year }} {{ $estimate->jobCard->vehicle->make }} {{ $estimate->jobCard->vehicle->model }}
</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $estimate->jobCard->vehicle->license_plate }}</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">VIN: {{ $estimate->jobCard->vehicle->vin }}</p>
@else
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">Unknown Vehicle</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">No vehicle information</p>
@endif
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Advanced Line Items with Interactive Features -->
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 shadow-sm">
<div class="px-6 py-4 border-b border-zinc-200 dark:border-zinc-700 bg-gradient-to-r from-zinc-50 to-zinc-100 dark:from-zinc-800 dark:to-zinc-700 rounded-t-xl">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 flex items-center">
<svg class="w-5 h-5 mr-2 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
Service Items & Parts
<span class="ml-2 text-xs bg-purple-100 dark:bg-purple-900 text-purple-600 dark:text-purple-400 px-2 py-1 rounded-full">
{{ $estimate->lineItems->count() }} items
</span>
</h2>
<button wire:click="toggleItemDetails" class="text-sm text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 transition-colors">
{{ $showItemDetails ? 'Hide Details' : 'Show Details' }}
</button>
</div>
</div>
<div class="overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-zinc-50 dark:bg-zinc-900">
<tr>
<th class="px-6 py-3 text-left text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Description</th>
<th class="px-6 py-3 text-center text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Qty</th>
<th class="px-6 py-3 text-right text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Unit Price</th>
<th class="px-6 py-3 text-right text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Total</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-200 dark:divide-zinc-700">
@foreach($estimate->lineItems as $item)
<tr class="hover:bg-zinc-50 dark:hover:bg-zinc-700/50 transition-colors group">
<td class="px-6 py-4">
<div class="flex items-start space-x-3">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium
@switch($item->type)
@case('labor') bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 @break
@case('parts') bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 @break
@case('miscellaneous') bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200 @break
@endswitch
">
{{ ucfirst($item->type) }}
</span>
<div class="flex-1">
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $item->description }}</p>
@if($showItemDetails && $item->labor_hours)
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
Labor: {{ $item->labor_hours }}h @ ${{ number_format($item->labor_rate, 2) }}/hr
</p>
@endif
@if($showItemDetails && $item->part_number)
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
Part #: {{ $item->part_number }}
</p>
@endif
</div>
</div>
</td>
<td class="px-6 py-4 text-center">
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-zinc-100 dark:bg-zinc-800 text-sm font-medium text-zinc-900 dark:text-zinc-100">
{{ $item->quantity }}
</span>
</td>
<td class="px-6 py-4 text-right">
<span class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
${{ number_format($item->unit_price, 2) }}
</span>
</td>
<td class="px-6 py-4 text-right">
<span class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
${{ number_format($item->total_amount, 2) }}
</span>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
<!-- Terms & Conditions -->
@if($estimate->terms_and_conditions)
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 shadow-sm">
<div class="px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 flex items-center">
<svg class="w-5 h-5 mr-2 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Terms & Conditions
</h2>
</div>
<div class="p-6">
<p class="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed">{{ $estimate->terms_and_conditions }}</p>
<!-- Customer & Vehicle Information -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Customer Information -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Customer Information</h3>
@php
$customer = $estimate->customer ?? $estimate->jobCard?->customer;
@endphp
@if($customer)
<div class="space-y-3">
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Name:</span>
<p class="text-gray-900 dark:text-gray-100">{{ $customer->name }}</p>
</div>
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Email:</span>
<p class="text-gray-900 dark:text-gray-100">{{ $customer->email }}</p>
</div>
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Phone:</span>
<p class="text-gray-900 dark:text-gray-100">{{ $customer->phone }}</p>
</div>
</div>
@else
<p class="text-gray-500 dark:text-gray-400">No customer information available</p>
@endif
</div>
<!-- Advanced Sidebar -->
<div class="space-y-6">
<!-- Financial Summary Card -->
<div class="bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/10 dark:to-emerald-900/10 rounded-xl border border-green-200 dark:border-green-800 shadow-sm">
<div class="p-6">
<h3 class="text-lg font-semibold text-green-900 dark:text-green-100 mb-4 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"/>
</svg>
Financial Summary
</h3>
<div class="space-y-4">
<div class="flex justify-between items-center py-2 border-b border-green-200 dark:border-green-700">
<span class="text-sm font-medium text-green-700 dark:text-green-300">Labor Cost</span>
<span class="text-sm font-semibold text-green-900 dark:text-green-100">${{ number_format($estimate->labor_cost, 2) }}</span>
</div>
<div class="flex justify-between items-center py-2 border-b border-green-200 dark:border-green-700">
<span class="text-sm font-medium text-green-700 dark:text-green-300">Parts Cost</span>
<span class="text-sm font-semibold text-green-900 dark:text-green-100">${{ number_format($estimate->parts_cost, 2) }}</span>
</div>
@if($estimate->miscellaneous_cost > 0)
<div class="flex justify-between items-center py-2 border-b border-green-200 dark:border-green-700">
<span class="text-sm font-medium text-green-700 dark:text-green-300">Miscellaneous</span>
<span class="text-sm font-semibold text-green-900 dark:text-green-100">${{ number_format($estimate->miscellaneous_cost, 2) }}</span>
</div>
@endif
<div class="flex justify-between items-center py-2 border-b border-green-200 dark:border-green-700">
<span class="text-sm font-medium text-green-700 dark:text-green-300">Subtotal</span>
<span class="text-sm font-semibold text-green-900 dark:text-green-100">${{ number_format($estimate->subtotal, 2) }}</span>
</div>
@if($estimate->discount_amount > 0)
<div class="flex justify-between items-center py-2 border-b border-green-200 dark:border-green-700">
<span class="text-sm font-medium text-red-600 dark:text-red-400">Discount</span>
<span class="text-sm font-semibold text-red-600 dark:text-red-400">-${{ number_format($estimate->discount_amount, 2) }}</span>
</div>
@endif
<div class="flex justify-between items-center py-2 border-b border-green-200 dark:border-green-700">
<span class="text-sm font-medium text-green-700 dark:text-green-300">Tax ({{ $estimate->tax_rate }}%)</span>
<span class="text-sm font-semibold text-green-900 dark:text-green-100">${{ number_format($estimate->tax_amount, 2) }}</span>
</div>
<div class="flex justify-between items-center pt-4 border-t-2 border-green-300 dark:border-green-600">
<span class="text-lg font-bold text-green-900 dark:text-green-100">Total</span>
<span class="text-2xl font-bold text-green-900 dark:text-green-100">${{ number_format($estimate->total_amount, 2) }}</span>
</div>
<!-- Vehicle Information -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Vehicle Information</h3>
@php
$vehicle = $estimate->vehicle ?? $estimate->jobCard?->vehicle;
@endphp
@if($vehicle)
<div class="space-y-3">
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Vehicle:</span>
<p class="text-gray-900 dark:text-gray-100">{{ $vehicle->year }} {{ $vehicle->make }} {{ $vehicle->model }}</p>
</div>
</div>
</div>
<!-- Status Timeline -->
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 shadow-sm">
<div class="px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 flex items-center">
<svg class="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Status Timeline
</h3>
</div>
<div class="p-6">
<div class="space-y-4">
<div class="flex items-center space-x-3">
<div class="h-3 w-3 bg-green-500 rounded-full"></div>
<div class="flex-1">
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">Created</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $estimate->created_at->format('M j, Y g:i A') }}</p>
</div>
</div>
@if($estimate->sent_at)
<div class="flex items-center space-x-3">
<div class="h-3 w-3 bg-blue-500 rounded-full"></div>
<div class="flex-1">
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">Sent to Customer</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $estimate->sent_at->format('M j, Y g:i A') }}</p>
</div>
</div>
@endif
@if($estimate->customer_viewed_at)
<div class="flex items-center space-x-3">
<div class="h-3 w-3 bg-yellow-500 rounded-full"></div>
<div class="flex-1">
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">Viewed by Customer</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $estimate->customer_viewed_at->format('M j, Y g:i A') }}</p>
</div>
</div>
@endif
@if($estimate->customer_responded_at)
<div class="flex items-center space-x-3">
<div class="h-3 w-3 {{ $estimate->customer_approval_status === 'approved' ? 'bg-green-500' : 'bg-red-500' }} rounded-full"></div>
<div class="flex-1">
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">Customer {{ ucfirst($estimate->customer_approval_status) }}</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $estimate->customer_responded_at->format('M j, Y g:i A') }}</p>
</div>
</div>
@endif
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">License Plate:</span>
<p class="text-gray-900 dark:text-gray-100">{{ $vehicle->license_plate }}</p>
</div>
</div>
</div>
<!-- Related Documents -->
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 shadow-sm">
<div class="px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 flex items-center">
<svg class="w-5 h-5 mr-2 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Related Documents
</h3>
</div>
<div class="p-6 space-y-3">
@if($estimate->diagnosis)
<a href="{{ route('diagnosis.show', $estimate->diagnosis) }}" class="flex items-center space-x-3 p-3 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors group">
<div class="h-8 w-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
<svg class="h-4 w-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="flex-1">
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">Diagnosis Report</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">View diagnostic findings</p>
</div>
</a>
@if($vehicle->vin)
<div>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">VIN:</span>
<p class="text-gray-900 dark:text-gray-100 font-mono text-sm">{{ $vehicle->vin }}</p>
</div>
@endif
</div>
@else
<p class="text-gray-500 dark:text-gray-400">No vehicle specified</p>
@endif
</div>
</div>
<!-- Line Items -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Line Items</h3>
<flux:button wire:click="$toggle('showItemDetails')" variant="ghost" size="sm">
{{ $showItemDetails ? 'Hide Details' : 'Show Details' }}
</flux:button>
</div>
</div>
<div class="overflow-x-auto">
@if($estimate->lineItems->count() > 0)
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Description</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Qty</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Unit Price</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Total</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@foreach($estimate->lineItems as $item)
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<flux:badge
:color="match($item->type) {
'labour' => 'blue',
'parts' => 'green',
'miscellaneous' => 'gray',
default => 'gray'
}"
size="sm"
>
{{ ucfirst($item->type) }}
</flux:badge>
</td>
<td class="px-6 py-4">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $item->description }}</div>
@if($showItemDetails && $item->part)
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
Part #: {{ $item->part->part_number }}
</div>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{{ $item->quantity }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
${{ number_format($item->unit_price, 2) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
${{ number_format($item->total_amount, 2) }}
</td>
</tr>
@endforeach
</tbody>
</table>
@else
<div class="p-12 text-center">
<svg class="w-12 h-12 mx-auto text-gray-400 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">No line items</h3>
<p class="text-gray-500 dark:text-gray-400">This estimate doesn't have any line items yet.</p>
</div>
@endif
</div>
</div>
<!-- Summary & Details -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Summary -->
<div class="lg:col-span-1">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Summary</h3>
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">Subtotal:</span>
<span class="font-medium text-gray-900 dark:text-gray-100">${{ number_format($estimate->subtotal, 2) }}</span>
</div>
@if($estimate->discount_amount > 0)
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">Discount:</span>
<span class="font-medium text-red-600 dark:text-red-400">-${{ number_format($estimate->discount_amount, 2) }}</span>
</div>
@endif
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">Tax ({{ $estimate->tax_rate }}%):</span>
<span class="font-medium text-gray-900 dark:text-gray-100">${{ number_format($estimate->tax_amount, 2) }}</span>
</div>
<div class="border-t border-gray-200 dark:border-gray-700 pt-3">
<div class="flex justify-between">
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">Total:</span>
<span class="text-lg font-bold text-accent">${{ number_format($estimate->total_amount, 2) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Notes & Terms -->
<div class="lg:col-span-2 space-y-6">
@if($estimate->notes)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Customer Notes</h3>
<p class="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{ $estimate->notes }}</p>
</div>
@endif
@if($estimate->internal_notes && auth()->user()->can('view', $estimate))
<div class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800 p-6">
<h3 class="text-lg font-semibold text-yellow-800 dark:text-yellow-200 mb-4">Internal Notes</h3>
<p class="text-yellow-700 dark:text-yellow-300 whitespace-pre-wrap">{{ $estimate->internal_notes }}</p>
</div>
@endif
@if($estimate->terms_and_conditions)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Terms & Conditions</h3>
<p class="text-gray-700 dark:text-gray-300 whitespace-pre-wrap text-sm">{{ $estimate->terms_and_conditions }}</p>
</div>
@endif
</div>
</div>
<!-- Audit Trail -->
@if($estimate->created_at)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Estimate History</h3>
<div class="space-y-3 text-sm">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Created:</span>
<span class="text-gray-900 dark:text-gray-100">{{ $estimate->created_at->format('M j, Y g:i A') }}</span>
</div>
@if($estimate->sent_at)
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Sent to Customer:</span>
<span class="text-gray-900 dark:text-gray-100">{{ $estimate->sent_at->format('M j, Y g:i A') }}</span>
</div>
@endif
@if($estimate->customer_approved_at)
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Customer Response:</span>
<span class="text-gray-900 dark:text-gray-100">
{{ ucfirst($estimate->customer_approval_status) }} on {{ $estimate->customer_approved_at->format('M j, Y g:i A') }}
</span>
</div>
@endif
@if($estimate->preparedBy)
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Prepared By:</span>
<span class="text-gray-900 dark:text-gray-100">{{ $estimate->preparedBy->name }}</span>
</div>
@endif
</div>
</div>
@endif
</div>

View File

@ -1,12 +1,19 @@
<div class="flex-1 relative" x-data="{ showResults: @entangle('showResults') }">
<flux:input
placeholder="Search customers, vehicles, job cards..."
icon="magnifying-glass"
class="w-full"
wire:model.live.debounce.300ms="search"
x-on:focus="$wire.showResults = true"
x-on:click.away="$wire.showResults = false"
/>
<div class="relative">
<flux:input
placeholder="Search customers, vehicles, job cards..."
icon="magnifying-glass"
class="w-full"
:loading="false"
wire:model.live.debounce.300ms="search"
x-on:focus="$wire.showResults = true"
x-on:click.away="$wire.showResults = false"
/>
<!-- Custom loading indicator with gear icon -->
<div wire:loading wire:target="search" class="absolute right-3 top-1/2 transform -translate-y-1/2">
<flux:icon.cog class="w-6 h-6 text-zinc-400 animate-spin" />
</div>
</div>
<!-- Search Results Dropdown -->
<div x-show="showResults && $wire.search.length >= 2"
@ -25,15 +32,15 @@
class="flex items-center px-4 py-2 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors"
x-on:click="$wire.clearSearch()">
@if($result['icon'] === 'user')
<flux:icon.user class="w-4 h-4 text-zinc-400 mr-3" />
<flux:icon.user class="w-6 h-6 text-zinc-400 mr-3" />
@elseif($result['icon'] === 'truck')
<flux:icon.truck class="w-4 h-4 text-zinc-400 mr-3" />
<flux:icon.truck class="w-6 h-6 text-zinc-400 mr-3" />
@elseif($result['icon'] === 'clipboard-document-list')
<flux:icon.clipboard-document-list class="w-4 h-4 text-zinc-400 mr-3" />
<flux:icon.clipboard-document-list class="w-6 h-6 text-zinc-400 mr-3" />
@elseif($result['icon'] === 'calendar')
<flux:icon.calendar class="w-4 h-4 text-zinc-400 mr-3" />
<flux:icon.calendar class="w-6 h-6 text-zinc-400 mr-3" />
@else
<flux:icon.document class="w-4 h-4 text-zinc-400 mr-3" />
<flux:icon.document class="w-6 h-6 text-zinc-400 mr-3" />
@endif
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-zinc-900 dark:text-white truncate">

View File

@ -424,19 +424,19 @@
<!-- Damage Legend -->
<div class="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div class="flex items-center">
<div class="w-4 h-4 bg-red-500 rounded-full mr-2"></div>
<div class="w-6 h-6 bg-red-500 rounded-full mr-2"></div>
<span class="text-zinc-700 dark:text-zinc-300">Damage</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-orange-500 rounded-full mr-2"></div>
<div class="w-6 h-6 bg-orange-500 rounded-full mr-2"></div>
<span class="text-zinc-700 dark:text-zinc-300">Dent</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-yellow-500 rounded-full mr-2"></div>
<div class="w-6 h-6 bg-yellow-500 rounded-full mr-2"></div>
<span class="text-zinc-700 dark:text-zinc-300">Scratch</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-blue-500 rounded-full mr-2"></div>
<div class="w-6 h-6 bg-blue-500 rounded-full mr-2"></div>
<span class="text-zinc-700 dark:text-zinc-300">Other</span>
</div>
</div>
@ -702,7 +702,7 @@
item.innerHTML = `
<span class="text-sm">${damage.description}</span>
<button type="button" onclick="removeDamageMarker(${damage.id})" class="text-red-600 hover:text-red-800">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>

View File

@ -11,9 +11,13 @@
<!-- Filters -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<div class="relative">
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Search</label>
<input type="text" wire:model.live="search" placeholder="Search job numbers or customers..." class="w-full px-3 py-2 border border-zinc-300 dark:border-zinc-600 rounded-lg bg-white dark:bg-zinc-700 text-zinc-900 dark:text-zinc-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<input type="text" wire:model.live="search" placeholder="Search job numbers or customers..." class="w-full px-3 py-2 border border-zinc-300 dark:border-zinc-600 rounded-lg bg-white dark:bg-zinc-700 text-zinc-900 dark:text-zinc-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 pr-10">
<!-- Custom loading indicator with magnifying glass icon -->
<div wire:loading wire:target="search" class="absolute right-3 top-10 transform -translate-y-1/2">
<flux:icon.document-magnifying-glass class="w-6 h-6 text-zinc-400 animate-pulse" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Type</label>

View File

@ -293,7 +293,7 @@
};
@endphp
<div class="flex items-center space-x-2">
<span class="inline-block w-4 h-4 rounded-full {{ $colorClass }}"></span>
<span class="inline-block w-6 h-6 rounded-full {{ $colorClass }}"></span>
<span class="text-sm font-medium {{ $textColorClass }} capitalize">{{ $damage['type'] }}</span>
</div>
</div>

View File

@ -168,11 +168,11 @@
<div class="flex-shrink-0">
@if($movement->movement_type === 'in')
<div class="p-1 bg-green-100 dark:bg-green-900 rounded">
<flux:icon.arrow-down class="w-4 h-4 text-green-600 dark:text-green-400" />
<flux:icon.arrow-down class="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
@else
<div class="p-1 bg-red-100 dark:bg-red-900 rounded">
<flux:icon.arrow-up class="w-4 h-4 text-red-600 dark:text-red-400" />
<flux:icon.arrow-up class="w-6 h-6 text-red-600 dark:text-red-400" />
</div>
@endif
</div>
@ -270,37 +270,37 @@
<h2 class="text-lg font-medium text-zinc-900 dark:text-white dark:text-white mb-4">Quick Actions</h2>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<flux:button wire:navigate href="{{ route('inventory.parts.create') }}" variant="primary" class="w-full">
<flux:icon.plus class="w-4 h-4 mr-2" />
<flux:icon.plus class="w-6 h-6 mr-2" />
Add Part
</flux:button>
<flux:button wire:navigate href="{{ route('inventory.purchase-orders.create') }}" variant="outline" class="w-full">
<flux:icon.shopping-cart class="w-4 h-4 mr-2" />
<flux:icon.shopping-cart class="w-6 h-6 mr-2" />
Create Purchase Order
</flux:button>
<flux:button wire:navigate href="{{ route('inventory.stock-movements.create') }}" variant="outline" class="w-full">
<flux:icon.clipboard-document-list class="w-4 h-4 mr-2" />
<flux:icon.clipboard-document-list class="w-6 h-6 mr-2" />
Record Stock Movement
</flux:button>
<flux:button wire:navigate href="{{ route('inventory.suppliers.create') }}" variant="outline" class="w-full">
<flux:icon.building-office class="w-4 h-4 mr-2" />
<flux:icon.building-office class="w-6 h-6 mr-2" />
Add Supplier
</flux:button>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
<flux:button wire:navigate href="{{ route('inventory.parts.index', ['stockFilter' => 'low_stock']) }}" variant="outline" class="w-full">
<flux:icon.exclamation-triangle class="w-4 h-4 mr-2" />
<flux:icon.exclamation-triangle class="w-6 h-6 mr-2" />
Low Stock Items
</flux:button>
<flux:button wire:navigate href="{{ route('inventory.parts.index', ['stockFilter' => 'out_of_stock']) }}" variant="outline" class="w-full">
<flux:icon.x-circle class="w-4 h-4 mr-2" />
<flux:icon.x-circle class="w-6 h-6 mr-2" />
Out of Stock
</flux:button>
<flux:button wire:navigate href="{{ route('inventory.stock-movements.index') }}" variant="outline" class="w-full">
<flux:icon.clipboard-document-list class="w-4 h-4 mr-2" />
<flux:icon.clipboard-document-list class="w-6 h-6 mr-2" />
View Stock History
</flux:button>
<flux:button wire:navigate href="{{ route('inventory.purchase-orders.index') }}" variant="outline" class="w-full">
<flux:icon.shopping-cart class="w-4 h-4 mr-2" />
<flux:icon.shopping-cart class="w-6 h-6 mr-2" />
Purchase Orders
</flux:button>
</div>

View File

@ -6,7 +6,7 @@
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400 dark:text-gray-400">Create a new part in your inventory catalog</p>
</div>
<flux:button wire:navigate href="{{ route('inventory.parts.index') }}" variant="subtle">
<flux:icon.arrow-left class="w-4 h-4 mr-2" />
<flux:icon.arrow-left class="w-6 h-6 mr-2" />
Back to Parts
</flux:button>
</div>

View File

@ -6,7 +6,7 @@
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400 dark:text-gray-400">Update part information in your inventory catalog</p>
</div>
<flux:button wire:navigate href="{{ route('inventory.parts.index') }}" variant="subtle">
<flux:icon.arrow-left class="w-4 h-4 mr-2" />
<flux:icon.arrow-left class="w-6 h-6 mr-2" />
Back to Parts
</flux:button>
</div>

View File

@ -151,7 +151,7 @@
<div class="flex items-center space-x-1">
<span>Part #</span>
@if($sortBy === 'part_number')
<flux:icon.chevron-up class="w-4 h-4 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
<flux:icon.chevron-up class="w-6 h-6 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
@endif
</div>
</th>
@ -159,7 +159,7 @@
<div class="flex items-center space-x-1">
<span>Part Details</span>
@if($sortBy === 'name')
<flux:icon.chevron-up class="w-4 h-4 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
<flux:icon.chevron-up class="w-6 h-6 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
@endif
</div>
</th>
@ -168,7 +168,7 @@
<div class="flex items-center justify-center space-x-1">
<span>Stock Level</span>
@if($sortBy === 'quantity_on_hand')
<flux:icon.chevron-up class="w-4 h-4 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
<flux:icon.chevron-up class="w-6 h-6 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
@endif
</div>
</th>
@ -176,7 +176,7 @@
<div class="flex items-center justify-end space-x-1">
<span>Pricing</span>
@if($sortBy === 'cost_price')
<flux:icon.chevron-up class="w-4 h-4 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
<flux:icon.chevron-up class="w-6 h-6 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
@endif
</div>
</th>

View File

@ -6,7 +6,7 @@
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400 dark:text-gray-400">Create a new purchase order for inventory restocking</p>
</div>
<flux:button wire:navigate href="{{ route('inventory.purchase-orders.index') }}" variant="ghost">
<flux:icon.arrow-left class="w-4 h-4" />
<flux:icon.arrow-left class="w-6 h-6" />
Back to Purchase Orders
</flux:button>
</div>

View File

@ -6,7 +6,7 @@
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400 dark:text-gray-400">Update purchase order details and items</p>
</div>
<flux:button wire:navigate href="{{ route('inventory.purchase-orders.show', $purchaseOrder) }}" variant="ghost">
<flux:icon.arrow-left class="w-4 h-4" />
<flux:icon.arrow-left class="w-6 h-6" />
Back to Order
</flux:button>
</div>

View File

@ -179,9 +179,9 @@
<span>Order Number</span>
@if($sortBy === 'po_number')
@if($sortDirection === 'asc')
<flux:icon.chevron-up class="w-4 h-4" />
<flux:icon.chevron-up class="w-6 h-6" />
@else
<flux:icon.chevron-down class="w-4 h-4" />
<flux:icon.chevron-down class="w-6 h-6" />
@endif
@endif
</div>
@ -191,9 +191,9 @@
<span>Order Date</span>
@if($sortBy === 'order_date')
@if($sortDirection === 'asc')
<flux:icon.chevron-up class="w-4 h-4" />
<flux:icon.chevron-up class="w-6 h-6" />
@else
<flux:icon.chevron-down class="w-4 h-4" />
<flux:icon.chevron-down class="w-6 h-6" />
@endif
@endif
</div>

View File

@ -33,7 +33,7 @@
</flux:button>
@endif
<flux:button wire:navigate href="{{ route('inventory.purchase-orders.index') }}" variant="ghost">
<flux:icon.arrow-left class="w-4 h-4" />
<flux:icon.arrow-left class="w-6 h-6" />
Back to Orders
</flux:button>
</div>

View File

@ -6,7 +6,7 @@
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400 dark:text-gray-400">Manually record inventory adjustments and movements</p>
</div>
<flux:button wire:navigate href="{{ route('inventory.stock-movements.index') }}" variant="ghost">
<flux:icon.arrow-left class="w-4 h-4" />
<flux:icon.arrow-left class="w-6 h-6" />
Back to Stock Movements
</flux:button>
</div>

View File

@ -161,9 +161,9 @@
<span>Date</span>
@if($sortBy === 'created_at')
@if($sortDirection === 'asc')
<flux:icon.chevron-up class="w-4 h-4" />
<flux:icon.chevron-up class="w-6 h-6" />
@else
<flux:icon.chevron-down class="w-4 h-4" />
<flux:icon.chevron-down class="w-6 h-6" />
@endif
@endif
</div>

View File

@ -6,7 +6,7 @@
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400 dark:text-gray-400">Create a new supplier for your inventory</p>
</div>
<flux:button wire:navigate href="{{ route('inventory.suppliers.index') }}" variant="ghost">
<flux:icon.arrow-left class="w-4 h-4" />
<flux:icon.arrow-left class="w-6 h-6" />
Back to Suppliers
</flux:button>
</div>

View File

@ -6,7 +6,7 @@
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400 dark:text-gray-400">Update supplier information</p>
</div>
<flux:button wire:navigate href="{{ route('inventory.suppliers.index') }}" variant="ghost">
<flux:icon.arrow-left class="w-4 h-4" />
<flux:icon.arrow-left class="w-6 h-6" />
Back to Suppliers
</flux:button>
</div>

View File

@ -108,7 +108,7 @@
<div class="flex items-center space-x-1">
<span>Name</span>
@if($sortBy === 'name')
<flux:icon.chevron-up class="w-4 h-4 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
<flux:icon.chevron-up class="w-6 h-6 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
@endif
</div>
</th>
@ -116,7 +116,7 @@
<div class="flex items-center space-x-1">
<span>Company</span>
@if($sortBy === 'company_name')
<flux:icon.chevron-up class="w-4 h-4 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
<flux:icon.chevron-up class="w-6 h-6 {{ $sortDirection === 'asc' ? '' : 'rotate-180' }}" />
@endif
</div>
</th>
@ -167,9 +167,9 @@
<div class="flex items-center">
@for($i = 1; $i <= 5; $i++)
@if($i <= $supplier->rating)
<flux:icon.star class="w-4 h-4 text-yellow-400" />
<flux:icon.star class="w-6 h-6 text-yellow-400" />
@else
<flux:icon.star class="w-4 h-4 text-gray-300 dark:text-zinc-600 dark:text-zinc-400" />
<flux:icon.star class="w-6 h-6 text-gray-300 dark:text-zinc-600 dark:text-zinc-400" />
@endif
@endfor
</div>

View File

@ -0,0 +1,214 @@
<div class="max-w-7xl mx-auto">
<div class="mb-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Create Invoice from Estimate</h1>
<p class="mt-1 text-sm text-gray-600">Converting estimate #{{ $estimate->estimate_number }} to invoice</p>
</div>
<div class="flex space-x-3">
<flux:button variant="outline" href="{{ route('estimates.show', $estimate) }}">
Back to Estimate
</flux:button>
</div>
</div>
</div>
<!-- Estimate Reference -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div class="flex items-center">
<flux:icon name="document-text" class="h-5 w-5 text-blue-400 mr-2" />
<div>
<h3 class="text-sm font-medium text-blue-800">Source Estimate</h3>
<p class="text-sm text-blue-600">{{ $estimate->subject }} - {{ $estimate->estimate_number }}</p>
</div>
</div>
</div>
<form wire:submit="save" class="space-y-6">
<!-- Basic Information -->
<div class="bg-white shadow rounded-lg p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Invoice Details</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<flux:field>
<flux:label>Subject</flux:label>
<flux:input wire:model="subject" placeholder="Invoice subject" />
<flux:error name="subject" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Branch</flux:label>
<flux:select wire:model="branch_id">
<flux:option value="">Select branch</flux:option>
@foreach($branches as $branch)
<flux:option value="{{ $branch->id }}">{{ $branch->name }}</flux:option>
@endforeach
</flux:select>
<flux:error name="branch_id" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Invoice Date</flux:label>
<flux:input type="date" wire:model="invoice_date" />
<flux:error name="invoice_date" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Due Date</flux:label>
<flux:input type="date" wire:model="due_date" />
<flux:error name="due_date" />
</flux:field>
</div>
<div class="md:col-span-2">
<flux:field>
<flux:label>Description</flux:label>
<flux:textarea wire:model="description" rows="3" placeholder="Invoice description..." />
<flux:error name="description" />
</flux:field>
</div>
</div>
</div>
<!-- Line Items -->
<div class="bg-white shadow rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-gray-900">Line Items</h3>
<flux:button type="button" wire:click="addLineItem" variant="outline">
Add Item
</flux:button>
</div>
<div class="space-y-4">
@foreach($lineItems as $index => $item)
<div class="border border-gray-200 rounded-lg p-4">
<div class="flex items-center justify-between mb-4">
<h4 class="text-sm font-medium text-gray-900">Item {{ $index + 1 }}</h4>
@if(count($lineItems) > 1)
<flux:button type="button" wire:click="removeLineItem({{ $index }})" variant="danger" size="sm">
Remove
</flux:button>
@endif
</div>
<div class="grid grid-cols-1 md:grid-cols-6 gap-4">
<div>
<flux:field>
<flux:label>Type</flux:label>
<flux:select wire:model="lineItems.{{ $index }}.type">
<flux:option value="service">Service</flux:option>
<flux:option value="part">Part</flux:option>
<flux:option value="other">Other</flux:option>
</flux:select>
</flux:field>
</div>
@if($item['type'] === 'service')
<div>
<flux:field>
<flux:label>Service Item</flux:label>
<flux:select wire:model="lineItems.{{ $index }}.service_item_id">
<flux:option value="">Select service</flux:option>
@foreach($serviceItems as $serviceItem)
<flux:option value="{{ $serviceItem->id }}">{{ $serviceItem->name }} - ${{ number_format($serviceItem->price, 2) }}</flux:option>
@endforeach
</flux:select>
</flux:field>
</div>
@elseif($item['type'] === 'part')
<div>
<flux:field>
<flux:label>Part</flux:label>
<select wire:model="lineItems.{{ $index }}.part_id" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
<option value="">Select part</option>
@foreach($parts as $part)
<option value="{{ $part->id }}">{{ $part->part_number }} - {{ $part->name }} - ${{ number_format($part->sell_price, 2) }}</option>
@endforeach
</select>
</flux:field>
</div>
@else
<div></div>
@endif
<div>
<flux:field>
<flux:label>Description</flux:label>
<flux:input wire:model="lineItems.{{ $index }}.description" placeholder="Description" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Quantity</flux:label>
<flux:input type="number" wire:model="lineItems.{{ $index }}.quantity" min="1" step="1" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Unit Price</flux:label>
<flux:input type="number" wire:model="lineItems.{{ $index }}.unit_price" step="0.01" min="0" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Total</flux:label>
<flux:input value="${{ number_format($item['total'], 2) }}" readonly />
</flux:field>
</div>
</div>
</div>
@endforeach
</div>
</div>
<!-- Invoice Summary -->
<div class="bg-white shadow rounded-lg p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Invoice Summary</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<flux:field>
<flux:label>Tax Rate (%)</flux:label>
<flux:input type="number" wire:model="tax_rate" step="0.01" min="0" max="100" />
<flux:error name="tax_rate" />
</flux:field>
</div>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-sm text-gray-600">Subtotal:</span>
<span class="text-sm font-medium">${{ number_format($this->subtotal, 2) }}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-gray-600">Tax ({{ $tax_rate }}%):</span>
<span class="text-sm font-medium">${{ number_format($this->taxAmount, 2) }}</span>
</div>
<div class="flex justify-between border-t pt-2">
<span class="text-base font-semibold">Total:</span>
<span class="text-base font-semibold">${{ number_format($this->total, 2) }}</span>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-end space-x-3">
<flux:button type="button" variant="outline" href="{{ route('estimates.show', $estimate) }}">
Cancel
</flux:button>
<flux:button type="submit" variant="primary">
Create Invoice
</flux:button>
</div>
</form>
</div>

View File

@ -0,0 +1,224 @@
<div>
<flux:header>
<flux:heading size="xl">Create Invoice</flux:heading>
<flux:subheading>Create a new invoice for services and parts</flux:subheading>
</flux:header>
<form wire:submit.prevent="save" class="space-y-6">
{{-- Basic Information --}}
<div class="bg-white dark:bg-zinc-800 shadow-sm rounded-lg border border-zinc-200 dark:border-zinc-700 p-6 space-y-6">
<flux:heading size="lg">Invoice Details</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<flux:field>
<flux:label>Customer</flux:label>
<flux:select wire:model="customer_id" placeholder="Select customer...">
@foreach($customers as $customer)
<flux:select.option value="{{ $customer->id }}">{{ $customer->name }}</flux:select.option>
@endforeach
</flux:select>
<flux:error name="customer_id" />
</flux:field>
<flux:field>
<flux:label>Branch</flux:label>
<flux:select wire:model="branch_id" placeholder="Select branch...">
@foreach($branches as $branch)
<flux:select.option value="{{ $branch->id }}">{{ $branch->name }}</flux:select.option>
@endforeach
</flux:select>
<flux:error name="branch_id" />
</flux:field>
<flux:field>
<flux:label>Invoice Date</flux:label>
<flux:input type="date" wire:model="invoice_date" />
<flux:error name="invoice_date" />
</flux:field>
<flux:field>
<flux:label>Due Date</flux:label>
<flux:input type="date" wire:model="due_date" />
<flux:error name="due_date" />
</flux:field>
</div>
<flux:field>
<flux:label>Description</flux:label>
<flux:textarea wire:model="description" placeholder="Brief description of work performed..." rows="2" />
<flux:error name="description" />
</flux:field>
</div>
{{-- Line Items --}}
<div class="bg-white dark:bg-zinc-800 shadow-sm rounded-lg border border-zinc-200 dark:border-zinc-700 p-6 space-y-6">
<div class="flex justify-between items-center">
<flux:heading size="lg">Line Items</flux:heading>
<flux:button wire:click="addLineItem" variant="ghost" size="sm">
<flux:icon.plus class="size-4" />
Add Item
</flux:button>
</div>
<div class="space-y-4">
@foreach($line_items as $index => $item)
<div class="border rounded-lg p-4 bg-zinc-50 dark:bg-zinc-900/50">
<div class="grid grid-cols-1 md:grid-cols-12 gap-4 items-start">
{{-- Type --}}
<div class="md:col-span-2">
<flux:label>Type</flux:label>
<flux:select wire:model="line_items.{{ $index }}.type">
<flux:select.option value="labour">Labour</flux:select.option>
<flux:select.option value="parts">Parts</flux:select.option>
<flux:select.option value="miscellaneous">Miscellaneous</flux:select.option>
</flux:select>
</div>
{{-- Part Selection (only for parts) --}}
@if($item['type'] === 'parts')
<div class="md:col-span-3">
<flux:label>Part</flux:label>
<select wire:model="line_items.{{ $index }}.part_id" class="w-full rounded-md border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Select part...</option>
@if(count($parts) === 0)
<option value="" disabled>No parts available</option>
@endif
@foreach($parts as $part)
<option value="{{ $part->id }}">{{ $part->name }} ({{ $part->part_number }}) - ${{ number_format($part->sell_price, 2) }}</option>
@endforeach
</select>
<small class="text-zinc-500">{{ count($parts) }} parts available</small>
</div>
@else
<div class="md:col-span-3">
<flux:label>Description</flux:label>
<flux:input wire:model="line_items.{{ $index }}.description" placeholder="Item description..." />
</div>
@endif
{{-- Quantity --}}
<div class="md:col-span-2">
<flux:label>Quantity</flux:label>
<flux:input type="number" wire:model="line_items.{{ $index }}.quantity" min="1" step="1" />
</div>
{{-- Unit Price --}}
<div class="md:col-span-2">
<flux:label>Unit Price</flux:label>
<flux:input type="number" wire:model="line_items.{{ $index }}.unit_price" min="0" step="0.01" />
</div>
{{-- Total --}}
<div class="md:col-span-2">
<flux:label>Total</flux:label>
<div class="px-3 py-2 bg-white dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-600 rounded text-sm">
${{ number_format(($item['quantity'] ?? 0) * ($item['unit_price'] ?? 0), 2) }}
</div>
</div>
{{-- Remove Button --}}
<div class="md:col-span-1 flex items-end">
@if(count($line_items) > 1)
<flux:button wire:click="removeLineItem({{ $index }})" variant="danger" size="sm">
<flux:icon.trash class="size-4" />
</flux:button>
@endif
</div>
</div>
{{-- Additional fields for parts --}}
@if($item['type'] === 'parts')
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<flux:field>
<flux:label>Part Number</flux:label>
<flux:input wire:model="line_items.{{ $index }}.part_number" placeholder="Part number..." />
</flux:field>
<flux:field>
<flux:label>Technical Notes</flux:label>
<flux:input wire:model="line_items.{{ $index }}.technical_notes" placeholder="Technical notes..." />
</flux:field>
</div>
@endif
{{-- Technical notes for labour --}}
@if($item['type'] === 'labour')
<div class="mt-4">
<flux:field>
<flux:label>Technical Notes</flux:label>
<flux:input wire:model="line_items.{{ $index }}.technical_notes" placeholder="Technical notes..." />
</flux:field>
</div>
@endif
</div>
@endforeach
</div>
<flux:error name="line_items" />
</div>
{{-- Totals and Additional Info --}}
<div class="bg-white dark:bg-zinc-800 shadow-sm rounded-lg border border-zinc-200 dark:border-zinc-700 p-6 space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{{-- Additional Information --}}
<div class="space-y-4">
<flux:field>
<flux:label>Tax Rate (%)</flux:label>
<flux:input type="number" wire:model="tax_rate" min="0" max="100" step="0.01" />
<flux:error name="tax_rate" />
</flux:field>
<flux:field>
<flux:label>Discount Amount</flux:label>
<flux:input type="number" wire:model="discount_amount" min="0" step="0.01" />
<flux:error name="discount_amount" />
</flux:field>
<flux:field>
<flux:label>Notes</flux:label>
<flux:textarea wire:model="notes" placeholder="Additional notes..." rows="3" />
<flux:error name="notes" />
</flux:field>
</div>
{{-- Totals Summary --}}
<div class="bg-zinc-50 dark:bg-zinc-900/50 rounded-lg p-4">
<flux:heading size="base" class="mb-4">Invoice Summary</flux:heading>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span>Subtotal:</span>
<span>${{ number_format($this->calculateSubtotal(), 2) }}</span>
</div>
@if($discount_amount > 0)
<div class="flex justify-between text-red-600">
<span>Discount:</span>
<span>-${{ number_format($discount_amount, 2) }}</span>
</div>
@endif
<div class="flex justify-between">
<span>Tax ({{ $tax_rate }}%):</span>
<span>${{ number_format($this->calculateTax(), 2) }}</span>
</div>
<hr class="my-2">
<div class="flex justify-between font-semibold text-lg">
<span>Total:</span>
<span>${{ number_format($this->calculateTotal(), 2) }}</span>
</div>
</div>
</div>
</div>
<flux:field>
<flux:label>Terms and Conditions</flux:label>
<flux:textarea wire:model="terms_and_conditions" placeholder="Payment terms and conditions..." rows="3" />
<flux:error name="terms_and_conditions" />
</flux:field>
</div>
{{-- Actions --}}
<div class="flex justify-end space-x-3">
<flux:button variant="ghost" href="{{ route('invoices.index') }}">Cancel</flux:button>
<flux:button type="submit" variant="primary">Create Invoice</flux:button>
</div>
</form>
</div>

View File

@ -0,0 +1,230 @@
<div>
<flux:header>
<flux:heading size="xl">Edit Invoice #{{ $invoice->invoice_number }}</flux:heading>
<flux:subheading>Modify invoice details and line items</flux:subheading>
<x-slot:actions>
<flux:badge :variant="$invoice->status === 'paid' ? 'success' : ($invoice->status === 'overdue' ? 'danger' : 'primary')">
{{ ucfirst($invoice->status) }}
</flux:badge>
</x-slot:actions>
</flux:header>
<form wire:submit.prevent="save" class="space-y-6">
{{-- Basic Information --}}
<div class="bg-white dark:bg-zinc-800 shadow-sm rounded-lg border border-zinc-200 dark:border-zinc-700 p-6 space-y-6">
<flux:heading size="lg">Invoice Details</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<flux:field>
<flux:label>Customer</flux:label>
<flux:select wire:model="customer_id" placeholder="Select customer...">
@foreach($customers as $customer)
<flux:select.option value="{{ $customer->id }}">{{ $customer->name }}</flux:select.option>
@endforeach
</flux:select>
<flux:error name="customer_id" />
</flux:field>
<flux:field>
<flux:label>Branch</flux:label>
<flux:select wire:model="branch_id" placeholder="Select branch...">
@foreach($branches as $branch)
<flux:select.option value="{{ $branch->id }}">{{ $branch->name }}</flux:select.option>
@endforeach
</flux:select>
<flux:error name="branch_id" />
</flux:field>
<flux:field>
<flux:label>Invoice Date</flux:label>
<flux:input type="date" wire:model="invoice_date" />
<flux:error name="invoice_date" />
</flux:field>
<flux:field>
<flux:label>Due Date</flux:label>
<flux:input type="date" wire:model="due_date" />
<flux:error name="due_date" />
</flux:field>
</div>
<flux:field>
<flux:label>Description</flux:label>
<flux:textarea wire:model="description" placeholder="Brief description of work performed..." rows="2" />
<flux:error name="description" />
</flux:field>
</div>
{{-- Line Items --}}
<div class="bg-white dark:bg-zinc-800 shadow-sm rounded-lg border border-zinc-200 dark:border-zinc-700 p-6 space-y-6">
<div class="flex justify-between items-center">
<flux:heading size="lg">Line Items</flux:heading>
<flux:button wire:click="addLineItem" variant="ghost" size="sm">
<flux:icon.plus class="size-4" />
Add Item
</flux:button>
</div>
<div class="space-y-4">
@foreach($line_items as $index => $item)
<div class="border rounded-lg p-4 bg-zinc-50 dark:bg-zinc-900/50">
<div class="grid grid-cols-1 md:grid-cols-12 gap-4 items-start">
{{-- Type --}}
<div class="md:col-span-2">
<flux:label>Type</flux:label>
<flux:select wire:model="line_items.{{ $index }}.type">
<flux:select.option value="labour">Labour</flux:select.option>
<flux:select.option value="parts">Parts</flux:select.option>
<flux:select.option value="miscellaneous">Miscellaneous</flux:select.option>
</flux:select>
</div>
{{-- Part Selection (only for parts) --}}
@if($item['type'] === 'parts')
<div class="md:col-span-3">
<flux:label>Part</flux:label>
<select wire:model="line_items.{{ $index }}.part_id" class="w-full rounded-md border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Select part...</option>
@if(count($parts) === 0)
<option value="" disabled>No parts available</option>
@endif
@foreach($parts as $part)
<option value="{{ $part->id }}">{{ $part->name }} ({{ $part->part_number }}) - ${{ number_format($part->sell_price, 2) }}</option>
@endforeach
</select>
<small class="text-zinc-500">{{ count($parts) }} parts available</small>
</div>
@else
<div class="md:col-span-3">
<flux:label>Description</flux:label>
<flux:input wire:model="line_items.{{ $index }}.description" placeholder="Item description..." />
</div>
@endif
{{-- Quantity --}}
<div class="md:col-span-2">
<flux:label>Quantity</flux:label>
<flux:input type="number" wire:model="line_items.{{ $index }}.quantity" min="1" step="1" />
</div>
{{-- Unit Price --}}
<div class="md:col-span-2">
<flux:label>Unit Price</flux:label>
<flux:input type="number" wire:model="line_items.{{ $index }}.unit_price" min="0" step="0.01" />
</div>
{{-- Total --}}
<div class="md:col-span-2">
<flux:label>Total</flux:label>
<div class="px-3 py-2 bg-white dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-600 rounded text-sm">
${{ number_format(($item['quantity'] ?? 0) * ($item['unit_price'] ?? 0), 2) }}
</div>
</div>
{{-- Remove Button --}}
<div class="md:col-span-1 flex items-end">
@if(count($line_items) > 1)
<flux:button wire:click="removeLineItem({{ $index }})" variant="danger" size="sm">
<flux:icon.trash class="size-4" />
</flux:button>
@endif
</div>
</div>
{{-- Additional fields for parts --}}
@if($item['type'] === 'parts')
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<flux:field>
<flux:label>Part Number</flux:label>
<flux:input wire:model="line_items.{{ $index }}.part_number" placeholder="Part number..." />
</flux:field>
<flux:field>
<flux:label>Technical Notes</flux:label>
<flux:input wire:model="line_items.{{ $index }}.technical_notes" placeholder="Technical notes..." />
</flux:field>
</div>
@endif
{{-- Technical notes for labour --}}
@if($item['type'] === 'labour')
<div class="mt-4">
<flux:field>
<flux:label>Technical Notes</flux:label>
<flux:input wire:model="line_items.{{ $index }}.technical_notes" placeholder="Technical notes..." />
</flux:field>
</div>
@endif
</div>
@endforeach
</div>
<flux:error name="line_items" />
</div>
{{-- Totals and Additional Info --}}
<div class="bg-white dark:bg-zinc-800 shadow-sm rounded-lg border border-zinc-200 dark:border-zinc-700 p-6 space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{{-- Additional Information --}}
<div class="space-y-4">
<flux:field>
<flux:label>Tax Rate (%)</flux:label>
<flux:input type="number" wire:model="tax_rate" min="0" max="100" step="0.01" />
<flux:error name="tax_rate" />
</flux:field>
<flux:field>
<flux:label>Discount Amount</flux:label>
<flux:input type="number" wire:model="discount_amount" min="0" step="0.01" />
<flux:error name="discount_amount" />
</flux:field>
<flux:field>
<flux:label>Notes</flux:label>
<flux:textarea wire:model="notes" placeholder="Additional notes..." rows="3" />
<flux:error name="notes" />
</flux:field>
</div>
{{-- Totals Summary --}}
<div class="bg-zinc-50 dark:bg-zinc-900/50 rounded-lg p-4">
<flux:heading size="base" class="mb-4">Invoice Summary</flux:heading>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span>Subtotal:</span>
<span>${{ number_format($this->calculateSubtotal(), 2) }}</span>
</div>
@if($discount_amount > 0)
<div class="flex justify-between text-red-600">
<span>Discount:</span>
<span>-${{ number_format($discount_amount, 2) }}</span>
</div>
@endif
<div class="flex justify-between">
<span>Tax ({{ $tax_rate }}%):</span>
<span>${{ number_format($this->calculateTax(), 2) }}</span>
</div>
<hr class="my-2">
<div class="flex justify-between font-semibold text-lg">
<span>Total:</span>
<span>${{ number_format($this->calculateTotal(), 2) }}</span>
</div>
</div>
</div>
</div>
<flux:field>
<flux:label>Terms and Conditions</flux:label>
<flux:textarea wire:model="terms_and_conditions" placeholder="Payment terms and conditions..." rows="3" />
<flux:error name="terms_and_conditions" />
</flux:field>
</div>
{{-- Actions --}}
<div class="flex justify-end space-x-3">
<flux:button variant="ghost" href="{{ route('invoices.show', $invoice) }}">Cancel</flux:button>
<flux:button type="submit" variant="primary">Update Invoice</flux:button>
</div>
</form>
</div>

View File

@ -0,0 +1,299 @@
<div>
<!-- Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Invoices</h1>
<p class="text-gray-600 dark:text-gray-400">Manage customer invoices and billing</p>
</div>
<div class="flex space-x-4">
<flux:button variant="outline" href="{{ route('invoices.create') }}">
<flux:icon.plus class="w-4 h-4 mr-2" />
New Invoice
</flux:button>
</div>
</div>
</div>
<!-- Filters -->
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow p-6 mb-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<div>
<flux:field>
<flux:label>Search</flux:label>
<flux:input wire:model.live.debounce.300ms="search" placeholder="Invoice number, customer name..." />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Status</flux:label>
<flux:select wire:model.live="filterStatus" placeholder="All Statuses">
<option value="">All Statuses</option>
@foreach($statusOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</flux:select>
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Branch</flux:label>
<flux:select wire:model.live="filterBranch" placeholder="All Branches">
<option value="">All Branches</option>
@foreach($branches as $branch)
<option value="{{ $branch->id }}">{{ $branch->name }}</option>
@endforeach
</flux:select>
</flux:field>
</div>
<div class="flex items-end">
<flux:button variant="outline" wire:click="clearFilters">
Clear Filters
</flux:button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<flux:field>
<flux:label>Date From</flux:label>
<flux:input type="date" wire:model.live="filterDateFrom" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Date To</flux:label>
<flux:input type="date" wire:model.live="filterDateTo" />
</flux:field>
</div>
</div>
</div>
<!-- Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
<flux:icon.document-text class="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Invoices</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ number_format($invoices->total()) }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
<flux:icon.check-circle class="w-6 h-6 text-green-600 dark:text-green-400" />
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Paid</p>
<p class="text-2xl font-bold text-green-600 dark:text-green-400">
{{ $invoices->where('status', 'paid')->count() }}
</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-yellow-100 dark:bg-yellow-900 rounded-lg">
<flux:icon.clock class="w-6 h-6 text-yellow-600 dark:text-yellow-400" />
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Pending</p>
<p class="text-2xl font-bold text-yellow-600 dark:text-yellow-400">
{{ $invoices->whereIn('status', ['draft', 'sent'])->count() }}
</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-red-100 dark:bg-red-900 rounded-lg">
<flux:icon.exclamation-triangle class="w-6 h-6 text-red-600 dark:text-red-400" />
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Overdue</p>
<p class="text-2xl font-bold text-red-600 dark:text-red-400">
{{ $invoices->where('status', 'overdue')->count() }}
</p>
</div>
</div>
</div>
</div>
<!-- Invoices Table -->
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-zinc-700">
<thead class="bg-gray-50 dark:bg-zinc-900">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer" wire:click="sortBy('invoice_number')">
<div class="flex items-center space-x-1">
<span>Invoice #</span>
@if($sortField === 'invoice_number')
@if($sortDirection === 'asc')
<flux:icon.chevron-up class="w-4 h-4" />
@else
<flux:icon.chevron-down class="w-4 h-4" />
@endif
@endif
</div>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Customer</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer" wire:click="sortBy('invoice_date')">
<div class="flex items-center space-x-1">
<span>Date</span>
@if($sortField === 'invoice_date')
@if($sortDirection === 'asc')
<flux:icon.chevron-up class="w-4 h-4" />
@else
<flux:icon.chevron-down class="w-4 h-4" />
@endif
@endif
</div>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer" wire:click="sortBy('due_date')">
<div class="flex items-center space-x-1">
<span>Due Date</span>
@if($sortField === 'due_date')
@if($sortDirection === 'asc')
<flux:icon.chevron-up class="w-4 h-4" />
@else
<flux:icon.chevron-down class="w-4 h-4" />
@endif
@endif
</div>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer" wire:click="sortBy('total_amount')">
<div class="flex items-center space-x-1">
<span>Amount</span>
@if($sortField === 'total_amount')
@if($sortDirection === 'asc')
<flux:icon.chevron-up class="w-4 h-4" />
@else
<flux:icon.chevron-down class="w-4 h-4" />
@endif
@endif
</div>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-zinc-800 divide-y divide-gray-200 dark:divide-zinc-700">
@forelse($invoices as $invoice)
<tr class="hover:bg-gray-50 dark:hover:bg-zinc-700">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900 dark:text-white">
{{ $invoice->invoice_number }}
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ $invoice->branch->name }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900 dark:text-white">
{{ $invoice->customer->name }}
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ $invoice->customer->email }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{{ $invoice->invoice_date->format('M j, Y') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{{ $invoice->due_date->format('M j, Y') }}
@if($invoice->isOverdue())
<span class="text-red-600 dark:text-red-400 font-medium">(Overdue)</span>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
${{ number_format($invoice->total_amount, 2) }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
@switch($invoice->status)
@case('draft')
bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200
@break
@case('sent')
bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-200
@break
@case('paid')
bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-200
@break
@case('overdue')
bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-200
@break
@case('cancelled')
bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200
@break
@endswitch
">
{{ ucfirst($invoice->status) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end space-x-2">
<flux:button size="sm" variant="ghost" href="{{ route('invoices.show', $invoice) }}" title="View">
<flux:icon.eye class="w-4 h-4" />
</flux:button>
@if($invoice->status !== 'paid')
<flux:button size="sm" variant="ghost" href="{{ route('invoices.edit', $invoice) }}" title="Edit">
<flux:icon.pencil class="w-4 h-4" />
</flux:button>
@if($invoice->status === 'draft')
<flux:button size="sm" variant="ghost" wire:click="sendInvoice({{ $invoice->id }})" title="Send">
<flux:icon.paper-airplane class="w-4 h-4" />
</flux:button>
@endif
<flux:button size="sm" variant="ghost" wire:click="markAsPaid({{ $invoice->id }})" title="Mark as Paid">
<flux:icon.check class="w-4 h-4" />
</flux:button>
@endif
@if($invoice->status === 'draft')
<flux:button size="sm" variant="ghost" wire:click="deleteInvoice({{ $invoice->id }})"
wire:confirm="Are you sure you want to delete this invoice?" title="Delete">
<flux:icon.trash class="w-4 h-4 text-red-500" />
</flux:button>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
<flux:icon.document-text class="w-12 h-12 mx-auto mb-4 opacity-50" />
<p class="text-lg font-medium">No invoices found</p>
<p class="mt-2">Get started by creating your first invoice.</p>
<flux:button variant="primary" href="{{ route('invoices.create') }}" class="mt-4">
Create Invoice
</flux:button>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($invoices->hasPages())
<div class="px-6 py-4 border-t border-gray-200 dark:border-zinc-700">
{{ $invoices->links() }}
</div>
@endif
</div>
</div>

View File

@ -0,0 +1,380 @@
<div>
<!-- Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center space-x-4">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Invoice {{ $invoice->invoice_number }}</h1>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-sm font-medium
@switch($invoice->status)
@case('draft')
bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200
@break
@case('sent')
bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-200
@break
@case('paid')
bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-200
@break
@case('overdue')
bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-200
@break
@case('cancelled')
bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200
@break
@endswitch
">
{{ ucfirst($invoice->status) }}
</span>
</div>
<p class="text-gray-600 dark:text-gray-400">
Created on {{ $invoice->created_at->format('M j, Y g:i A') }} by {{ $invoice->createdBy->name }}
</p>
</div>
<div class="flex space-x-4">
<flux:button variant="outline" wire:click="downloadPDF">
<flux:icon.arrow-down-tray class="w-4 h-4 mr-2" />
Download PDF
</flux:button>
@if($invoice->status !== 'paid')
<flux:button variant="outline" href="{{ route('invoices.edit', $invoice) }}">
<flux:icon.pencil class="w-4 h-4 mr-2" />
Edit
</flux:button>
@if($invoice->status === 'draft')
<flux:button variant="outline" wire:click="sendInvoice">
<flux:icon.paper-airplane class="w-4 h-4 mr-2" />
Send Invoice
</flux:button>
@endif
<flux:button variant="primary" wire:click="markAsPaid">
<flux:icon.check class="w-4 h-4 mr-2" />
Mark as Paid
</flux:button>
@endif
<flux:button variant="outline" wire:click="duplicateInvoice">
<flux:icon.document-duplicate class="w-4 h-4 mr-2" />
Duplicate
</flux:button>
</div>
</div>
</div>
<!-- Invoice Details -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Main Content -->
<div class="lg:col-span-2 space-y-8">
<!-- Invoice Information -->
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow p-6">
<h2 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Invoice Information</h2>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Invoice Number</p>
<p class="text-gray-900 dark:text-white">{{ $invoice->invoice_number }}</p>
</div>
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Branch</p>
<p class="text-gray-900 dark:text-white">{{ $invoice->branch->name }}</p>
</div>
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Invoice Date</p>
<p class="text-gray-900 dark:text-white">{{ $invoice->invoice_date->format('M j, Y') }}</p>
</div>
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Due Date</p>
<p class="text-gray-900 dark:text-white">{{ $invoice->due_date->format('M j, Y') }}
@if($invoice->isOverdue())
<span class="text-red-600 dark:text-red-400 font-medium">(Overdue)</span>
@endif
</p>
</div>
</div>
@if($invoice->description)
<div class="mt-4">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Description</p>
<p class="text-gray-900 dark:text-white">{{ $invoice->description }}</p>
</div>
@endif
</div>
<!-- Customer Information -->
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow p-6">
<h2 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Customer Information</h2>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Name</p>
<p class="text-gray-900 dark:text-white">{{ $invoice->customer->name }}</p>
</div>
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Email</p>
<p class="text-gray-900 dark:text-white">{{ $invoice->customer->email }}</p>
</div>
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Phone</p>
<p class="text-gray-900 dark:text-white">{{ $invoice->customer->phone }}</p>
</div>
@if($invoice->customer->address)
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Address</p>
<p class="text-gray-900 dark:text-white">{{ $invoice->customer->address }}</p>
</div>
@endif
</div>
</div>
<!-- Vehicle Information (if applicable) -->
@php
$vehicle = $invoice->serviceOrder?->vehicle ?? $invoice->jobCard?->vehicle;
@endphp
@if($vehicle)
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow p-6">
<h2 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Vehicle Information</h2>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Vehicle</p>
<p class="text-gray-900 dark:text-white">{{ $vehicle->year }} {{ $vehicle->make }} {{ $vehicle->model }}</p>
</div>
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">License Plate</p>
<p class="text-gray-900 dark:text-white">{{ $vehicle->license_plate }}</p>
</div>
@if($vehicle->vin)
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">VIN</p>
<p class="text-gray-900 dark:text-white">{{ $vehicle->vin }}</p>
</div>
@endif
@if($vehicle->mileage)
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Mileage</p>
<p class="text-gray-900 dark:text-white">{{ number_format($vehicle->mileage) }} miles</p>
</div>
@endif
</div>
</div>
@endif
<!-- Line Items -->
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-zinc-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Services & Parts</h2>
</div>
@if($invoice->lineItems->count() > 0)
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-zinc-700">
<thead class="bg-gray-50 dark:bg-zinc-900">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Description</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Qty</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Unit Price</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Total</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-zinc-800 divide-y divide-gray-200 dark:divide-zinc-700">
@foreach($invoice->lineItems as $item)
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
@switch($item->type)
@case('labour')
bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-200
@break
@case('parts')
bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-200
@break
@case('miscellaneous')
bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200
@break
@endswitch
">
{{ ucfirst($item->type) }}
</span>
</td>
<td class="px-6 py-4">
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ $item->description }}</div>
@if($item->part)
<div class="text-sm text-gray-500 dark:text-gray-400">Part #: {{ $item->part->part_number }}</div>
@endif
@if($item->part_number)
<div class="text-sm text-gray-500 dark:text-gray-400">Part #: {{ $item->part_number }}</div>
@endif
@if($item->technical_notes)
<div class="text-sm text-gray-500 dark:text-gray-400">{{ $item->technical_notes }}</div>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap text-center text-sm text-gray-900 dark:text-white">
{{ $item->quantity }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white">
${{ number_format($item->unit_price, 2) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium text-gray-900 dark:text-white">
${{ number_format($item->total_amount, 2) }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<div class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
<flux:icon.document-text class="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>No line items added to this invoice.</p>
</div>
@endif
</div>
<!-- Notes -->
@if($invoice->notes)
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow p-6">
<h2 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Notes</h2>
<p class="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{ $invoice->notes }}</p>
</div>
@endif
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Financial Summary -->
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow p-6">
<h2 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Financial Summary</h2>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Subtotal:</span>
<span class="text-gray-900 dark:text-white">${{ number_format($invoice->subtotal, 2) }}</span>
</div>
@if($invoice->discount_amount > 0)
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Discount:</span>
<span class="text-red-600 dark:text-red-400">-${{ number_format($invoice->discount_amount, 2) }}</span>
</div>
@endif
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">Tax ({{ $invoice->tax_rate }}%):</span>
<span class="text-gray-900 dark:text-white">${{ number_format($invoice->tax_amount, 2) }}</span>
</div>
<div class="border-t border-gray-200 dark:border-zinc-700 pt-3">
<div class="flex justify-between">
<span class="text-lg font-semibold text-gray-900 dark:text-white">Total:</span>
<span class="text-lg font-semibold text-gray-900 dark:text-white">${{ number_format($invoice->total_amount, 2) }}</span>
</div>
</div>
</div>
</div>
<!-- Payment Information -->
@if($invoice->isPaid())
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg shadow p-6">
<h2 class="text-lg font-semibold mb-4 text-green-800 dark:text-green-200">Payment Information</h2>
<div class="space-y-3">
<div>
<p class="text-sm font-medium text-green-600 dark:text-green-400">Payment Date</p>
<p class="text-green-800 dark:text-green-200">{{ $invoice->paid_at->format('M j, Y g:i A') }}</p>
</div>
@if($invoice->payment_method)
<div>
<p class="text-sm font-medium text-green-600 dark:text-green-400">Payment Method</p>
<p class="text-green-800 dark:text-green-200">{{ ucfirst(str_replace('_', ' ', $invoice->payment_method)) }}</p>
</div>
@endif
@if($invoice->payment_reference)
<div>
<p class="text-sm font-medium text-green-600 dark:text-green-400">Reference</p>
<p class="text-green-800 dark:text-green-200">{{ $invoice->payment_reference }}</p>
</div>
@endif
@if($invoice->payment_notes)
<div>
<p class="text-sm font-medium text-green-600 dark:text-green-400">Notes</p>
<p class="text-green-800 dark:text-green-200">{{ $invoice->payment_notes }}</p>
</div>
@endif
</div>
</div>
@endif
<!-- Related Records -->
@if($invoice->serviceOrder || $invoice->jobCard || $invoice->estimate)
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow p-6">
<h2 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Related Records</h2>
<div class="space-y-3">
@if($invoice->serviceOrder)
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Service Order</p>
<flux:button variant="ghost" href="{{ route('service-orders.show', $invoice->serviceOrder) }}" class="p-0 text-blue-600 hover:text-blue-800">
#{{ $invoice->serviceOrder->order_number }}
</flux:button>
</div>
@endif
@if($invoice->jobCard)
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Job Card</p>
<flux:button variant="ghost" href="{{ route('job-cards.show', $invoice->jobCard) }}" class="p-0 text-blue-600 hover:text-blue-800">
#{{ $invoice->jobCard->job_number }}
</flux:button>
</div>
@endif
@if($invoice->estimate)
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Estimate</p>
<flux:button variant="ghost" href="{{ route('estimates.show', $invoice->estimate) }}" class="p-0 text-blue-600 hover:text-blue-800">
#{{ $invoice->estimate->estimate_number }}
</flux:button>
</div>
@endif
</div>
</div>
@endif
<!-- Actions -->
<div class="bg-white dark:bg-zinc-800 rounded-lg shadow p-6">
<h2 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Quick Actions</h2>
<div class="space-y-3">
<flux:button variant="outline" wire:click="downloadPDF" class="w-full justify-center">
<flux:icon.arrow-down-tray class="w-4 h-4 mr-2" />
Download PDF
</flux:button>
@if($invoice->status !== 'paid')
@if($invoice->status === 'draft')
<flux:button variant="outline" wire:click="sendInvoice" class="w-full justify-center">
<flux:icon.paper-airplane class="w-4 h-4 mr-2" />
Send Invoice
</flux:button>
@endif
<flux:button variant="primary" wire:click="markAsPaid" class="w-full justify-center">
<flux:icon.check class="w-4 h-4 mr-2" />
Mark as Paid
</flux:button>
@endif
<flux:button variant="outline" wire:click="duplicateInvoice" class="w-full justify-center">
<flux:icon.document-duplicate class="w-4 h-4 mr-2" />
Duplicate Invoice
</flux:button>
<flux:button variant="outline" href="{{ route('invoices.index') }}" class="w-full justify-center">
<flux:icon.arrow-left class="w-4 h-4 mr-2" />
Back to Invoices
</flux:button>
</div>
</div>
</div>
</div>
</div>

View File

@ -107,11 +107,18 @@
<!-- Filters -->
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 p-4">
<flux:input
wire:model.live="search"
placeholder="Search job cards..."
icon="magnifying-glass"
/>
<div class="relative">
<flux:input
wire:model.live="search"
placeholder="Search job cards..."
icon="magnifying-glass"
:loading="false"
/>
<!-- Custom loading indicator with clipboard icon -->
<div wire:loading wire:target="search" class="absolute right-3 top-1/2 transform -translate-y-1/2">
<flux:icon.clipboard-document-list class="w-6 h-6 text-zinc-400 animate-pulse" />
</div>
</div>
<flux:select wire:model.live="statusFilter" placeholder="All Statuses">
@foreach($statusOptions as $value => $label)

View File

@ -41,7 +41,7 @@
<div class="flex flex-col space-y-3 ml-6">
<div class="flex space-x-3">
<a href="{{ route('job-cards.edit', $jobCard) }}" class="inline-flex items-center px-4 py-2 border border-zinc-300 dark:border-zinc-600 hover:bg-zinc-50 dark:hover:bg-zinc-700 text-zinc-700 dark:text-zinc-300 font-medium rounded-lg transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
Edit
@ -50,24 +50,24 @@
<!-- Diagnosis Workflow Actions -->
@if($jobCard->status === 'inspected')
<flux:button wire:click="openAssignmentModal" variant="filled" color="blue" size="sm">
<flux:icon.wrench-screwdriver class="w-4 h-4" />
<flux:icon.wrench-screwdriver class="w-6 h-6" />
Assign for Diagnosis
</flux:button>
@elseif($jobCard->status === 'assigned_for_diagnosis')
<flux:button wire:click="startDiagnosis" variant="filled" color="green" size="sm">
<flux:icon.play class="w-4 h-4" />
<flux:icon.play class="w-6 h-6" />
Start Diagnosis
</flux:button>
@elseif($jobCard->status === 'in_diagnosis' && !$jobCard->diagnosis)
<a href="{{ route('diagnosis.create', $jobCard) }}" class="inline-flex items-center px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white font-medium rounded-lg transition-colors shadow-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Create Diagnosis
</a>
@elseif($jobCard->diagnosis)
<a href="{{ route('diagnosis.show', $jobCard->diagnosis) }}" class="inline-flex items-center px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors shadow-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
View Diagnosis
@ -76,7 +76,7 @@
@if(in_array($jobCard->status, ['received', 'in_diagnosis']))
<a href="{{ route('job-cards.workflow', $jobCard) }}" class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors shadow-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
{{ $jobCard->status === 'received' ? 'Start Workflow' : 'Continue Workflow' }}
@ -462,13 +462,13 @@
<!-- Additional Quick Actions -->
<div class="grid grid-cols-2 gap-3 mt-6">
<a href="#" class="inline-flex items-center justify-center px-4 py-2 bg-amber-100 dark:bg-amber-900/20 hover:bg-amber-200 dark:hover:bg-amber-900/40 text-amber-800 dark:text-amber-300 text-sm font-medium rounded-lg transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H9.5a2 2 0 01-2-2V5a2 2 0 012-2H14"></path>
</svg>
Print
</a>
<a href="#" class="inline-flex items-center justify-center px-4 py-2 bg-indigo-100 dark:bg-indigo-900/20 hover:bg-indigo-200 dark:hover:bg-indigo-900/40 text-indigo-800 dark:text-indigo-300 text-sm font-medium rounded-lg transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z"></path>
</svg>
Share

View File

@ -229,21 +229,21 @@
<div class="flex flex-wrap gap-4">
<button wire:click="exportWorkflowReport"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Workflow Summary
</button>
<button wire:click="exportLaborReport"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Labor Utilization
</button>
<button wire:click="exportQualityReport"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Quality Metrics

View File

@ -10,7 +10,7 @@
</div>
<button wire:click="toggleForm"
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
{{ $showForm ? 'Cancel' : 'Add Service Item' }}

View File

@ -120,7 +120,7 @@
<div class="flex items-center justify-between mb-4">
<flux:heading size="lg">Service Items</flux:heading>
<flux:button wire:click="addServiceItem" variant="ghost" size="sm">
<flux:icon.plus class="w-4 h-4" />
<flux:icon.plus class="w-6 h-6" />
Add Service
</flux:button>
</div>
@ -151,7 +151,7 @@
</flux:field>
<div class="flex items-end">
<flux:button wire:click="removeServiceItem({{ $index }})" variant="danger" size="sm">
<flux:icon.trash class="w-4 h-4" />
<flux:icon.trash class="w-6 h-6" />
</flux:button>
</div>
</div>
@ -171,7 +171,7 @@
<div class="flex items-center justify-between mb-4">
<flux:heading size="lg">Parts</flux:heading>
<flux:button wire:click="addPart" variant="ghost" size="sm">
<flux:icon.plus class="w-4 h-4" />
<flux:icon.plus class="w-6 h-6" />
Add Part
</flux:button>
</div>
@ -208,7 +208,7 @@
</flux:field>
<div class="flex items-end">
<flux:button wire:click="removePart({{ $index }})" variant="danger" size="sm">
<flux:icon.trash class="w-4 h-4" />
<flux:icon.trash class="w-6 h-6" />
</flux:button>
</div>
</div>

View File

@ -54,11 +54,18 @@
<!-- Filters -->
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
<div class="grid grid-cols-1 md:grid-cols-6 gap-4 p-4">
<flux:input
wire:model.live="search"
placeholder="Search orders..."
icon="magnifying-glass"
/>
<div class="relative">
<flux:input
wire:model.live="search"
placeholder="Search orders..."
icon="magnifying-glass"
:loading="false"
/>
<!-- Custom loading indicator with wrench icon -->
<div wire:loading wire:target="search" class="absolute right-3 top-1/2 transform -translate-y-1/2">
<flux:icon.wrench class="w-6 h-6 text-zinc-400 animate-spin" />
</div>
</div>
<select wire:model.live="status" class="rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-2 focus:ring-blue-500">
<option value="">All Statuses</option>

View File

@ -12,8 +12,12 @@
<!-- Filters and Search -->
<div class="flex flex-col lg:flex-row gap-4">
<div class="flex-1">
<flux:input wire:model.live="search" placeholder="Search technicians..." icon="magnifying-glass" />
<div class="flex-1 relative">
<flux:input wire:model.live="search" placeholder="Search technicians..." icon="magnifying-glass" :loading="false" />
<!-- Custom loading indicator with user icon -->
<div wire:loading wire:target="search" class="absolute right-3 top-1/2 transform -translate-y-1/2">
<flux:icon.user class="w-6 h-6 text-zinc-400 animate-bounce" />
</div>
</div>
<div class="flex gap-2">
<flux:select wire:model.live="statusFilter" placeholder="All Statuses">

View File

@ -8,7 +8,7 @@
<a href="{{ route('users.index') }}"
class="inline-flex items-center px-4 py-2 text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-700 text-sm font-medium rounded-md hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
wire:navigate>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Back to Users

View File

@ -8,14 +8,14 @@
<div class="flex items-center space-x-3">
<button wire:click="toggleShowDetails"
class="inline-flex items-center px-3 py-2 text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-700 text-sm font-medium rounded-md hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
{{ $showDetails ? 'Hide Details' : 'Show Details' }}
</button>
<button wire:click="exportUsers"
class="inline-flex items-center px-4 py-2 text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-700 text-sm font-medium rounded-md hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
Export
@ -23,7 +23,7 @@
<a href="{{ route('users.create') }}"
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors"
wire:navigate>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Add User
@ -217,21 +217,21 @@
<div class="flex flex-wrap items-center gap-2">
<button wire:click="bulkActivate"
class="inline-flex items-center px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-md transition-colors">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Activate
</button>
<button wire:click="bulkDeactivate"
class="inline-flex items-center px-3 py-1 bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium rounded-md transition-colors">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18.364 5.636M5.636 18.364l12.728-12.728"></path>
</svg>
Deactivate
</button>
<button wire:click="bulkSuspend"
class="inline-flex items-center px-3 py-1 bg-orange-600 hover:bg-orange-700 text-white text-sm font-medium rounded-md transition-colors">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
Suspend
@ -241,11 +241,11 @@
<div class="relative" x-data="{ open: false }">
<button @click="open = !open"
class="inline-flex items-center px-3 py-1 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium rounded-md transition-colors">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"></path>
</svg>
Assign Role
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
@ -263,7 +263,7 @@
<div class="border-l border-blue-300 dark:border-blue-600 pl-3 ml-1">
<button wire:click="confirmBulkDelete"
class="inline-flex items-center px-3 py-1 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-md transition-colors">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
Delete
@ -315,7 +315,7 @@
<div class="flex items-center space-x-1">
<span>User</span>
@if($sortField === 'name')
<svg class="w-4 h-4 {{ $sortDirection === 'asc' ? 'rotate-180' : '' }}" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 {{ $sortDirection === 'asc' ? 'rotate-180' : '' }}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
@endif
@ -326,7 +326,7 @@
<div class="flex items-center space-x-1">
<span>Contact</span>
@if($sortField === 'email')
<svg class="w-4 h-4 {{ $sortDirection === 'asc' ? 'rotate-180' : '' }}" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 {{ $sortDirection === 'asc' ? 'rotate-180' : '' }}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
@endif
@ -338,7 +338,7 @@
<div class="flex items-center space-x-1">
<span>Status</span>
@if($sortField === 'status')
<svg class="w-4 h-4 {{ $sortDirection === 'asc' ? 'rotate-180' : '' }}" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 {{ $sortDirection === 'asc' ? 'rotate-180' : '' }}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
@endif
@ -349,7 +349,7 @@
<div class="flex items-center space-x-1">
<span>Created</span>
@if($sortField === 'created_at')
<svg class="w-4 h-4 {{ $sortDirection === 'asc' ? 'rotate-180' : '' }}" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 {{ $sortDirection === 'asc' ? 'rotate-180' : '' }}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
@endif
@ -433,7 +433,7 @@
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200"
wire:navigate
title="View">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
@ -442,7 +442,7 @@
class="text-zinc-600 dark:text-zinc-400 hover:text-zinc-800 dark:hover:text-zinc-200"
wire:navigate
title="Edit">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</a>
@ -452,7 +452,7 @@
<button wire:click="deactivateUser({{ $user->id }})"
class="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
title="Deactivate">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18.364 5.636M5.636 18.364l12.728-12.728"></path>
</svg>
</button>
@ -460,7 +460,7 @@
<button wire:click="activateUser({{ $user->id }})"
class="text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-200"
title="Activate">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</button>
@ -470,7 +470,7 @@
<button wire:click="suspendUser({{ $user->id }})"
class="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200"
title="Suspend">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636"></path>
</svg>
</button>
@ -499,7 +499,7 @@
<a href="{{ route('users.create') }}"
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors"
wire:navigate>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Add your first user

View File

@ -92,7 +92,7 @@
<h2 class="text-lg font-medium text-zinc-900 dark:text-white">User Management</h2>
<div class="flex items-center space-x-1">
<button class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-700 rounded-md transition-colors">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
User Details
@ -104,7 +104,7 @@
<a href="{{ route('users.index') }}"
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-700 rounded-md transition-colors"
wire:navigate>
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"></path>
</svg>
All Users
@ -113,7 +113,7 @@
<a href="{{ route('users.show', $user) }}"
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 rounded-md border border-blue-200 dark:border-blue-800"
wire:navigate>
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
User Details
@ -122,7 +122,7 @@
<a href="{{ route('users.manage-roles', $user) }}"
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-700 rounded-md transition-colors"
wire:navigate>
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
</svg>
Roles & Permissions
@ -132,7 +132,7 @@
<a href="{{ route('customers.show', $user->customer) }}"
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 hover:bg-purple-50 dark:hover:bg-purple-900/20 rounded-md transition-colors"
wire:navigate>
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
</svg>
Customer Profile
@ -142,7 +142,7 @@
<a href="{{ route('users.edit', $user) }}"
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-700 rounded-md transition-colors"
wire:navigate>
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
Edit User
@ -152,7 +152,7 @@
<button onclick="showUserActionsMenu()"
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-zinc-700 rounded-md transition-colors">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"></path>
</svg>
Actions
@ -377,7 +377,7 @@
@foreach($permissions as $permission)
<div class="flex items-center justify-between">
<span class="text-xs text-zinc-600 dark:text-zinc-300">{{ str_replace($module.'.', '', $permission->name) }}</span>
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>

View File

@ -34,11 +34,18 @@
<!-- Filters -->
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 p-4">
<flux:input
wire:model.live="search"
placeholder="Search vehicles..."
icon="magnifying-glass"
/>
<div class="relative">
<flux:input
wire:model.live="search"
placeholder="Search vehicles..."
icon="magnifying-glass"
:loading="false"
/>
<!-- Custom loading indicator with truck icon -->
<div wire:loading wire:target="search" class="absolute right-3 top-1/2 transform -translate-y-1/2">
<flux:icon.truck class="w-6 h-6 text-zinc-400 animate-bounce" />
</div>
</div>
<select wire:model.live="customer_id" class="rounded-md border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-2 focus:ring-blue-500">
<option value="">All Customers</option>
@ -116,7 +123,7 @@
<div class="font-medium">{{ $vehicle->display_name }}</div>
<div class="flex items-center space-x-2 text-sm text-zinc-500 dark:text-zinc-400">
<div
class="w-4 h-4 rounded border border-zinc-300 dark:border-zinc-600"
class="w-6 h-6 rounded border border-zinc-300 dark:border-zinc-600"
style="background-color: {{ $vehicle->color }}"
title="{{ $vehicle->color }}"
></div>

View File

@ -11,9 +11,13 @@
<!-- Filters -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<div class="relative">
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Search</label>
<input type="text" wire:model.live="search" placeholder="Search work orders, job numbers, or customers..." class="w-full px-3 py-2 border border-zinc-300 dark:border-zinc-600 rounded-lg bg-white dark:bg-zinc-700 text-zinc-900 dark:text-zinc-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<input type="text" wire:model.live="search" placeholder="Search work orders, job numbers, or customers..." class="w-full px-3 py-2 border border-zinc-300 dark:border-zinc-600 rounded-lg bg-white dark:bg-zinc-700 text-zinc-900 dark:text-zinc-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 pr-10">
<!-- Custom loading indicator with hammer icon -->
<div wire:loading wire:target="search" class="absolute right-3 top-10 transform -translate-y-1/2">
<flux:icon.hammer class="w-6 h-6 text-zinc-400 animate-pulse" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Status</label>

View File

@ -226,6 +226,15 @@ Route::middleware(['auth', 'admin.only'])->group(function () {
Route::get('/{user}/manage-roles', \App\Livewire\Users\ManageRolesPermissions::class)->middleware('permission:users.manage-roles')->name('manage-roles');
});
// Invoice Management Routes
Route::prefix('invoices')->name('invoices.')->middleware('permission:invoices.view')->group(function () {
Route::get('/', \App\Livewire\Invoices\Index::class)->name('index');
Route::get('/create', \App\Livewire\Invoices\Create::class)->middleware('permission:invoices.create')->name('create');
Route::get('/create-from-estimate/{estimate}', \App\Livewire\Invoices\CreateFromEstimate::class)->middleware('permission:invoices.create')->name('create-from-estimate');
Route::get('/{invoice}', \App\Livewire\Invoices\Show::class)->name('show');
Route::get('/{invoice}/edit', \App\Livewire\Invoices\Edit::class)->middleware('permission:invoices.update')->name('edit');
});
// Legacy User Management Route (redirect to new structure)
Route::get('/user-management', function () {
return redirect()->route('users.index');

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Estimates;
use App\Livewire\Estimates\Show;
use App\Models\Estimate;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
class EstimatePdfTest extends TestCase
{
use RefreshDatabase;
public function test_estimate_show_component_can_be_mounted(): void
{
// Create test user with admin permissions
$user = User::factory()->create();
$user->assignRole('admin');
// Create a simple estimate
$estimate = Estimate::factory()->create([
'estimate_number' => 'TEST-001',
'status' => 'draft',
'total_amount' => 100.00,
]);
// Test the Show component can be mounted
$component = Livewire::actingAs($user)
->test(Show::class, ['estimate' => $estimate])
->assertOk();
// Verify estimate is loaded correctly
$this->assertEquals($estimate->id, $component->estimate->id);
$this->assertEquals($estimate->estimate_number, $component->estimate->estimate_number);
}
public function test_pdf_template_renders_without_errors(): void
{
$estimate = Estimate::factory()->create([
'estimate_number' => 'TEST-PDF-001',
'status' => 'draft',
'total_amount' => 150.00,
]);
// Test that the PDF view can be rendered
$view = view('estimates.pdf', compact('estimate'));
$this->assertNotNull($view);
$html = $view->render();
$this->assertStringContainsString('ESTIMATE', $html);
$this->assertStringContainsString($estimate->estimate_number, $html);
}
}