Enhance UI and functionality across various components
- Increased icon sizes in service items, service orders, users, and technician management for better visibility. - Added custom loading indicators with appropriate icons in search fields for vehicles, work orders, and technicians. - Introduced invoice management routes for better organization and access control. - Created a new test for the estimate PDF functionality to ensure proper rendering and data integrity.
This commit is contained in:
23
.github/copilot-instructions.md
vendored
23
.github/copilot-instructions.md
vendored
@ -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);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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()
|
||||
{
|
||||
|
||||
@ -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()
|
||||
{
|
||||
|
||||
222
app/Livewire/Invoices/Create.php
Normal file
222
app/Livewire/Invoices/Create.php
Normal 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');
|
||||
}
|
||||
}
|
||||
196
app/Livewire/Invoices/CreateFromEstimate.php
Normal file
196
app/Livewire/Invoices/CreateFromEstimate.php
Normal 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');
|
||||
}
|
||||
}
|
||||
275
app/Livewire/Invoices/Edit.php
Normal file
275
app/Livewire/Invoices/Edit.php
Normal 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');
|
||||
}
|
||||
}
|
||||
178
app/Livewire/Invoices/Index.php
Normal file
178
app/Livewire/Invoices/Index.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
171
app/Livewire/Invoices/Show.php
Normal file
171
app/Livewire/Invoices/Show.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
268
app/Models/Invoice.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
96
app/Models/InvoiceLineItem.php
Normal file
96
app/Models/InvoiceLineItem.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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
367
composer.lock
generated
@ -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
301
config/dompdf.php
Normal 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,
|
||||
],
|
||||
|
||||
];
|
||||
@ -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");
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
111
database/seeders/InvoiceSeeder.php
Normal file
111
database/seeders/InvoiceSeeder.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
419
resources/views/estimates/pdf.blade.php
Normal file
419
resources/views/estimates/pdf.blade.php
Normal 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>
|
||||
@ -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>
|
||||
|
||||
512
resources/views/invoices/pdf.blade.php
Normal file
512
resources/views/invoices/pdf.blade.php
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
265
resources/views/livewire/estimates/edit-improved.blade.php
Normal file
265
resources/views/livewire/estimates/edit-improved.blade.php
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
<div>
|
||||
{{-- Nothing in the world is as soft and yielding as water. --}}
|
||||
</div>
|
||||
@ -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
|
||||
|
||||
317
resources/views/livewire/estimates/show-improved.blade.php
Normal file
317
resources/views/livewire/estimates/show-improved.blade.php
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
214
resources/views/livewire/invoices/create-from-estimate.blade.php
Normal file
214
resources/views/livewire/invoices/create-from-estimate.blade.php
Normal 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>
|
||||
224
resources/views/livewire/invoices/create.blade.php
Normal file
224
resources/views/livewire/invoices/create.blade.php
Normal 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>
|
||||
230
resources/views/livewire/invoices/edit.blade.php
Normal file
230
resources/views/livewire/invoices/edit.blade.php
Normal 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>
|
||||
299
resources/views/livewire/invoices/index.blade.php
Normal file
299
resources/views/livewire/invoices/index.blade.php
Normal 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>
|
||||
380
resources/views/livewire/invoices/show.blade.php
Normal file
380
resources/views/livewire/invoices/show.blade.php
Normal 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>
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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' }}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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');
|
||||
|
||||
58
tests/Feature/Estimates/EstimatePdfTest.php
Normal file
58
tests/Feature/Estimates/EstimatePdfTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user