sackey 5403c3591d
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
feat: Enhance job card workflow with diagnosis actions and technician assignment modal
- Added buttons for assigning diagnosis and starting diagnosis based on job card status in the job card view.
- Implemented a modal for assigning technicians for diagnosis, including form validation and technician selection.
- Updated routes to include a test route for job cards.
- Created a new Blade view for testing inspection inputs.
- Developed comprehensive feature tests for the estimate module, including creation, viewing, editing, and validation of estimates.
- Added tests for estimate model relationships and statistics calculations.
- Introduced a basic feature test for job cards index.
2025-08-15 08:37:45 +00:00

411 lines
29 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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="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">
{{ $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 }}
@endif
</p>
@if($lastSaved)
<p class="text-sm text-green-600 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">
<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">
@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
</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>
</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="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"
>
<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>
</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">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900">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">
<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>
@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>
@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">
<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>
</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>
@endif
</div>
</div>
</div>
<!-- Settings & Summary Sidebar (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>
<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>
</div>
<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>
</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="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>
</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>
<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>
<!-- 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>
</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>
<flux:field>
<flux:textarea wire:model="terms_and_conditions" rows="6" placeholder="Enter 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>