feat: Enhance job card workflow with diagnosis actions and technician assignment modal
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run

- 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.
This commit is contained in:
2025-08-15 08:37:45 +00:00
parent fcff9900a2
commit 5403c3591d
57 changed files with 9082 additions and 1747 deletions

View File

@ -80,3 +80,449 @@ The system follows a structured automotive repair workflow:
- Follow existing permission key patterns (`domain.action`).
If anything above is unclear or you need deeper details (e.g., settings schema, specific Livewire page conventions), propose a short diff or ask for a quick pointer to the relevant file.
===
<laravel-boost-guidelines>
=== foundation rules ===
# Laravel Boost Guidelines
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.4.1
- laravel/framework (LARAVEL) - v12
- laravel/prompts (PROMPTS) - v0
- livewire/flux (FLUXUI_FREE) - v2
- livewire/livewire (LIVEWIRE) - v3
- livewire/volt (VOLT) - v1
- laravel/pint (PINT) - v1
- tailwindcss (TAILWINDCSS) - v4
## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
## Application Structure & Architecture
- Stick to existing directory structure - don't create new base folders without approval.
- Do not change the application's dependencies without approval.
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
=== boost rules ===
## Laravel Boost
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
## Artisan
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters.
## URLs
- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port.
## Tinker / Debugging
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
- Use the `database-query` tool when you only need to read from the database.
## Reading Browser Logs With the `browser-logs` Tool
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
- Only recent browser logs will be useful - ignore old logs.
## Searching Documentation (Critically Important)
- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches.
- Search the documentation before making code changes to ensure we are taking the correct approach.
- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
- Do not add package names to queries, package information is already shared. Use `test resource table`, not `filament 4 test resource table`.
### Available Search Syntax
- You can and should pass multiple queries at once. The most relevant results will be returned first.
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit"
3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit"
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms
=== laravel/core rules ===
## Do Things the Laravel Way
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
- If you're creating a generic PHP class, use `artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
### Database
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
- Use Eloquent models and relationships before suggesting raw database queries
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
- Generate code that prevents N+1 query problems by using eager loading.
- Use Laravel's query builder for very complex database operations.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.
### APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
### Controllers & Validation
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
- Check sibling Form Requests to see if the application uses array or string based validation rules.
### Queues
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
### Authentication & Authorization
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
### URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.
### Configuration
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
### Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] <name>` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
### Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
=== laravel/v12 rules ===
## Laravel 12
- Use the `search-docs` tool to get version specific documentation.
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
### Laravel 12 Structure
- No middleware files in `app/Http/Middleware/`.
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
- `bootstrap/providers.php` contains application specific service providers.
- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration.
- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration.
### Database
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
### Models
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
=== fluxui-free/core rules ===
## Flux UI Free
- This project is using the free edition of Flux UI. It has full access to the free components and variants, but does not have access to the Pro components.
- Flux UI is a component library for Livewire. Flux is a robust, hand-crafted, UI component library for your Livewire applications. It's built using Tailwind CSS and provides a set of components that are easy to use and customize.
- You should use Flux UI components when available.
- Fallback to standard Blade components if Flux is unavailable.
- If available, use Laravel Boost's `search-docs` tool to get the exact documentation and code snippets available for this project.
- Flux UI components look like this:
<code-snippet name="Flux UI Component Usage Example" lang="blade">
<flux:button variant="primary"/>
</code-snippet>
### Available Components
This is correct as of Boost installation, but there may be additional components within the codebase.
<available-flux-components>
avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip
</available-flux-components>
=== livewire/core rules ===
## Livewire Core
- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests.
- Use the `php artisan make:livewire [Posts\\CreatePost]` artisan command to create new components
- State should live on the server, with the UI reflecting it.
- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions.
## Livewire Best Practices
- Livewire components require a single root element.
- Use `wire:loading` and `wire:dirty` for delightful loading states.
- Add `wire:key` in loops:
```blade
@foreach ($items as $item)
<div wire:key="item-{{ $item->id }}">
{{ $item->name }}
</div>
@endforeach
```
- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects:
<code-snippet name="Lifecycle hook examples" lang="php">
public function mount(User $user) { $this->user = $user; }
public function updatedSearch() { $this->resetPage(); }
</code-snippet>
## Testing Livewire
<code-snippet name="Example Livewire component test" lang="php">
Livewire::test(Counter::class)
->assertSet('count', 0)
->call('increment')
->assertSet('count', 1)
->assertSee(1)
->assertStatus(200);
</code-snippet>
<code-snippet name="Testing a Livewire component exists within a page" lang="php">
$this->get('/posts/create')
->assertSeeLivewire(CreatePost::class);
</code-snippet>
=== livewire/v3 rules ===
## Livewire 3
### Key Changes From Livewire 2
- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions.
- Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default.
- Components now use the `App\Livewire` namespace (not `App\Http\Livewire`).
- Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`).
- Use the `components.layouts.app` view as the typical layout path (not `layouts.app`).
### New Directives
- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples.
### Alpine
- Alpine is now included with Livewire, don't manually include Alpine.js.
- Plugins included with Alpine: persist, intersect, collapse, and focus.
### Lifecycle Hooks
- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring:
<code-snippet name="livewire:load example" lang="js">
document.addEventListener('livewire:init', function () {
Livewire.hook('request', ({ fail }) => {
if (fail && fail.status === 419) {
alert('Your session expired');
}
});
Livewire.hook('message.failed', (message, component) => {
console.error(message);
});
});
</code-snippet>
=== volt/core rules ===
## Livewire Volt
- This project uses Livewire Volt for interactivity within its pages. New pages requiring interactivity must also use Livewire Volt. There is documentation available for it.
- Make new Volt components using `php artisan make:volt [name] [--test] [--pest]`
- Volt is a **class-based** and **functional** API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to co-exist in the same file
- Livewire Volt allows PHP logic and Blade templates in one file. Components use the `@livewire("volt-anonymous-fragment-eyJuYW1lIjoidm9sdC1hbm9ueW1vdXMtZnJhZ21lbnQtYmQ5YWJiNTE3YWMyMTgwOTA1ZmUxMzAxODk0MGJiZmIiLCJwYXRoIjoic3RvcmFnZVwvZnJhbWV3b3JrXC92aWV3c1wvMTUxYWRjZWRjMzBhMzllOWIxNzQ0ZDRiMWRjY2FjYWIuYmxhZGUucGhwIn0=", Livewire\Volt\Precompilers\ExtractFragments::componentArguments([...get_defined_vars(), ...array (
)]))
</code-snippet>
### Volt Class Based Component Example
To get started, define an anonymous class that extends Livewire\Volt\Component. Within the class, you may utilize all of the features of Livewire using traditional Livewire syntax:
<code-snippet name="Volt Class-based Volt Component Example" lang="php">
use Livewire\Volt\Component;
new class extends Component {
public $count = 0;
public function increment()
{
$this->count++;
}
} ?>
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+</button>
</div>
</code-snippet>
### Testing Volt & Volt Components
- Use the existing directory for tests if it already exists. Otherwise, fallback to `tests/Feature/Volt`.
<code-snippet name="Livewire Test Example" lang="php">
use Livewire\Volt\Volt;
test('counter increments', function () {
Volt::test('counter')
->assertSee('Count: 0')
->call('increment')
->assertSee('Count: 1');
});
</code-snippet>
<code-snippet name="Volt Component Test Using Pest" lang="php">
declare(strict_types=1);
use App\Models\{User, Product};
use Livewire\Volt\Volt;
test('product form creates product', function () {
$user = User::factory()->create();
Volt::test('pages.products.create')
->actingAs($user)
->set('form.name', 'Test Product')
->set('form.description', 'Test Description')
->set('form.price', 99.99)
->call('create')
->assertHasNoErrors();
expect(Product::where('name', 'Test Product')->exists())->toBeTrue();
});
</code-snippet>
### Common Patterns
<code-snippet name="CRUD With Volt" lang="php">
<?php
use App\Models\Product;
use function Livewire\Volt\{state, computed};
state(['editing' => null, 'search' => '']);
$products = computed(fn() => Product::when($this->search,
fn($q) => $q->where('name', 'like', "%{$this->search}%")
)->get());
$edit = fn(Product $product) => $this->editing = $product->id;
$delete = fn(Product $product) => $product->delete();
?>
<!-- HTML / UI Here -->
</code-snippet>
<code-snippet name="Real-Time Search With Volt" lang="php">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="Search..."
/>
</code-snippet>
<code-snippet name="Loading States With Volt" lang="php">
<flux:button wire:click="save" wire:loading.attr="disabled">
<span wire:loading.remove>Save</span>
<span wire:loading>Saving...</span>
</flux:button>
</code-snippet>
=== pint/core rules ===
## Laravel Pint Code Formatter
- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues.
=== tailwindcss/core rules ===
## Tailwind Core
- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own.
- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..)
- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically
- You can use the `search-docs` tool to get exact examples from the official documentation when needed.
### Spacing
- When listing items, use gap utilities for spacing, don't use margins.
<code-snippet name="Valid Flex Gap Spacing Example" lang="html">
<div class="flex gap-8">
<div>Superior</div>
<div>Michigan</div>
<div>Erie</div>
</div>
</code-snippet>
### Dark Mode
- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`.
=== tailwindcss/v4 rules ===
## Tailwind 4
- Always use Tailwind CSS v4 - do not use the deprecated utilities.
- `corePlugins` is not supported in Tailwind v4.
- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3:
<code-snippet name="Tailwind v4 Import Tailwind Diff" lang="diff"
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
</code-snippet>
### Replaced Utilities
- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement.
- Opacity values are still numeric.
| Deprecated | Replacement |
|------------+--------------|
| bg-opacity-* | bg-black/* |
| text-opacity-* | text-black/* |
| border-opacity-* | border-black/* |
| divide-opacity-* | divide-black/* |
| ring-opacity-* | ring-black/* |
| placeholder-opacity-* | placeholder-black/* |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |
=== tests rules ===
## Test Enforcement
- 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>

View File

@ -2,13 +2,13 @@
namespace App\Livewire\CustomerPortal;
use Livewire\Component;
use App\Models\JobCard;
use App\Services\WorkflowService;
use Livewire\Component;
class WorkflowProgress extends Component
{
public JobCard $jobCard;
public array $progressSteps = [];
public function mount(JobCard $jobCard)
@ -22,7 +22,7 @@ class WorkflowProgress extends Component
'diagnosis.serviceCoordinator',
'estimates.preparedBy',
'workOrders',
'timesheets'
'timesheets',
]);
$this->loadProgressSteps();
@ -31,7 +31,7 @@ class WorkflowProgress extends Component
public function loadProgressSteps()
{
$currentStatus = $this->jobCard->status;
$this->progressSteps = [
[
'step' => 1,
@ -42,10 +42,10 @@ class WorkflowProgress extends Component
'icon' => 'truck',
'details' => [
'Arrival Time' => $this->jobCard->arrival_datetime?->format('M j, Y g:i A'),
'Mileage' => number_format($this->jobCard->mileage_in) . ' miles',
'Fuel Level' => $this->jobCard->fuel_level_in . '%',
'Mileage' => number_format($this->jobCard->mileage_in).' miles',
'Fuel Level' => $this->jobCard->fuel_level_in.'%',
'Service Advisor' => $this->jobCard->serviceAdvisor?->name,
]
],
],
[
'step' => 2,
@ -58,7 +58,7 @@ class WorkflowProgress extends Component
'Inspector' => $this->jobCard->incomingInspection?->inspector?->name,
'Overall Condition' => $this->jobCard->incomingInspection?->overall_condition,
'Inspection Date' => $this->jobCard->incomingInspection?->inspection_date?->format('M j, Y g:i A'),
]
],
],
[
'step' => 3,
@ -70,7 +70,7 @@ class WorkflowProgress extends Component
'details' => [
'Service Coordinator' => $this->jobCard->diagnosis?->serviceCoordinator?->name,
'Assignment Date' => $this->jobCard->diagnosis?->created_at?->format('M j, Y g:i A'),
]
],
],
[
'step' => 4,
@ -83,7 +83,7 @@ class WorkflowProgress extends Component
'Diagnosis Status' => ucfirst(str_replace('_', ' ', $this->jobCard->diagnosis?->diagnosis_status ?? '')),
'Started' => $this->jobCard->diagnosis?->started_at?->format('M j, Y g:i A'),
'Completed' => $this->jobCard->diagnosis?->completed_at?->format('M j, Y g:i A'),
]
],
],
[
'step' => 5,
@ -93,9 +93,9 @@ class WorkflowProgress extends Component
'completed_at' => $this->jobCard->estimates->where('status', 'sent')->first()?->created_at,
'icon' => 'document-text',
'details' => [
'Estimate Total' => '$' . number_format($this->jobCard->estimates->where('status', 'sent')->first()?->total_amount ?? 0, 2),
'Valid Until' => $this->jobCard->estimates->where('status', 'sent')->first()?->valid_until?->format('M j, Y'),
]
'Estimate Total' => '$'.number_format($this->jobCard->estimates->where('status', 'sent')->first()?->total_amount ?? 0, 2),
'Valid Until' => $this->jobCard->estimates->where('status', 'sent')->first()?->created_at?->addDays($this->jobCard->estimates->where('status', 'sent')->first()?->validity_period_days ?? 30)?->format('M j, Y'),
],
],
[
'step' => 6,
@ -107,7 +107,7 @@ class WorkflowProgress extends Component
'details' => [
'Approved At' => $this->jobCard->estimates->where('status', 'approved')->first()?->customer_approved_at?->format('M j, Y g:i A'),
'Approval Method' => ucfirst($this->jobCard->estimates->where('status', 'approved')->first()?->customer_approval_method ?? ''),
]
],
],
[
'step' => 7,
@ -116,7 +116,7 @@ class WorkflowProgress extends Component
'status' => $this->getStepStatus('parts_procurement', $currentStatus),
'completed_at' => null, // Would need to track this separately
'icon' => 'cog-6-tooth',
'details' => []
'details' => [],
],
[
'step' => 8,
@ -128,7 +128,7 @@ class WorkflowProgress extends Component
'details' => [
'Started' => $this->jobCard->workOrders->first()?->actual_start_time?->format('M j, Y g:i A'),
'Technician' => $this->jobCard->workOrders->first()?->assignedTechnician?->name,
]
],
],
[
'step' => 9,
@ -140,7 +140,7 @@ class WorkflowProgress extends Component
'details' => [
'Inspector' => $this->jobCard->outgoingInspection?->inspector?->name,
'Inspection Date' => $this->jobCard->outgoingInspection?->inspection_date?->format('M j, Y g:i A'),
]
],
],
[
'step' => 10,
@ -151,8 +151,8 @@ class WorkflowProgress extends Component
'icon' => 'truck',
'details' => [
'Completion Time' => $this->jobCard->completion_datetime?->format('M j, Y g:i A'),
'Final Mileage' => number_format($this->jobCard->mileage_out) . ' miles',
]
'Final Mileage' => number_format($this->jobCard->mileage_out).' miles',
],
],
];
}

View File

@ -2,45 +2,63 @@
namespace App\Livewire\Diagnosis;
use App\Models\JobCard;
use App\Models\Diagnosis;
use App\Models\Timesheet;
use App\Models\Part;
use App\Models\ServiceItem;
use App\Models\Estimate;
use App\Models\EstimateLineItem;
use App\Models\User;
use App\Models\JobCard;
use App\Models\Part;
use App\Models\ServiceItem;
use App\Models\Timesheet;
use Livewire\Component;
use Livewire\WithFileUploads;
use Carbon\Carbon;
class Create extends Component
{
use WithFileUploads;
public JobCard $jobCard;
public $customer_reported_issues = '';
public $diagnostic_findings = '';
public $root_cause_analysis = '';
public $recommended_repairs = '';
public $additional_issues_found = '';
public $priority_level = 'medium';
public $estimated_repair_time = '';
public $parts_required = [];
public $labor_operations = [];
public $special_tools_required = [];
public $safety_concerns = '';
public $diagnostic_codes = [];
public $test_results = [];
public $photos = [];
public $notes = '';
public $environmental_impact = '';
public $customer_authorization_required = false;
// Timesheet tracking
public $currentTimesheet = null;
public $timesheets = [];
public $selectedDiagnosisType = 'general_inspection';
public $diagnosisTypes = [
'general_inspection' => 'General Inspection',
'electrical_diagnosis' => 'Electrical Diagnosis',
@ -56,25 +74,39 @@ class Create extends Component
// Parts integration
public $availableParts = [];
public $partSearch = '';
public $partSearchTerm = '';
public $partCategoryFilter = '';
public $filteredParts;
// Service items integration
public $availableServiceItems = [];
public $serviceItemSearch = '';
public $serviceSearchTerm = '';
public $serviceCategoryFilter = '';
public $filteredServiceItems;
// UI state
public $showPartsSection = false;
public $showLaborSection = false;
public $showDiagnosticCodesSection = false;
public $showTestResultsSection = false;
public $showAdvancedOptions = false;
public $showTimesheetSection = true;
public $createEstimateAutomatically = true;
protected $rules = [
@ -102,9 +134,22 @@ class Create extends Component
public function mount(JobCard $jobCard)
{
$this->jobCard = $jobCard->load(['customer', 'vehicle', 'timesheets']);
// Validate that initial inspection has been completed
if ($jobCard->status === 'received') {
session()->flash('error', 'Initial vehicle inspection must be completed before starting diagnosis. Please complete the inspection first.');
return redirect()->route('job-cards.show', $jobCard);
}
if (! $jobCard->incomingInspection) {
session()->flash('error', 'No incoming inspection record found. Please complete the initial inspection before proceeding to diagnosis.');
return redirect()->route('inspections.create', ['jobCard' => $jobCard, 'type' => 'incoming']);
}
$this->jobCard = $jobCard->load(['customer', 'vehicle', 'timesheets', 'incomingInspection']);
$this->customer_reported_issues = $jobCard->customer_reported_issues ?? '';
// Initialize arrays to prevent null issues - load from session if available
$this->parts_required = session()->get("diagnosis_parts_{$jobCard->id}", []);
$this->labor_operations = session()->get("diagnosis_labor_{$jobCard->id}", []);
@ -112,18 +157,18 @@ class Create extends Component
$this->diagnostic_codes = session()->get("diagnosis_codes_{$jobCard->id}", []);
$this->test_results = [];
$this->special_tools_required = [];
// Initialize filtered collections
$this->filteredParts = collect();
$this->filteredServiceItems = collect();
// Load existing timesheets for this job card related to diagnosis
$this->loadTimesheets();
// Load available parts and service items
$this->loadAvailableParts();
$this->loadAvailableServiceItems();
// Initialize with one empty part and labor operation for convenience if none exist
if (empty($this->parts_required)) {
$this->addPart();
@ -165,30 +210,30 @@ class Create extends Component
public function loadAvailableParts()
{
$query = Part::where('status', 'active');
if (!empty($this->partSearch)) {
if (! empty($this->partSearch)) {
$query->where(function ($q) {
$q->where('name', 'like', '%' . $this->partSearch . '%')
->orWhere('part_number', 'like', '%' . $this->partSearch . '%')
->orWhere('description', 'like', '%' . $this->partSearch . '%');
$q->where('name', 'like', '%'.$this->partSearch.'%')
->orWhere('part_number', 'like', '%'.$this->partSearch.'%')
->orWhere('description', 'like', '%'.$this->partSearch.'%');
});
}
$this->availableParts = $query->limit(20)->get()->toArray();
}
public function loadAvailableServiceItems()
{
$query = ServiceItem::query();
if (!empty($this->serviceItemSearch)) {
if (! empty($this->serviceItemSearch)) {
$query->where(function ($q) {
$q->where('service_name', 'like', '%' . $this->serviceItemSearch . '%')
->orWhere('description', 'like', '%' . $this->serviceItemSearch . '%')
->orWhere('category', 'like', '%' . $this->serviceItemSearch . '%');
$q->where('service_name', 'like', '%'.$this->serviceItemSearch.'%')
->orWhere('description', 'like', '%'.$this->serviceItemSearch.'%')
->orWhere('category', 'like', '%'.$this->serviceItemSearch.'%');
});
}
$this->availableServiceItems = $query->limit(20)->get()->toArray();
}
@ -197,62 +242,63 @@ class Create extends Component
{
$this->updateFilteredParts();
}
public function updatedPartCategoryFilter()
{
$this->updateFilteredParts();
}
public function updateFilteredParts()
{
try {
// If no search criteria provided, return empty collection
if (empty($this->partSearchTerm) && empty($this->partCategoryFilter)) {
$this->filteredParts = collect();
return;
}
// Start with active parts only
$query = Part::where('status', 'active');
// Add search term filter if provided
if (!empty($this->partSearchTerm)) {
if (! empty($this->partSearchTerm)) {
$searchTerm = trim($this->partSearchTerm);
$query->where(function ($q) use ($searchTerm) {
$q->where('name', 'like', '%' . $searchTerm . '%')
->orWhere('part_number', 'like', '%' . $searchTerm . '%')
->orWhere('description', 'like', '%' . $searchTerm . '%')
->orWhere('manufacturer', 'like', '%' . $searchTerm . '%');
$q->where('name', 'like', '%'.$searchTerm.'%')
->orWhere('part_number', 'like', '%'.$searchTerm.'%')
->orWhere('description', 'like', '%'.$searchTerm.'%')
->orWhere('manufacturer', 'like', '%'.$searchTerm.'%');
});
}
// Add category filter if provided
if (!empty($this->partCategoryFilter)) {
if (! empty($this->partCategoryFilter)) {
$query->where('category', $this->partCategoryFilter);
}
// Order by name for consistent results
$query->orderBy('name');
// Get results and assign to property
$this->filteredParts = $query->limit(20)->get();
// Log for debugging
\Log::info('Parts search executed', [
'search_term' => $this->partSearchTerm,
'category_filter' => $this->partCategoryFilter,
'results_count' => $this->filteredParts->count(),
'results' => $this->filteredParts->pluck('name')->toArray()
'results' => $this->filteredParts->pluck('name')->toArray(),
]);
} catch (\Exception $e) {
// Log error and return empty collection
\Log::error('Error in updateFilteredParts', [
'error' => $e->getMessage(),
'search_term' => $this->partSearchTerm,
'category_filter' => $this->partCategoryFilter
'category_filter' => $this->partCategoryFilter,
]);
$this->filteredParts = collect();
}
}
@ -260,18 +306,18 @@ class Create extends Component
public function getFilteredServiceItemsProperty()
{
$query = ServiceItem::query();
if (!empty($this->serviceSearchTerm)) {
if (! empty($this->serviceSearchTerm)) {
$query->where(function ($q) {
$q->where('name', 'like', '%' . $this->serviceSearchTerm . '%')
->orWhere('description', 'like', '%' . $this->serviceSearchTerm . '%');
$q->where('name', 'like', '%'.$this->serviceSearchTerm.'%')
->orWhere('description', 'like', '%'.$this->serviceSearchTerm.'%');
});
}
if (!empty($this->serviceCategoryFilter)) {
if (! empty($this->serviceCategoryFilter)) {
$query->where('category', $this->serviceCategoryFilter);
}
return $query->limit(20)->get();
}
@ -284,53 +330,54 @@ class Create extends Component
{
$this->updateFilteredServiceItems();
}
public function updatedServiceCategoryFilter()
{
$this->updateFilteredServiceItems();
}
public function updateFilteredServiceItems()
{
try {
// If no search criteria provided, return empty collection
if (empty($this->serviceSearchTerm) && empty($this->serviceCategoryFilter)) {
$this->filteredServiceItems = collect();
return;
}
// Start with active service items only
$query = ServiceItem::where('status', 'active');
// Add search term filter if provided
if (!empty($this->serviceSearchTerm)) {
if (! empty($this->serviceSearchTerm)) {
$searchTerm = trim($this->serviceSearchTerm);
$query->where(function ($q) use ($searchTerm) {
$q->where('service_name', 'like', '%' . $searchTerm . '%')
->orWhere('description', 'like', '%' . $searchTerm . '%');
$q->where('service_name', 'like', '%'.$searchTerm.'%')
->orWhere('description', 'like', '%'.$searchTerm.'%');
});
}
// Add category filter if provided
if (!empty($this->serviceCategoryFilter)) {
if (! empty($this->serviceCategoryFilter)) {
$query->where('category', $this->serviceCategoryFilter);
}
// Order by name for consistent results
$query->orderBy('service_name');
// Get results
$results = $query->limit(20)->get();
$this->filteredServiceItems = $results;
} catch (\Exception $e) {
// Log error and return empty collection
\Log::error('Error in updateFilteredServiceItems', [
'error' => $e->getMessage(),
'search_term' => $this->serviceSearchTerm,
'category_filter' => $this->serviceCategoryFilter
'category_filter' => $this->serviceCategoryFilter,
]);
$this->filteredServiceItems = collect();
}
}
@ -346,7 +393,7 @@ class Create extends Component
'job_card_id' => $this->jobCard->id,
'user_id' => auth()->id(),
'entry_type' => 'manual',
'description' => 'Diagnosis: ' . ($this->diagnosisTypes[$this->selectedDiagnosisType] ?? 'General Diagnosis'),
'description' => 'Diagnosis: '.($this->diagnosisTypes[$this->selectedDiagnosisType] ?? 'General Diagnosis'),
'date' => now()->toDateString(),
'start_time' => now(),
'hourly_rate' => auth()->user()->hourly_rate ?? 85.00,
@ -354,21 +401,21 @@ class Create extends Component
]);
$this->loadTimesheets();
session()->flash('timesheet_message', 'Timesheet started for ' . ($this->diagnosisTypes[$this->selectedDiagnosisType] ?? 'Diagnosis'));
session()->flash('timesheet_message', 'Timesheet started for '.($this->diagnosisTypes[$this->selectedDiagnosisType] ?? 'Diagnosis'));
}
public function endTimesheet()
{
if (!$this->currentTimesheet) {
if (! $this->currentTimesheet) {
return;
}
$timesheet = Timesheet::find($this->currentTimesheet['id']);
if ($timesheet && !$timesheet->end_time) {
if ($timesheet && ! $timesheet->end_time) {
$endTime = now();
$totalMinutes = $timesheet->start_time->diffInMinutes($endTime);
$billableHours = round($totalMinutes / 60, 2);
$timesheet->update([
'end_time' => $endTime,
'hours_worked' => $billableHours,
@ -377,7 +424,7 @@ class Create extends Component
'status' => 'submitted',
]);
session()->flash('timesheet_message', 'Timesheet ended. Total time: ' . $billableHours . ' hours');
session()->flash('timesheet_message', 'Timesheet ended. Total time: '.$billableHours.' hours');
}
$this->currentTimesheet = null;
@ -394,7 +441,7 @@ class Create extends Component
'part_number' => $part->part_number,
'quantity' => 1,
'estimated_cost' => $part->sell_price,
'availability' => $part->quantity_on_hand > 0 ? 'in_stock' : 'out_of_stock'
'availability' => $part->quantity_on_hand > 0 ? 'in_stock' : 'out_of_stock',
];
$this->updatedPartsRequired(); // Save to session
}
@ -411,7 +458,7 @@ class Create extends Component
'estimated_hours' => $serviceItem->estimated_hours,
'labor_rate' => $serviceItem->labor_rate,
'category' => $serviceItem->category,
'complexity' => 'medium'
'complexity' => 'medium',
];
$this->updatedLaborOperations(); // Save to session
}
@ -425,7 +472,7 @@ class Create extends Component
'part_number' => '',
'quantity' => 1,
'estimated_cost' => 0,
'availability' => 'in_stock'
'availability' => 'in_stock',
];
$this->updatedPartsRequired(); // Save to session
}
@ -446,7 +493,7 @@ class Create extends Component
'estimated_hours' => 0,
'labor_rate' => 85.00,
'category' => '',
'complexity' => 'medium'
'complexity' => 'medium',
];
$this->updatedLaborOperations(); // Save to session
}
@ -464,7 +511,7 @@ class Create extends Component
$this->updatedPartsRequired();
$this->updatedLaborOperations();
$this->updatedDiagnosticCodes();
session()->flash('progress_saved', 'Progress saved successfully!');
}
@ -474,7 +521,7 @@ class Create extends Component
'code' => '',
'description' => '',
'system' => '',
'severity' => 'medium'
'severity' => 'medium',
];
}
@ -490,7 +537,7 @@ class Create extends Component
'test_name' => '',
'result' => '',
'specification' => '',
'status' => 'pass'
'status' => 'pass',
];
}
@ -505,7 +552,7 @@ class Create extends Component
$this->special_tools_required[] = [
'tool_name' => '',
'tool_type' => '',
'availability' => 'available'
'availability' => 'available',
];
}
@ -517,58 +564,58 @@ class Create extends Component
public function togglePartsSection()
{
$this->showPartsSection = !$this->showPartsSection;
$this->showPartsSection = ! $this->showPartsSection;
}
public function toggleLaborSection()
{
$this->showLaborSection = !$this->showLaborSection;
$this->showLaborSection = ! $this->showLaborSection;
}
public function toggleDiagnosticCodesSection()
{
$this->showDiagnosticCodesSection = !$this->showDiagnosticCodesSection;
$this->showDiagnosticCodesSection = ! $this->showDiagnosticCodesSection;
}
public function toggleTestResultsSection()
{
$this->showTestResultsSection = !$this->showTestResultsSection;
$this->showTestResultsSection = ! $this->showTestResultsSection;
}
public function toggleAdvancedOptions()
{
$this->showAdvancedOptions = !$this->showAdvancedOptions;
$this->showAdvancedOptions = ! $this->showAdvancedOptions;
}
public function toggleTimesheetSection()
{
$this->showTimesheetSection = !$this->showTimesheetSection;
$this->showTimesheetSection = ! $this->showTimesheetSection;
}
public function calculateTotalEstimatedCost()
{
$partsCost = array_sum(array_map(function($part) {
$partsCost = array_sum(array_map(function ($part) {
return ($part['quantity'] ?? 1) * ($part['estimated_cost'] ?? 0);
}, $this->parts_required));
$laborCost = 0;
foreach ($this->labor_operations as $operation) {
$laborCost += ($operation['estimated_hours'] ?? 0) * ($operation['labor_rate'] ?? 85);
}
// Include diagnostic time costs
$diagnosticCost = collect($this->timesheets)->sum('total_amount');
return $partsCost + $laborCost + $diagnosticCost;
}
private function createEstimateFromDiagnosis($diagnosis)
{
// Calculate totals
$partsCost = array_sum(array_map(function($part) {
$partsCost = array_sum(array_map(function ($part) {
return ($part['quantity'] ?? 1) * ($part['estimated_cost'] ?? 0);
}, $this->parts_required));
$laborCost = 0;
foreach ($this->labor_operations as $operation) {
$laborCost += ($operation['estimated_hours'] ?? 0) * ($operation['labor_rate'] ?? 85);
@ -583,7 +630,7 @@ class Create extends Component
$estimate = Estimate::create([
'job_card_id' => $this->jobCard->id,
'diagnosis_id' => $diagnosis->id,
'estimate_number' => 'EST-' . str_pad(Estimate::max('id') + 1, 6, '0', STR_PAD_LEFT),
'estimate_number' => 'EST-'.str_pad(Estimate::max('id') + 1, 6, '0', STR_PAD_LEFT),
'customer_id' => $this->jobCard->customer_id,
'vehicle_id' => $this->jobCard->vehicle_id,
'prepared_by_id' => auth()->id(),
@ -594,13 +641,13 @@ class Create extends Component
'tax_rate' => $taxRate,
'tax_amount' => $taxAmount,
'total_amount' => $totalAmount,
'notes' => 'Auto-generated from diagnosis: ' . $diagnosis->id,
'valid_until' => now()->addDays(30),
'notes' => 'Auto-generated from diagnosis: '.$diagnosis->id,
'validity_period_days' => 30,
]);
// Create line items for parts
foreach ($this->parts_required as $part) {
if (!empty($part['part_name'])) {
if (! empty($part['part_name'])) {
EstimateLineItem::create([
'estimate_id' => $estimate->id,
'type' => 'part',
@ -616,7 +663,7 @@ class Create extends Component
// Create line items for labor
foreach ($this->labor_operations as $operation) {
if (!empty($operation['operation'])) {
if (! empty($operation['operation'])) {
EstimateLineItem::create([
'estimate_id' => $estimate->id,
'type' => 'labor',
@ -659,21 +706,21 @@ class Create extends Component
'additional_issues_found' => $this->additional_issues_found,
'priority_level' => $this->priority_level,
'estimated_repair_time' => $this->estimated_repair_time,
'parts_required' => array_filter($this->parts_required, function($part) {
return !empty($part['part_name']);
'parts_required' => array_filter($this->parts_required, function ($part) {
return ! empty($part['part_name']);
}),
'labor_operations' => array_filter($this->labor_operations, function($operation) {
return !empty($operation['operation']);
'labor_operations' => array_filter($this->labor_operations, function ($operation) {
return ! empty($operation['operation']);
}),
'special_tools_required' => array_filter($this->special_tools_required, function($tool) {
return !empty($tool['tool_name']);
'special_tools_required' => array_filter($this->special_tools_required, function ($tool) {
return ! empty($tool['tool_name']);
}),
'safety_concerns' => $this->safety_concerns,
'diagnostic_codes' => array_filter($this->diagnostic_codes, function($code) {
return !empty($code['code']);
'diagnostic_codes' => array_filter($this->diagnostic_codes, function ($code) {
return ! empty($code['code']);
}),
'test_results' => array_filter($this->test_results, function($result) {
return !empty($result['test_name']);
'test_results' => array_filter($this->test_results, function ($result) {
return ! empty($result['test_name']);
}),
'photos' => $photoUrls,
'notes' => $this->notes,
@ -693,10 +740,11 @@ class Create extends Component
session()->forget([
"diagnosis_parts_{$this->jobCard->id}",
"diagnosis_labor_{$this->jobCard->id}",
"diagnosis_codes_{$this->jobCard->id}"
"diagnosis_codes_{$this->jobCard->id}",
]);
session()->flash('message', 'Diagnosis completed successfully! Estimate #' . $estimate->estimate_number . ' has been created automatically.');
session()->flash('message', 'Diagnosis completed successfully! Estimate #'.$estimate->estimate_number.' has been created automatically.');
return redirect()->route('estimates.show', $estimate);
}

View File

@ -2,14 +2,102 @@
namespace App\Livewire\Diagnosis;
use Livewire\Component;
use App\Models\Diagnosis;
use Livewire\Component;
use Livewire\WithPagination;
class Index extends Component
{
use WithPagination;
public $search = '';
public $statusFilter = '';
public $priorityFilter = '';
public $dateFrom = '';
protected $queryString = [
'search' => ['except' => ''],
'statusFilter' => ['except' => ''],
'priorityFilter' => ['except' => ''],
'dateFrom' => ['except' => ''],
];
public function updatingSearch()
{
$this->resetPage();
}
public function updatingStatusFilter()
{
$this->resetPage();
}
public function updatingPriorityFilter()
{
$this->resetPage();
}
public function updatingDateFrom()
{
$this->resetPage();
}
public function clearFilters()
{
$this->search = '';
$this->statusFilter = '';
$this->priorityFilter = '';
$this->dateFrom = '';
$this->resetPage();
}
public function refreshList()
{
$this->resetPage();
$this->dispatch('$refresh');
}
public function render()
{
$diagnoses = Diagnosis::with(['jobCard', 'serviceCoordinator'])->paginate(20);
$query = Diagnosis::with([
'jobCard.customer',
'jobCard.vehicle',
'serviceCoordinator',
'estimate',
]);
// Apply search filter
if ($this->search) {
$query->whereHas('jobCard', function ($q) {
$q->where('job_card_number', 'like', '%'.$this->search.'%')
->orWhereHas('customer', function ($customerQuery) {
$customerQuery->where('first_name', 'like', '%'.$this->search.'%')
->orWhere('last_name', 'like', '%'.$this->search.'%')
->orWhere('phone', 'like', '%'.$this->search.'%');
});
});
}
// Apply status filter
if ($this->statusFilter) {
$query->where('diagnosis_status', $this->statusFilter);
}
// Apply priority filter
if ($this->priorityFilter) {
$query->where('priority_level', $this->priorityFilter);
}
// Apply date filter
if ($this->dateFrom) {
$query->whereDate('diagnosis_date', '>=', $this->dateFrom);
}
$diagnoses = $query->orderBy('created_at', 'desc')->paginate(20);
return view('livewire.diagnosis.index', compact('diagnoses'));
}
}

View File

@ -12,16 +12,25 @@ use Livewire\Component;
class Create extends Component
{
public Diagnosis $diagnosis;
public $terms_and_conditions = '';
public $validity_period_days = 30;
public $tax_rate = 8.25;
public $discount_amount = 0;
public $notes = '';
public $internal_notes = '';
public $lineItems = [];
public $subtotal = 0;
public $tax_amount = 0;
public $total_amount = 0;
protected $rules = [
@ -39,12 +48,12 @@ class Create extends Component
{
$this->diagnosis = $diagnosis->load([
'jobCard.customer',
'jobCard.vehicle'
'jobCard.vehicle',
]);
// Pre-populate from diagnosis
$this->initializeLineItems();
$this->terms_and_conditions = config('app.default_estimate_terms',
$this->terms_and_conditions = config('app.default_estimate_terms',
'This estimate is valid for 30 days. All work will be performed according to industry standards.'
);
}
@ -52,29 +61,47 @@ class Create extends Component
public function initializeLineItems()
{
// Add labor operations from diagnosis
foreach ($this->diagnosis->labor_operations as $labor) {
$this->lineItems[] = [
'type' => 'labor',
'description' => $labor['operation'],
'quantity' => $labor['estimated_hours'],
'unit_price' => $labor['labor_rate'],
'total_amount' => $labor['estimated_hours'] * $labor['labor_rate'],
'labor_hours' => $labor['estimated_hours'],
'labor_rate' => $labor['labor_rate'],
'required' => true,
];
if ($this->diagnosis->labor_operations) {
foreach ($this->diagnosis->labor_operations as $labor) {
$this->lineItems[] = [
'type' => 'labor',
'description' => $labor['operation'] ?? 'Labor Operation',
'quantity' => $labor['estimated_hours'] ?? 1,
'unit_price' => $labor['labor_rate'] ?? 75,
'total_amount' => ($labor['estimated_hours'] ?? 1) * ($labor['labor_rate'] ?? 75),
'labor_hours' => $labor['estimated_hours'] ?? 1,
'labor_rate' => $labor['labor_rate'] ?? 75,
'required' => true,
];
}
}
// Add parts from diagnosis
foreach ($this->diagnosis->parts_required as $part) {
if ($this->diagnosis->parts_required) {
foreach ($this->diagnosis->parts_required as $part) {
$this->lineItems[] = [
'type' => 'parts',
'part_id' => null,
'description' => ($part['part_name'] ?? 'Part').' ('.($part['part_number'] ?? 'N/A').')',
'quantity' => $part['quantity'] ?? 1,
'unit_price' => $part['estimated_cost'] ?? 0,
'total_amount' => ($part['quantity'] ?? 1) * ($part['estimated_cost'] ?? 0),
'markup_percentage' => 20,
'required' => true,
];
}
}
// If no line items from diagnosis, add a default labor item
if (empty($this->lineItems)) {
$this->lineItems[] = [
'type' => 'parts',
'part_id' => null,
'description' => $part['part_name'] . ' (' . $part['part_number'] . ')',
'quantity' => $part['quantity'],
'unit_price' => $part['estimated_cost'],
'total_amount' => $part['quantity'] * $part['estimated_cost'],
'markup_percentage' => 20,
'type' => 'labor',
'description' => 'Diagnostic and Repair Services',
'quantity' => $this->diagnosis->estimated_repair_time ?? 1,
'unit_price' => 75,
'total_amount' => ($this->diagnosis->estimated_repair_time ?? 1) * 75,
'labor_hours' => $this->diagnosis->estimated_repair_time ?? 1,
'labor_rate' => 75,
'required' => true,
];
}
@ -128,10 +155,26 @@ class Create extends Component
// Generate estimate number
$branchCode = $this->diagnosis->jobCard->branch_code;
$lastEstimateNumber = Estimate::where('estimate_number', 'like', $branchCode . '/EST%')
->whereYear('created_at', now()->year)
->count();
$estimateNumber = $branchCode . '/EST' . str_pad($lastEstimateNumber + 1, 4, '0', STR_PAD_LEFT);
// Use database lock to prevent race conditions
$maxNumber = \DB::transaction(function () use ($branchCode) {
$lastEstimate = Estimate::where('estimate_number', 'like', $branchCode.'/EST%')
->whereYear('created_at', now()->year)
->orderByRaw('CAST(SUBSTRING(estimate_number, '.(strlen($branchCode) + 5).') AS UNSIGNED) DESC')
->lockForUpdate()
->first();
$nextNumber = 1;
if ($lastEstimate) {
// Extract the number part from the estimate number (e.g., MAIN/EST0001 -> 1)
preg_match('/'.preg_quote($branchCode).'\/EST(\d+)/', $lastEstimate->estimate_number, $matches);
$nextNumber = isset($matches[1]) ? intval($matches[1]) + 1 : 1;
}
return $nextNumber;
});
$estimateNumber = $branchCode.'/EST'.str_pad($maxNumber, 4, '0', STR_PAD_LEFT);
$estimate = Estimate::create([
'estimate_number' => $estimateNumber,
@ -174,6 +217,7 @@ class Create extends Component
$this->diagnosis->jobCard->update(['status' => 'estimate_prepared']);
session()->flash('message', 'Estimate created successfully!');
return redirect()->route('estimates.show', $estimate);
}
@ -182,7 +226,7 @@ class Create extends Component
// This would be called after saving
$customer = $this->diagnosis->jobCard->customer;
$customer->notify(new EstimateNotification($estimate));
session()->flash('message', 'Estimate sent to customer successfully!');
}

View File

@ -0,0 +1,286 @@
<?php
namespace App\Livewire\Estimates;
use App\Models\Customer;
use App\Models\Estimate;
use App\Models\EstimateLineItem;
use App\Models\Part;
use App\Models\Vehicle;
use Livewire\Attributes\Layout;
use Livewire\Component;
class CreateStandalone extends Component
{
public $customerId = '';
public $vehicleId = '';
public $selectedCustomer = null;
public $selectedVehicle = null;
public $customerVehicles = [];
public $terms_and_conditions = '';
public $validity_period_days = 30;
public $tax_rate = 8.25;
public $discount_amount = 0;
public $notes = '';
public $internal_notes = '';
public $lineItems = [];
public $subtotal = 0;
public $tax_amount = 0;
public $total_amount = 0;
// Parts search
public $partSearch = '';
public $availableParts = [];
public $showPartsDropdown = false;
protected $rules = [
'customerId' => 'required|exists:customers,id',
'vehicleId' => 'required|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.*.description' => 'required|string',
'lineItems.*.quantity' => 'required|numeric|min:0.01',
'lineItems.*.unit_price' => 'required|numeric|min:0',
'lineItems.*.part_id' => 'nullable|exists:parts,id',
];
public function mount()
{
$this->initializeDefaults();
$this->addLineItem();
}
private function initializeDefaults()
{
$this->terms_and_conditions = 'This estimate is valid for '.$this->validity_period_days.' days from the date of issue. All prices are subject to change without notice. Additional charges may apply for unforeseen complications.';
}
public function updatedCustomerId($value)
{
if ($value) {
$this->selectedCustomer = Customer::find($value);
$this->customerVehicles = Vehicle::where('customer_id', $value)->get();
$this->vehicleId = '';
$this->selectedVehicle = null;
} else {
$this->selectedCustomer = null;
$this->customerVehicles = [];
$this->vehicleId = '';
$this->selectedVehicle = null;
}
}
public function updatedVehicleId($value)
{
if ($value) {
$this->selectedVehicle = Vehicle::find($value);
} else {
$this->selectedVehicle = null;
}
}
public function addLineItem()
{
$this->lineItems[] = [
'part_id' => null,
'part_number' => '',
'description' => '',
'quantity' => 1,
'unit_price' => 0,
'subtotal' => 0,
'type' => 'labour',
'stock_available' => null,
];
}
public function removeLineItem($index)
{
unset($this->lineItems[$index]);
$this->lineItems = array_values($this->lineItems);
$this->calculateTotals();
}
public function updatedLineItems($value, $name)
{
// Extract the index and field name from the wire:model path
if (preg_match('/(\d+)\.(.+)/', $name, $matches)) {
$index = $matches[1];
$field = $matches[2];
// Validate stock levels for parts
if ($field === 'quantity' && isset($this->lineItems[$index]['part_id']) && $this->lineItems[$index]['part_id']) {
$part = Part::find($this->lineItems[$index]['part_id']);
if ($part && $value > $part->quantity_on_hand) {
$this->addError("lineItems.{$index}.quantity", "Quantity cannot exceed available stock ({$part->quantity_on_hand})");
return;
}
}
// Clear the quantity error if validation passes
if ($field === 'quantity') {
$this->resetErrorBag("lineItems.{$index}.quantity");
}
}
$this->calculateTotals();
}
public function updatedPartSearch()
{
$this->searchParts($this->partSearch);
}
public function calculateTotals()
{
$this->subtotal = 0;
foreach ($this->lineItems as $index => $item) {
if (isset($item['quantity'], $item['unit_price'])) {
$lineSubtotal = $item['quantity'] * $item['unit_price'];
$this->lineItems[$index]['subtotal'] = $lineSubtotal;
$this->subtotal += $lineSubtotal;
}
}
$discountedSubtotal = max(0, $this->subtotal - ($this->discount_amount ?? 0));
$this->tax_amount = $discountedSubtotal * ($this->tax_rate / 100);
$this->total_amount = $discountedSubtotal + $this->tax_amount;
}
public function searchParts($query = '')
{
if (strlen($query) < 2) {
$this->availableParts = [];
$this->showPartsDropdown = false;
return;
}
$this->availableParts = Part::where('status', 'active')
->where(function ($q) use ($query) {
$q->where('name', 'like', "%{$query}%")
->orWhere('part_number', 'like', "%{$query}%")
->orWhere('description', 'like', "%{$query}%");
})
->orderBy('name')
->limit(10)
->get();
$this->showPartsDropdown = true;
}
public function selectPart($lineIndex, $partId)
{
$part = Part::find($partId);
if ($part) {
$this->lineItems[$lineIndex]['part_id'] = $part->id;
$this->lineItems[$lineIndex]['part_number'] = $part->part_number;
$this->lineItems[$lineIndex]['description'] = $part->name;
$this->lineItems[$lineIndex]['unit_price'] = $part->sell_price;
$this->lineItems[$lineIndex]['stock_available'] = $part->quantity_on_hand;
$this->lineItems[$lineIndex]['type'] = 'parts';
$this->calculateTotals();
}
$this->partSearch = '';
$this->availableParts = [];
$this->showPartsDropdown = false;
}
public function clearPartSelection($lineIndex)
{
$this->lineItems[$lineIndex]['part_id'] = null;
$this->lineItems[$lineIndex]['part_number'] = '';
$this->lineItems[$lineIndex]['stock_available'] = null;
$this->calculateTotals();
}
public function save()
{
$this->validate();
if (empty($this->lineItems)) {
session()->flash('error', 'At least one line item is required.');
return;
}
$estimate = Estimate::create([
'estimate_number' => $this->generateEstimateNumber(),
'customer_id' => $this->customerId,
'vehicle_id' => $this->vehicleId,
'job_card_id' => null, // No job card for standalone estimates
'diagnosis_id' => null, // No diagnosis for standalone estimates
'status' => 'draft',
'customer_approval_status' => 'pending',
'terms_and_conditions' => $this->terms_and_conditions,
'validity_period_days' => $this->validity_period_days,
'tax_rate' => $this->tax_rate,
'discount_amount' => $this->discount_amount ?? 0,
'subtotal_amount' => $this->subtotal,
'tax_amount' => $this->tax_amount,
'total_amount' => $this->total_amount,
'notes' => $this->notes,
'internal_notes' => $this->internal_notes,
'prepared_by_id' => auth()->id(),
]);
foreach ($this->lineItems as $item) {
EstimateLineItem::create([
'estimate_id' => $estimate->id,
'part_id' => $item['part_id'],
'part_number' => $item['part_number'],
'type' => $item['type'],
'description' => $item['description'],
'quantity' => $item['quantity'],
'unit_price' => $item['unit_price'],
'total_amount' => $item['quantity'] * $item['unit_price'], // Calculate total_amount
'subtotal' => $item['subtotal'],
]);
}
session()->flash('success', 'Estimate created successfully.');
return redirect()->route('estimates.show', $estimate);
}
private function generateEstimateNumber()
{
$lastEstimate = Estimate::latest()->first();
$lastNumber = $lastEstimate ? (int) substr($lastEstimate->estimate_number, -5) : 0;
return 'EST-'.str_pad($lastNumber + 1, 5, '0', STR_PAD_LEFT);
}
#[Layout('components.layouts.app')]
public function render()
{
$customers = Customer::orderBy('first_name')->orderBy('last_name')->get();
return view('livewire.estimates.create-standalone', [
'customers' => $customers,
]);
}
}

View File

@ -2,10 +2,501 @@
namespace App\Livewire\Estimates;
use App\Models\Estimate;
use App\Models\EstimateLineItem;
use App\Models\Part;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Edit extends Component
{
public Estimate $estimate;
#[Validate('required|string')]
public $terms_and_conditions = '';
#[Validate('required|integer|min:1|max:365')]
public $validity_period_days = 30;
#[Validate('required|numeric|min:0|max:50')]
public $tax_rate = 8.25;
#[Validate('nullable|numeric|min:0')]
public $discount_amount = 0;
#[Validate('nullable|string')]
public $notes = '';
#[Validate('nullable|string')]
public $internal_notes = '';
public $lineItems = [];
public $deletedItems = [];
public $subtotal = 0;
public $tax_amount = 0;
public $total_amount = 0;
// Advanced features
public $showAdvancedOptions = false;
public $bulkOperationMode = false;
public $selectedItems = [];
public $autoSave = true;
public $lastSaved;
// 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],
];
// Line item templates
public $newItem = [
'type' => 'labor',
'description' => '',
'quantity' => 1,
'unit_price' => 0,
'part_id' => null,
'markup_percentage' => 15,
'discount_type' => 'none',
'discount_value' => 0,
'notes' => '',
'is_taxable' => true,
];
protected $rules = [
'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.*.description' => 'required|string',
'lineItems.*.quantity' => 'required|numeric|min:0.01',
'lineItems.*.unit_price' => 'required|numeric|min:0',
];
public function mount(Estimate $estimate)
{
$this->estimate = $estimate->load([
'jobCard.customer',
'jobCard.vehicle',
'customer', // For standalone estimates
'vehicle', // For standalone estimates
'lineItems.part',
]);
$this->loadEstimateData();
$this->loadLineItems();
$this->calculateTotals();
}
protected function loadEstimateData()
{
$this->terms_and_conditions = $this->estimate->terms_and_conditions ?? '';
$this->validity_period_days = $this->estimate->validity_period_days ?? 30;
$this->tax_rate = $this->estimate->tax_rate ?? 8.25;
$this->discount_amount = $this->estimate->discount_amount ?? 0;
$this->notes = $this->estimate->notes ?? '';
$this->internal_notes = $this->estimate->internal_notes ?? '';
}
protected function loadLineItems()
{
$this->lineItems = $this->estimate->lineItems->map(function ($item) {
return [
'id' => $item->id,
'type' => $item->type,
'description' => $item->description,
'quantity' => $item->quantity,
'unit_price' => $item->unit_price,
'total_amount' => $item->total_amount,
'labor_hours' => $item->labor_hours,
'labor_rate' => $item->labor_rate,
'markup_percentage' => $item->markup_percentage ?? 15,
'discount_type' => $item->discount_type ?? 'none',
'discount_value' => $item->discount_value ?? 0,
'part_id' => $item->part_id,
'part_name' => $item->part?->name,
'notes' => $item->notes ?? '',
'is_taxable' => $item->is_taxable ?? true,
'is_editing' => false,
'required' => true,
];
})->toArray();
}
public function addLineItem()
{
$this->validate([
'newItem.type' => 'required|in:labor,parts,miscellaneous',
'newItem.description' => 'required|string|max:255',
'newItem.quantity' => 'required|numeric|min:0.01',
'newItem.unit_price' => 'required|numeric|min:0',
]);
$lineItem = [
'id' => null,
'type' => $this->newItem['type'],
'description' => $this->newItem['description'],
'quantity' => $this->newItem['quantity'],
'unit_price' => $this->newItem['unit_price'],
'part_id' => $this->newItem['part_id'],
'markup_percentage' => $this->newItem['markup_percentage'],
'discount_type' => $this->newItem['discount_type'],
'discount_value' => $this->newItem['discount_value'],
'notes' => $this->newItem['notes'],
'is_taxable' => $this->newItem['is_taxable'],
'is_editing' => false,
'required' => true,
];
$lineItem['total_amount'] = $this->calculateLineItemTotal($lineItem);
$this->lineItems[] = $lineItem;
// Reset new item form
$this->newItem = [
'type' => 'labor',
'description' => '',
'quantity' => 1,
'unit_price' => 0,
'part_id' => null,
'markup_percentage' => 15,
'discount_type' => 'none',
'discount_value' => 0,
'notes' => '',
'is_taxable' => true,
];
$this->calculateTotals();
if ($this->autoSave) {
$this->autoSaveEstimate();
}
}
public function addQuickPreset($presetKey)
{
if (isset($this->quickAddPresets[$presetKey])) {
$preset = $this->quickAddPresets[$presetKey];
$lineItem = array_merge([
'id' => null,
'part_id' => null,
'markup_percentage' => 15,
'discount_type' => 'none',
'discount_value' => 0,
'notes' => '',
'is_taxable' => true,
'is_editing' => false,
'required' => true,
], $preset);
$lineItem['total_amount'] = $this->calculateLineItemTotal($lineItem);
$this->lineItems[] = $lineItem;
$this->calculateTotals();
if ($this->autoSave) {
$this->autoSaveEstimate();
}
}
}
public function removeLineItem($index)
{
if (isset($this->lineItems[$index]['id'])) {
$this->deletedItems[] = $this->lineItems[$index]['id'];
}
unset($this->lineItems[$index]);
$this->lineItems = array_values($this->lineItems);
$this->calculateTotals();
if ($this->autoSave) {
$this->autoSaveEstimate();
}
}
public function duplicateLineItem($index)
{
if (isset($this->lineItems[$index])) {
$item = $this->lineItems[$index];
unset($item['id']); // Remove ID so it's treated as new
$item['description'] .= ' (Copy)';
$item['is_editing'] = false;
$this->lineItems[] = $item;
$this->calculateTotals();
}
}
public function editLineItem($index)
{
$this->lineItems[$index]['is_editing'] = true;
}
public function saveLineItem($index)
{
$this->lineItems[$index]['is_editing'] = false;
$this->lineItems[$index]['total_amount'] = $this->calculateLineItemTotal($this->lineItems[$index]);
$this->calculateTotals();
if ($this->autoSave) {
$this->autoSaveEstimate();
}
}
public function cancelEditLineItem($index)
{
$this->lineItems[$index]['is_editing'] = false;
// Reload from database if it's an existing item
if (isset($this->lineItems[$index]['id'])) {
$this->loadLineItems();
$this->calculateTotals();
}
}
public function toggleBulkMode()
{
$this->bulkOperationMode = ! $this->bulkOperationMode;
$this->selectedItems = [];
}
public function bulkDelete()
{
foreach ($this->selectedItems as $index) {
if (isset($this->lineItems[$index]['id'])) {
$this->deletedItems[] = $this->lineItems[$index]['id'];
}
unset($this->lineItems[$index]);
}
$this->lineItems = array_values($this->lineItems);
$this->selectedItems = [];
$this->bulkOperationMode = false;
$this->calculateTotals();
}
public function bulkApplyDiscount($discountType, $discountValue)
{
foreach ($this->selectedItems as $index) {
if (isset($this->lineItems[$index])) {
$this->lineItems[$index]['discount_type'] = $discountType;
$this->lineItems[$index]['discount_value'] = $discountValue;
$this->lineItems[$index]['total_amount'] = $this->calculateLineItemTotal($this->lineItems[$index]);
}
}
$this->selectedItems = [];
$this->bulkOperationMode = false;
$this->calculateTotals();
}
public function bulkApplyMarkup($markupPercentage)
{
foreach ($this->selectedItems as $index) {
if (isset($this->lineItems[$index])) {
$this->lineItems[$index]['markup_percentage'] = $markupPercentage;
$this->lineItems[$index]['total_amount'] = $this->calculateLineItemTotal($this->lineItems[$index]);
}
}
$this->selectedItems = [];
$this->bulkOperationMode = false;
$this->calculateTotals();
}
protected function calculateLineItemTotal($item)
{
$baseAmount = $item['quantity'] * $item['unit_price'];
// Apply markup
if (isset($item['markup_percentage']) && $item['markup_percentage'] > 0) {
$baseAmount += $baseAmount * ($item['markup_percentage'] / 100);
}
// Apply discount
if ($item['discount_type'] === 'percentage') {
$discount = $baseAmount * ($item['discount_value'] / 100);
} elseif ($item['discount_type'] === 'fixed') {
$discount = min($item['discount_value'], $baseAmount);
} else {
$discount = 0;
}
return $baseAmount - $discount;
}
public function updatedLineItems()
{
$this->calculateTotals();
if ($this->autoSave) {
$this->autoSaveEstimate();
}
}
public function calculateTotals()
{
// Calculate line item totals
foreach ($this->lineItems as $index => $item) {
$this->lineItems[$index]['total_amount'] = $this->calculateLineItemTotal($item);
}
// Calculate subtotal
$this->subtotal = collect($this->lineItems)->sum('total_amount');
// Calculate tax on taxable items only
$taxableAmount = collect($this->lineItems)
->where('is_taxable', true)
->sum('total_amount') - $this->discount_amount;
$this->tax_amount = max(0, $taxableAmount) * ($this->tax_rate / 100);
// Calculate total
$this->total_amount = $this->subtotal - $this->discount_amount + $this->tax_amount;
}
public function autoSaveEstimate()
{
try {
$this->estimate->update([
'subtotal' => $this->subtotal,
'tax_amount' => $this->tax_amount,
'total_amount' => $this->total_amount,
'labor_cost' => collect($this->lineItems)->where('type', 'labor')->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'),
'last_auto_saved_at' => now(),
]);
$this->lastSaved = now()->format('H:i:s');
} catch (\Exception $e) {
Log::error('Auto-save failed', [
'estimate_id' => $this->estimate->id,
'error' => $e->getMessage(),
]);
}
}
public function save()
{
$this->validate();
try {
DB::transaction(function () {
$this->calculateTotals();
// Update estimate
$this->estimate->update([
'terms_and_conditions' => $this->terms_and_conditions,
'validity_period_days' => $this->validity_period_days,
'tax_rate' => $this->tax_rate,
'discount_amount' => $this->discount_amount,
'notes' => $this->notes,
'internal_notes' => $this->internal_notes,
'subtotal' => $this->subtotal,
'tax_amount' => $this->tax_amount,
'total_amount' => $this->total_amount,
'labor_cost' => collect($this->lineItems)->where('type', 'labor')->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'),
'updated_by_id' => auth()->id(),
]);
// Delete removed items
if (! empty($this->deletedItems)) {
EstimateLineItem::whereIn('id', $this->deletedItems)->delete();
}
// Update/create line items
foreach ($this->lineItems as $item) {
if ($item['id']) {
EstimateLineItem::where('id', $item['id'])->update([
'type' => $item['type'],
'description' => $item['description'],
'quantity' => $item['quantity'],
'unit_price' => $item['unit_price'],
'total_amount' => $item['total_amount'],
'labor_hours' => $item['labor_hours'] ?? null,
'labor_rate' => $item['labor_rate'] ?? null,
'markup_percentage' => $item['markup_percentage'] ?? null,
'discount_type' => $item['discount_type'] ?? 'none',
'discount_value' => $item['discount_value'] ?? 0,
'part_id' => $item['part_id'],
'notes' => $item['notes'] ?? '',
'is_taxable' => $item['is_taxable'] ?? true,
]);
} else {
$this->estimate->lineItems()->create([
'type' => $item['type'],
'description' => $item['description'],
'quantity' => $item['quantity'],
'unit_price' => $item['unit_price'],
'total_amount' => $item['total_amount'],
'labor_hours' => $item['labor_hours'] ?? null,
'labor_rate' => $item['labor_rate'] ?? null,
'markup_percentage' => $item['markup_percentage'] ?? null,
'discount_type' => $item['discount_type'] ?? 'none',
'discount_value' => $item['discount_value'] ?? 0,
'part_id' => $item['part_id'],
'notes' => $item['notes'] ?? '',
'is_taxable' => $item['is_taxable'] ?? true,
]);
}
}
});
Log::info('Estimate updated', [
'estimate_id' => $this->estimate->id,
'updated_by' => auth()->id(),
]);
session()->flash('success', 'Estimate updated successfully.');
return redirect()->route('estimates.show', $this->estimate);
} catch (\Exception $e) {
Log::error('Failed to update estimate', [
'estimate_id' => $this->estimate->id,
'error' => $e->getMessage(),
]);
session()->flash('error', 'Failed to update estimate. Please try again.');
}
}
public function getAvailablePartsProperty()
{
return Part::where('stock_quantity', '>', 0)
->orderBy('name')
->get(['id', 'name', 'part_number', 'unit_price']);
}
public function toggleAdvancedOptions()
{
$this->showAdvancedOptions = ! $this->showAdvancedOptions;
}
public function toggleAutoSave()
{
$this->autoSave = ! $this->autoSave;
}
#[Layout('components.layouts.app')]
public function render()
{
return view('livewire.estimates.edit');

View File

@ -2,7 +2,11 @@
namespace App\Livewire\Estimates;
use App\Models\Customer;
use App\Models\Estimate;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
@ -10,39 +14,461 @@ class Index extends Component
{
use WithPagination;
#[Url(as: 'search')]
public $search = '';
#[Url(as: 'status')]
public $statusFilter = '';
#[Url(as: 'approval')]
public $approvalStatusFilter = '';
public function updatingSearch()
#[Url(as: 'customer')]
public $customerFilter = '';
#[Url(as: 'date_from')]
public $dateFrom = '';
#[Url(as: 'date_to')]
public $dateTo = '';
#[Url(as: 'sort')]
public $sortBy = 'created_at';
#[Url(as: 'direction')]
public $sortDirection = 'desc';
#[Url(as: 'per_page')]
public $perPage = 15;
// Advanced filters
public $showAdvancedFilters = false;
public $totalAmountMin = '';
public $totalAmountMax = '';
public $validityFilter = '';
public $branchFilter = '';
// Bulk operations
public $bulkMode = false;
public $selectedEstimates = [];
public $selectAll = false;
// Quick stats
public $stats = [];
protected $queryString = [
'search' => ['except' => ''],
'statusFilter' => ['except' => ''],
'approvalStatusFilter' => ['except' => ''],
'customerFilter' => ['except' => ''],
'dateFrom' => ['except' => ''],
'dateTo' => ['except' => ''],
'sortBy' => ['except' => 'created_at'],
'sortDirection' => ['except' => 'desc'],
'perPage' => ['except' => 15],
];
public function mount()
{
$this->loadStats();
}
public function loadStats()
{
$userId = auth()->id();
// Check if user can view all estimates or only their own
$canViewAll = Auth::user()->can('viewAny', Estimate::class);
// Build base where clause
$baseWhere = [];
if (! $canViewAll) {
$baseWhere['prepared_by_id'] = $userId;
}
// Calculate stats individually with fresh queries each time
$this->stats = [
'total' => Estimate::where($baseWhere)->count(),
'draft' => Estimate::where($baseWhere)->where('status', 'draft')->count(),
'sent' => Estimate::where($baseWhere)->where('status', 'sent')->count(),
'approved' => Estimate::where($baseWhere)->where('customer_approval_status', 'approved')->count(),
'pending' => Estimate::where($baseWhere)->whereIn('status', ['sent', 'viewed'])->where('customer_approval_status', 'pending')->count(),
'expired' => $this->getExpiredCount($canViewAll ? null : $userId),
'total_value' => Estimate::where($baseWhere)->sum('total_amount') ?: 0,
'avg_value' => Estimate::where($baseWhere)->avg('total_amount') ?: 0,
];
}
private function getExpiredCount($userId = null)
{
$query = Estimate::where('status', '!=', 'approved')
->whereNotNull('validity_period_days');
if ($userId) {
$query->where('prepared_by_id', $userId);
}
if (\DB::getDriverName() === 'mysql') {
return $query->whereRaw('DATE_ADD(created_at, INTERVAL validity_period_days DAY) < NOW()')->count();
} else {
return $query->whereRaw("datetime(created_at, '+' || validity_period_days || ' days') < datetime('now')")->count();
}
}
public function getDateAddExpressionForDatabase($comparison = 'expired')
{
// Use the actual database connection driver instead of config
$databaseDriver = \DB::getDriverName();
if ($databaseDriver === 'mysql') {
switch ($comparison) {
case 'expired':
return 'DATE_ADD(created_at, INTERVAL validity_period_days DAY) < NOW()';
case 'expiring_soon':
return 'DATE_ADD(created_at, INTERVAL validity_period_days DAY) BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 7 DAY)';
case 'valid':
return 'DATE_ADD(created_at, INTERVAL validity_period_days DAY) > NOW()';
default:
return 'DATE_ADD(created_at, INTERVAL validity_period_days DAY) < NOW()';
}
} else {
// SQLite syntax
switch ($comparison) {
case 'expired':
return "datetime(created_at, '+' || validity_period_days || ' days') < datetime('now')";
case 'expiring_soon':
return "datetime(created_at, '+' || validity_period_days || ' days') BETWEEN datetime('now') AND datetime('now', '+7 days')";
case 'valid':
return "datetime(created_at, '+' || validity_period_days || ' days') > datetime('now')";
default:
return "datetime(created_at, '+' || validity_period_days || ' days') < datetime('now')";
}
}
}
public function updatedSearch()
{
$this->resetPage();
}
public function updatedStatusFilter()
{
$this->resetPage();
}
public function updatedApprovalStatusFilter()
{
$this->resetPage();
}
public function updatedCustomerFilter()
{
$this->resetPage();
}
public function updatedDateFrom()
{
$this->resetPage();
}
public function updatedDateTo()
{
$this->resetPage();
}
public function updatedPerPage()
{
$this->resetPage();
}
public function sortBy($field)
{
if ($this->sortBy === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $field;
$this->sortDirection = 'asc';
}
$this->resetPage();
}
public function clearFilters()
{
$this->reset([
'search',
'statusFilter',
'approvalStatusFilter',
'customerFilter',
'dateFrom',
'dateTo',
'totalAmountMin',
'totalAmountMax',
'validityFilter',
'branchFilter',
]);
$this->resetPage();
}
public function toggleAdvancedFilters()
{
$this->showAdvancedFilters = ! $this->showAdvancedFilters;
}
public function toggleBulkMode()
{
$this->bulkMode = ! $this->bulkMode;
$this->selectedEstimates = [];
$this->selectAll = false;
}
public function updatedSelectAll($value)
{
if ($value) {
$this->selectedEstimates = $this->getEstimates()->pluck('id')->toArray();
} else {
$this->selectedEstimates = [];
}
}
public function bulkAction($action)
{
if (empty($this->selectedEstimates)) {
session()->flash('error', 'Please select estimates to perform bulk action.');
return;
}
$estimates = Estimate::whereIn('id', $this->selectedEstimates)->get();
switch ($action) {
case 'delete':
$estimates->each(function ($estimate) {
if (Auth::user()->can('delete', $estimate)) {
$estimate->delete();
}
});
session()->flash('success', count($this->selectedEstimates).' estimates deleted.');
break;
case 'mark_sent':
$estimates->each(function ($estimate) {
if (Auth::user()->can('update', $estimate)) {
$estimate->update([
'status' => 'sent',
'sent_to_customer_at' => now(),
]);
}
});
session()->flash('success', count($this->selectedEstimates).' estimates marked as sent.');
break;
case 'export':
// Export functionality would go here
session()->flash('success', 'Export started for '.count($this->selectedEstimates).' estimates.');
break;
}
$this->selectedEstimates = [];
$this->selectAll = false;
$this->bulkMode = false;
$this->loadStats();
}
public function getEstimates()
{
$query = Estimate::with([
'jobCard.customer',
'jobCard.vehicle',
'jobCard.branch',
'customer', // For standalone estimates
'vehicle', // For standalone estimates
'preparedBy',
]);
// Apply permissions
if (! Auth::user()->can('viewAny', Estimate::class)) {
$query->where('prepared_by_id', Auth::id());
}
// Search filter
if ($this->search) {
$query->where(function ($q) {
$q->where('estimate_number', 'like', '%'.$this->search.'%')
// Search in jobCard customers (traditional estimates)
->orWhereHas('jobCard.customer', function ($customerQuery) {
$customerQuery->where('first_name', 'like', '%'.$this->search.'%')
->orWhere('last_name', 'like', '%'.$this->search.'%')
->orWhere('email', 'like', '%'.$this->search.'%')
->orWhere('phone', 'like', '%'.$this->search.'%');
})
// Search in direct customers (standalone estimates)
->orWhereHas('customer', function ($customerQuery) {
$customerQuery->where('first_name', 'like', '%'.$this->search.'%')
->orWhere('last_name', 'like', '%'.$this->search.'%')
->orWhere('email', 'like', '%'.$this->search.'%')
->orWhere('phone', 'like', '%'.$this->search.'%');
})
// Search in jobCard vehicles (traditional estimates)
->orWhereHas('jobCard.vehicle', function ($vehicleQuery) {
$vehicleQuery->where('license_plate', 'like', '%'.$this->search.'%')
->orWhere('vin', 'like', '%'.$this->search.'%')
->orWhereRaw("CONCAT(year, ' ', make, ' ', model) LIKE ?", ['%'.$this->search.'%']);
})
// Search in direct vehicles (standalone estimates)
->orWhereHas('vehicle', function ($vehicleQuery) {
$vehicleQuery->where('license_plate', 'like', '%'.$this->search.'%')
->orWhere('vin', 'like', '%'.$this->search.'%')
->orWhereRaw("CONCAT(year, ' ', make, ' ', model) LIKE ?", ['%'.$this->search.'%']);
});
});
}
// Status filter
if ($this->statusFilter) {
if ($this->statusFilter === 'pending_approval') {
$query->whereIn('status', ['sent', 'viewed'])
->where('customer_approval_status', 'pending');
} elseif ($this->statusFilter === 'expired') {
$query->whereRaw($this->getDateAddExpressionForDatabase('expired'))
->where('status', '!=', 'approved');
} else {
$query->where('status', $this->statusFilter);
}
}
// Approval status filter
if ($this->approvalStatusFilter) {
$query->where('customer_approval_status', $this->approvalStatusFilter);
}
// Customer filter
if ($this->customerFilter) {
$query->whereHas('jobCard.customer', function ($customerQuery) {
$customerQuery->where('id', $this->customerFilter);
});
}
// Date filters
if ($this->dateFrom) {
$query->whereDate('created_at', '>=', $this->dateFrom);
}
if ($this->dateTo) {
$query->whereDate('created_at', '<=', $this->dateTo);
}
// Advanced filters
if ($this->totalAmountMin) {
$query->where('total_amount', '>=', $this->totalAmountMin);
}
if ($this->totalAmountMax) {
$query->where('total_amount', '<=', $this->totalAmountMax);
}
if ($this->validityFilter) {
if ($this->validityFilter === 'expired') {
$query->whereRaw($this->getDateAddExpressionForDatabase('expired'));
} elseif ($this->validityFilter === 'expiring_soon') {
$query->whereRaw($this->getDateAddExpressionForDatabase('expiring_soon'));
} elseif ($this->validityFilter === 'valid') {
$query->whereRaw($this->getDateAddExpressionForDatabase('valid'));
}
}
if ($this->branchFilter) {
$query->whereHas('jobCard.branch', function ($branchQuery) {
$branchQuery->where('code', $this->branchFilter);
});
}
// Sorting
$query->orderBy($this->sortBy, $this->sortDirection);
return $query->paginate($this->perPage);
}
public function getCustomersProperty()
{
return Customer::orderBy('first_name')->orderBy('last_name')->get(['id', 'first_name', 'last_name'])->map(function ($customer) {
return (object) [
'id' => $customer->id,
'name' => $customer->name,
];
});
}
public function getBranchesProperty()
{
return \App\Models\Branch::orderBy('name')->get(['code', 'name']);
}
public function sendEstimate($estimateId)
{
$estimate = Estimate::findOrFail($estimateId);
if (! Auth::user()->can('update', $estimate)) {
session()->flash('error', 'You are not authorized to send this estimate.');
return;
}
if ($estimate->status !== 'draft') {
session()->flash('error', 'Only draft estimates can be sent.');
return;
}
$estimate->update([
'status' => 'sent',
'sent_to_customer_at' => now(),
]);
// TODO: Send email/SMS notification to customer
session()->flash('success', 'Estimate sent to customer successfully.');
$this->loadStats();
}
public function confirmDelete($estimateId)
{
$estimate = Estimate::findOrFail($estimateId);
if (! Auth::user()->can('delete', $estimate)) {
session()->flash('error', 'You are not authorized to delete this estimate.');
return;
}
// For now, just delete directly. In production, you might want a confirmation modal
$estimate->delete();
session()->flash('success', 'Estimate deleted successfully.');
$this->loadStats();
}
#[Layout('components.layouts.app')]
public function render()
{
$estimates = Estimate::with(['jobCard.customer', 'jobCard.vehicle', 'preparedBy'])
->when($this->search, function ($query) {
$query->where(function ($q) {
$q->where('estimate_number', 'like', '%' . $this->search . '%')
->orWhereHas('jobCard', function ($jobQuery) {
$jobQuery->where('job_number', 'like', '%' . $this->search . '%')
->orWhereHas('customer', function ($customerQuery) {
$customerQuery->where('first_name', 'like', '%' . $this->search . '%')
->orWhere('last_name', 'like', '%' . $this->search . '%');
});
});
});
})
->when($this->statusFilter, function ($query) {
$query->where('status', $this->statusFilter);
})
->when($this->approvalStatusFilter, function ($query) {
$query->where('customer_approval_status', $this->approvalStatusFilter);
})
// Get available diagnoses that don't have estimates yet
$availableDiagnoses = \App\Models\Diagnosis::whereDoesntHave('estimate')
->with(['jobCard.customer', 'jobCard.vehicle'])
->latest()
->paginate(15);
->limit(5)
->get();
return view('livewire.estimates.index', compact('estimates'));
return view('livewire.estimates.index', [
'estimates' => $this->getEstimates(),
'customers' => $this->customers,
'branches' => $this->branches,
'stats' => $this->stats,
'availableDiagnoses' => $availableDiagnoses,
]);
}
}

View File

@ -2,10 +2,270 @@
namespace App\Livewire\Estimates;
use App\Models\Estimate;
use App\Notifications\EstimateNotification;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Log;
use Livewire\Attributes\Layout;
use Livewire\Component;
class Show extends Component
{
public Estimate $estimate;
public $showItemDetails = false;
public function mount(Estimate $estimate)
{
$this->estimate = $estimate->load([
'jobCard.customer',
'jobCard.vehicle',
'jobCard.branch',
'customer', // For standalone estimates
'vehicle', // For standalone estimates
'diagnosis',
'preparedBy',
'lineItems',
'workOrders',
]);
}
public function approveEstimate()
{
if (! auth()->user()->can('approve', $this->estimate)) {
session()->flash('error', 'You do not have permission to approve this estimate.');
return;
}
try {
$this->estimate->update([
'customer_approval_status' => 'approved',
'customer_approved_at' => now(),
'customer_approval_method' => 'staff_portal',
'status' => 'approved',
]);
// Update job card status to approved (if job card exists)
if ($this->estimate->jobCard) {
$this->estimate->jobCard->update([
'status' => 'approved',
]);
}
// Notify relevant parties
$customer = $this->estimate->customer ?? $this->estimate->jobCard?->customer;
if ($customer) {
$customer->notify(
new EstimateNotification($this->estimate, 'approved')
);
}
Log::info('Estimate approved', [
'estimate_id' => $this->estimate->id,
'approved_by' => auth()->id(),
]);
session()->flash('success', 'Estimate approved successfully.');
// Refresh estimate data
$this->estimate->refresh();
$this->dispatch('$refresh');
} catch (\Exception $e) {
Log::error('Failed to approve estimate', [
'estimate_id' => $this->estimate->id,
'error' => $e->getMessage(),
]);
session()->flash('error', 'Failed to approve estimate. Please try again.');
}
}
public function rejectEstimate()
{
if (! auth()->user()->can('reject', $this->estimate)) {
session()->flash('error', 'You do not have permission to reject this estimate.');
return;
}
try {
$this->estimate->update([
'customer_approval_status' => 'rejected',
'customer_approved_at' => now(),
'customer_approval_method' => 'staff_portal',
'status' => 'rejected',
]);
// Notify relevant parties
$customer = $this->estimate->customer ?? $this->estimate->jobCard?->customer;
if ($customer) {
$customer->notify(
new EstimateNotification($this->estimate, 'rejected')
);
}
Log::info('Estimate rejected', [
'estimate_id' => $this->estimate->id,
'rejected_by' => auth()->id(),
]);
session()->flash('success', 'Estimate rejected.');
// Refresh estimate data
$this->estimate->refresh();
$this->dispatch('$refresh');
} catch (\Exception $e) {
Log::error('Failed to reject estimate', [
'estimate_id' => $this->estimate->id,
'error' => $e->getMessage(),
]);
session()->flash('error', 'Failed to reject estimate. Please try again.');
}
}
public function sendToCustomer()
{
try {
$this->estimate->update([
'status' => 'sent',
'sent_to_customer_at' => now(),
'sent_by_id' => auth()->id(),
]);
// Send notification to customer
$customer = $this->estimate->customer ?? $this->estimate->jobCard?->customer;
if ($customer) {
$customer->notify(
new EstimateNotification($this->estimate, 'sent')
);
}
// Update job card status (if job card exists)
if ($this->estimate->jobCard) {
$this->estimate->jobCard->update([
'status' => 'estimate_sent',
]);
}
Log::info('Estimate sent to customer', [
'estimate_id' => $this->estimate->id,
'customer_id' => $this->estimate->customer_id ?? $this->estimate->jobCard?->customer?->id,
'sent_by' => auth()->id(),
]);
session()->flash('success', 'Estimate sent to customer successfully.');
// Refresh estimate data
$this->estimate->refresh();
$this->dispatch('$refresh');
} catch (\Exception $e) {
Log::error('Failed to send estimate to customer', [
'estimate_id' => $this->estimate->id,
'error' => $e->getMessage(),
]);
session()->flash('error', 'Failed to send estimate to customer. Please try again.');
}
}
public function duplicateEstimate()
{
try {
// Create a new estimate based on current one
$newEstimate = $this->estimate->replicate();
$newEstimate->estimate_number = 'EST-'.str_pad(Estimate::max('id') + 1, 6, '0', STR_PAD_LEFT);
$newEstimate->status = 'draft';
$newEstimate->customer_approval_status = 'pending';
$newEstimate->sent_to_customer_at = null;
$newEstimate->customer_viewed_at = null;
$newEstimate->customer_approved_at = null;
$newEstimate->prepared_by_id = auth()->id();
$newEstimate->save();
// Duplicate line items
foreach ($this->estimate->lineItems as $lineItem) {
$newLineItem = $lineItem->replicate();
$newLineItem->estimate_id = $newEstimate->id;
$newLineItem->save();
}
Log::info('Estimate duplicated', [
'original_estimate_id' => $this->estimate->id,
'new_estimate_id' => $newEstimate->id,
'duplicated_by' => auth()->id(),
]);
session()->flash('success', 'Estimate has been duplicated successfully.');
return redirect()->route('estimates.edit', $newEstimate);
} catch (\Exception $e) {
Log::error('Failed to duplicate estimate', [
'estimate_id' => $this->estimate->id,
'error' => $e->getMessage(),
]);
session()->flash('error', 'Failed to duplicate estimate. Please try again.');
}
}
public function downloadPDF()
{
try {
// Generate PDF using DomPDF
$pdf = Pdf::loadView('estimates.pdf', [
'estimate' => $this->estimate,
]);
// Log the download
Log::info('Estimate PDF downloaded', [
'estimate_id' => $this->estimate->id,
'downloaded_by' => auth()->id(),
]);
return response()->streamDownload(function () use ($pdf) {
echo $pdf->output();
}, "estimate-{$this->estimate->estimate_number}.pdf");
} catch (\Exception $e) {
Log::error('Failed to generate estimate PDF', [
'estimate_id' => $this->estimate->id,
'error' => $e->getMessage(),
]);
session()->flash('error', 'Failed to generate PDF. Please try again.');
}
}
public function toggleItemDetails()
{
$this->showItemDetails = ! $this->showItemDetails;
}
public function refreshEstimate()
{
$this->estimate->refresh();
session()->flash('success', 'Estimate data refreshed.');
$this->dispatch('$refresh');
}
public function createWorkOrder()
{
if ($this->estimate->customer_approval_status !== 'approved') {
session()->flash('error', 'Estimate must be approved before creating a work order.');
return;
}
return redirect()->route('work-orders.create', ['estimate' => $this->estimate->id]);
}
#[Layout('components.layouts.app')]
public function render()
{
return view('livewire.estimates.show');

View File

@ -12,72 +12,180 @@ class Create extends Component
use WithFileUploads;
public JobCard $jobCard;
public $type; // 'incoming' or 'outgoing'
public $current_mileage = '';
public $fuel_level = '';
public $overall_condition = '';
public $recommendations = '';
public $damage_notes = '';
public $cleanliness_rating = 5;
public $quality_rating = '';
public $quality_rating = null;
public $follow_up_required = false;
public $notes = '';
public $additional_comments = '';
public $photos = [];
public $videos = [];
// Inspection checklist items
public $checklist = [
'exterior' => [
'body_condition' => '',
'paint_condition' => '',
'lights_working' => '',
'mirrors_intact' => '',
'windshield_condition' => '',
],
'interior' => [
'seats_condition' => '',
'dashboard_condition' => '',
'electronics_working' => '',
'upholstery_condition' => '',
],
'mechanical' => [
'engine_condition' => '',
'transmission_condition' => '',
'brakes_condition' => '',
'suspension_condition' => '',
'tires_condition' => '',
],
'fluids' => [
'oil_level' => '',
'coolant_level' => '',
'brake_fluid_level' => '',
'power_steering_fluid' => '',
]
];
public $damage_diagram_data = [];
// Comprehensive inspection checklist items matching the form
public $checklist = [];
protected $rules = [
'current_mileage' => 'required|numeric|min:0',
'fuel_level' => 'required|string',
'overall_condition' => 'required|in:excellent,good,fair,poor,damaged',
'current_mileage' => 'required|numeric|min:0|max:999999',
'fuel_level' => 'required|string|in:empty,low,quarter,half,three_quarter,full',
'overall_condition' => 'required|in:excellent,good,fair,poor',
'cleanliness_rating' => 'required|integer|min:1|max:10',
'quality_rating' => 'nullable|integer|min:1|max:10',
'additional_comments' => 'nullable|string|max:1000',
'damage_notes' => 'nullable|string|max:500',
'recommendations' => 'nullable|string|max:500',
'notes' => 'nullable|string|max:500',
'damage_diagram_data' => 'nullable|array',
];
protected $messages = [
'current_mileage.required' => 'Current mileage is required.',
'current_mileage.max' => 'Mileage cannot exceed 999,999 km.',
'fuel_level.required' => 'Please select the fuel level.',
'overall_condition.required' => 'Please select the overall vehicle condition.',
'additional_comments.max' => 'Additional comments cannot exceed 1000 characters.',
];
public function mount(JobCard $jobCard, $type)
{
$this->jobCard = $jobCard->load(['customer', 'vehicle']);
$this->type = $type;
$this->current_mileage = $jobCard->vehicle->current_mileage ?? '';
// Pull mileage and fuel level from job card if available
// Pre-populate with job card data if available
$this->current_mileage = $jobCard->mileage_in ?? $jobCard->vehicle->current_mileage ?? '';
// Normalize fuel level from job card
$jobCardFuelLevel = $jobCard->fuel_level_in ?? '';
$this->fuel_level = $this->normalizeFuelLevel($jobCardFuelLevel); // Initialize the comprehensive checklist array properly
$this->checklist = [
'documentation' => [
'manufacturers_handbook' => '',
'service_record_book' => '',
'company_drivers_handbook' => '',
'accident_report_form' => '',
'safety_inspection_sticker' => '',
],
'exterior' => [
'windshield_not_cracked' => '',
'windshield_wipers_functional' => '',
'headlights_high_low' => '',
'tail_lights_brake_lights' => '',
'emergency_brake_working' => '',
'power_brakes_working' => '',
'horn_works' => '',
'tires_good_shape' => '',
'no_air_leaks' => '',
'no_oil_grease_leaks' => '',
'no_fuel_leaks' => '',
'mirrors_good_position' => '',
'exhaust_system_working' => '',
'wheels_fasteners_tight' => '',
'turn_signals' => '',
'vehicle_free_damage' => '',
'loads_fastened' => '',
'spare_tire_good' => '',
'vehicle_condition_satisfactory' => '',
'defects_recorded' => '',
],
'interior' => [
'heating' => '',
'air_conditioning' => '',
'windshield_defrosting_system' => '',
'window_operation' => '',
'door_handles_locks' => '',
'alarm' => '',
'signals' => '',
'seat_belts_work' => '',
'interior_lights' => '',
'mirrors_good_position' => '',
'warning_lights' => '',
'fuel_levels' => '',
'oil_level_sufficient' => '',
'washer_fluids_sufficient' => '',
'radiator_fluid_sufficient' => '',
'emergency_roadside_supplies' => '',
],
'engine' => [
'engine_oil_level' => '',
'coolant_level_antifreeze' => '',
'battery_secured' => '',
'brake_fluid_level' => '',
'air_filter_clean' => '',
'belts_hoses_good' => '',
],
];
if ($type === 'outgoing') {
$this->rules['quality_rating'] = 'required|integer|min:1|max:10';
}
}
private function normalizeFuelLevel($level)
{
// Map various possible fuel level values to our standard ones
$fuelLevelMap = [
'empty' => 'empty',
'low' => 'low',
'quarter' => 'quarter',
'1/4' => 'quarter',
'half' => 'half',
'1/2' => 'half',
'three_quarter' => 'three_quarter',
'3/4' => 'three_quarter',
'full' => 'full',
// Add other possible variations
'E' => 'empty',
'L' => 'low',
'Q' => 'quarter',
'H' => 'half',
'F' => 'full',
];
$normalized = strtolower(trim($level));
return $fuelLevelMap[$normalized] ?? $level;
}
public function save()
{
$this->validate();
// Validate that at least some checklist items are completed
$completedItems = 0;
foreach ($this->checklist as $section => $items) {
foreach ($items as $item => $value) {
if (! empty($value)) {
$completedItems++;
}
}
}
if ($completedItems < 5) {
$this->addError('checklist', 'Please complete at least 5 inspection checklist items before saving.');
return;
}
// Handle file uploads
$photoUrls = [];
foreach ($this->photos as $photo) {
@ -100,36 +208,41 @@ class Create extends Component
'photos' => $photoUrls,
'videos' => $videoUrls,
'overall_condition' => $this->overall_condition,
'recommendations' => $this->recommendations,
'damage_notes' => $this->damage_notes,
'recommendations' => $this->recommendations ?: null,
'damage_notes' => $this->damage_notes ?: null,
'cleanliness_rating' => $this->cleanliness_rating,
'quality_rating' => $this->quality_rating,
'quality_rating' => $this->quality_rating ?: null,
'follow_up_required' => $this->follow_up_required,
'notes' => $this->notes,
'notes' => $this->notes ?: null,
'additional_comments' => $this->additional_comments ?: null,
'damage_diagram_data' => $this->damage_diagram_data,
'inspection_date' => now(),
'service_order_id' => null, // This field is nullable
]);
// Update job card status based on inspection type
if ($this->type === 'incoming') {
$this->jobCard->update([
'status' => 'inspection_completed',
'status' => 'in_diagnosis', // Use the correct status from enum
'mileage_in' => $this->current_mileage,
'fuel_level_in' => $this->fuel_level,
]);
} else {
$this->jobCard->update([
'status' => 'quality_check_completed',
'status' => 'completed', // Outgoing inspection means work is completed
'mileage_out' => $this->current_mileage,
'fuel_level_out' => $this->fuel_level,
]);
}
session()->flash('message', ucfirst($this->type) . ' inspection completed successfully!');
session()->flash('message', ucfirst($this->type).' inspection completed successfully!');
return redirect()->route('inspections.show', $inspection);
}
public function render()
{
return view('livewire.inspections.create');
return view('livewire.inspections.create')
->layout('components.layouts.app', ['title' => 'Vehicle Inspection']);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Livewire\Inspections;
use App\Models\VehicleInspection;
use Livewire\Component;
class InspectionPrint extends Component
{
public VehicleInspection $inspection;
public $jobCard;
public function mount(VehicleInspection $inspection)
{
$this->inspection = $inspection->load(['jobCard.customer', 'jobCard.vehicle', 'jobCard.branch', 'inspector']);
$this->jobCard = $this->inspection->jobCard;
}
public function render()
{
return view('livewire.inspections.print')
->layout('components.layouts.print', ['title' => 'Vehicle Inspection Report - '.$this->jobCard->job_card_number]);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Livewire\Inspections;
use App\Models\VehicleInspection;
use Livewire\Component;
class PrintView extends Component
{
public VehicleInspection $inspection;
public $jobCard;
public function mount(VehicleInspection $inspection)
{
$this->inspection = $inspection->load(['jobCard.customer', 'jobCard.vehicle', 'jobCard.branch', 'inspector']);
$this->jobCard = $this->inspection->jobCard;
}
public function render()
{
return view('livewire.inspections.print')
->layout('components.layouts.print', ['title' => 'Vehicle Inspection Report - '.$this->jobCard->job_card_number]);
}
}

View File

@ -2,12 +2,21 @@
namespace App\Livewire\Inspections;
use App\Models\VehicleInspection;
use Livewire\Component;
class Show extends Component
{
public VehicleInspection $inspection;
public function mount(VehicleInspection $inspection)
{
$this->inspection = $inspection->load(['jobCard.customer', 'jobCard.vehicle', 'inspector']);
}
public function render()
{
return view('livewire.inspections.show');
return view('livewire.inspections.show')
->layout('components.layouts.app', ['title' => 'Inspection Details']);
}
}

View File

@ -2,52 +2,70 @@
namespace App\Livewire\JobCards;
use Livewire\Component;
use App\Models\JobCard;
use App\Models\Customer;
use App\Models\Vehicle;
use App\Models\User;
use App\Models\Branch;
use App\Models\Customer;
use App\Models\JobCard;
use App\Models\User;
use App\Models\Vehicle;
use App\Services\WorkflowService;
use Illuminate\Validation\Rule;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Create extends Component
{
use AuthorizesRequests;
public $customer_id = '';
public $vehicle_id = '';
public $service_advisor_id = '';
public $branch_code = '';
public $arrival_datetime = '';
public $expected_completion_date = '';
public $mileage_in = '';
public $fuel_level_in = '';
public $customer_reported_issues = '';
public $vehicle_condition_notes = '';
public $keys_location = '';
public $personal_items_removed = false;
public $photos_taken = false;
public $priority = 'medium';
public $notes = '';
// Inspection fields
public $jobCardId = null;
// Inspection fields
public $perform_inspection = true;
public $inspector_id = '';
public $overall_condition = '';
public $inspection_notes = '';
public $inspection_checklist = [
'exterior_damage' => '',
'interior_condition' => '',
'tire_condition' => '',
'fluid_levels' => '',
'lights_working' => '',
];
public $inspection_checklist = [];
public $customers = [];
public $vehicles = [];
public $serviceAdvisors = [];
public $inspectors = [];
public $branches = [];
protected function rules()
@ -59,71 +77,47 @@ class Create extends Component
'branch_code' => 'required|string|max:10',
'arrival_datetime' => 'required|date',
'expected_completion_date' => 'nullable|date|after:arrival_datetime',
'mileage_in' => 'required|integer|min:0|max:999999',
'mileage_in' => 'nullable|integer|min:0',
'fuel_level_in' => 'nullable|string|max:20',
'customer_reported_issues' => 'required|string|max:2000',
'vehicle_condition_notes' => 'nullable|string|max:1000',
'keys_location' => 'nullable|string|max:255',
'personal_items_removed' => 'boolean',
'photos_taken' => 'boolean',
'priority' => 'required|in:low,medium,high,urgent',
'notes' => 'nullable|string|max:2000',
// Inspection fields
'perform_inspection' => 'boolean',
'inspector_id' => $this->perform_inspection ? 'required|exists:users,id' : 'nullable',
'overall_condition' => $this->perform_inspection ? 'required|in:excellent,good,fair,poor' : 'nullable',
'inspection_notes' => 'nullable|string|max:1000',
'inspection_checklist.exterior_damage' => $this->perform_inspection ? 'required|in:excellent,good,fair,poor' : 'nullable',
'inspection_checklist.interior_condition' => $this->perform_inspection ? 'required|in:excellent,good,fair,poor' : 'nullable',
'inspection_checklist.tire_condition' => $this->perform_inspection ? 'required|in:excellent,good,fair,poor' : 'nullable',
'inspection_checklist.fluid_levels' => $this->perform_inspection ? 'required|in:excellent,good,fair,poor' : 'nullable',
'inspection_checklist.lights_working' => $this->perform_inspection ? 'required|in:excellent,good,fair,poor' : 'nullable',
];
}
public function mount()
{
$this->loadData();
// Set default values
// Check if user has permission to create job cards
$this->authorize('create', JobCard::class);
$this->branch_code = auth()->user()->branch_code ?? config('app.default_branch_code', 'ACC');
$this->arrival_datetime = now()->format('Y-m-d\TH:i');
$this->expected_completion_date = now()->addDays(2)->format('Y-m-d');
$this->mileage_in = 0; // Set default mileage
$this->fuel_level_in = '1/2';
$this->keys_location = 'Reception Desk';
$this->branch_code = auth()->user()->branch_code ?? 'MAIN';
// Initialize inspection checklist with empty values
$this->inspection_checklist = [
'exterior_damage' => '',
'interior_condition' => '',
'tire_condition' => '',
'fluid_levels' => '',
'lights_working' => '',
];
$this->loadData();
$this->initializeInspectionChecklist();
}
public function loadData()
{
$user = auth()->user();
$this->customers = Customer::orderBy('first_name')->get();
// Load active branches
$this->branches = Branch::active()->orderBy('name')->get();
// Filter service advisors based on user's permissions and branch
$this->serviceAdvisors = User::whereIn('role', ['service_advisor', 'service_supervisor'])
->where('status', 'active')
->when(!$user->hasPermission('job-cards.view-all'), function ($query) use ($user) {
->when(! $user->hasPermission('job-cards.view-all'), function ($query) use ($user) {
return $query->where('branch_code', $user->branch_code);
})
->orderBy('name')
->get();
$this->inspectors = User::whereIn('role', ['service_supervisor', 'quality_inspector'])
->where('status', 'active')
->when(!$user->hasPermission('job-cards.view-all'), function ($query) use ($user) {
->when(! $user->hasPermission('job-cards.view-all'), function ($query) use ($user) {
return $query->where('branch_code', $user->branch_code);
})
->orderBy('name')
@ -160,44 +154,16 @@ class Create extends Component
];
}
protected function cleanFormData()
{
// Convert empty strings to null for optional fields
if ($this->expected_completion_date === '') {
$this->expected_completion_date = null;
}
if ($this->vehicle_condition_notes === '') {
$this->vehicle_condition_notes = null;
}
if ($this->notes === '') {
$this->notes = null;
}
if ($this->inspection_notes === '') {
$this->inspection_notes = null;
}
}
public function save()
{
// Check if user still has permission to create job cards
$this->authorize('create', JobCard::class);
$this->validate();
try {
// Check if user still has permission to create job cards
$this->authorize('create', JobCard::class);
// Clean form data (convert empty strings to null)
$this->cleanFormData();
// Add debug log
\Log::info('JobCard Create: Starting validation', ['user_id' => auth()->id()]);
$this->validate();
\Log::info('JobCard Create: Validation passed');
$workflowService = app(WorkflowService::class);
$data = [
'customer_id' => $this->customer_id,
'vehicle_id' => $this->vehicle_id,
@ -208,41 +174,32 @@ class Create extends Component
'mileage_in' => $this->mileage_in,
'fuel_level_in' => $this->fuel_level_in,
'customer_reported_issues' => $this->customer_reported_issues,
'vehicle_condition_notes' => $this->vehicle_condition_notes,
'keys_location' => $this->keys_location,
'personal_items_removed' => $this->personal_items_removed,
'photos_taken' => $this->photos_taken,
'priority' => $this->priority,
'notes' => $this->notes,
// Set default values for removed fields
'vehicle_condition_notes' => null,
'personal_items_removed' => false,
'photos_taken' => false,
];
if ($this->perform_inspection) {
$data['inspector_id'] = $this->inspector_id;
$data['inspection_checklist'] = $this->inspection_checklist;
$data['overall_condition'] = $this->overall_condition;
$data['inspection_notes'] = $this->inspection_notes;
}
\Log::info('JobCard Create: Creating job card with data', $data);
$jobCard = $workflowService->createJobCard($data);
\Log::info('JobCard Create: Job card created successfully', ['job_card_id' => $jobCard->id]);
// Set the job card ID to show the inspection button
$this->jobCardId = $jobCard->id;
session()->flash('success', 'Job card created successfully! Job Card #: ' . $jobCard->job_card_number);
return redirect()->route('job-cards.show', $jobCard);
session()->flash('success', 'Job card created successfully! Job Card #: '.$jobCard->job_card_number.'. You can now perform the initial inspection.');
} catch (\Illuminate\Validation\ValidationException $e) {
\Log::error('JobCard Create: Validation failed', ['errors' => $e->errors()]);
session()->flash('error', 'Please check the form for errors and try again.');
throw $e;
} catch (\Exception $e) {
\Log::error('JobCard Create: Exception occurred', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
// Reset form fields except jobCardId
$this->reset([
'customer_id', 'vehicle_id', 'service_advisor_id', 'branch_code',
'arrival_datetime', 'expected_completion_date', 'mileage_in',
'fuel_level_in', 'customer_reported_issues', 'keys_location',
'priority', 'notes',
]);
session()->flash('error', 'Failed to create job card: ' . $e->getMessage());
} catch (\Exception $e) {
session()->flash('error', 'Failed to create job card: '.$e->getMessage());
}
}

View File

@ -2,129 +2,52 @@
namespace App\Livewire\JobCards;
use App\Models\Branch;
use App\Models\JobCard;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Livewire\WithPagination;
use App\Models\JobCard;
use App\Models\Branch;
use App\Models\User;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class Index extends Component
{
use WithPagination, AuthorizesRequests;
use AuthorizesRequests, WithPagination;
public $search = '';
public $statusFilter = '';
public $branchFilter = '';
public $priorityFilter = '';
public $serviceAdvisorFilter = '';
public $dateRange = '';
public $sortBy = 'created_at';
public $sortDirection = 'desc';
// Bulk actions
public $selectedJobCards = [];
public $selectAll = false;
public $bulkAction = '';
// Statistics
public $statistics = [
'total' => 0,
'received' => 0,
'in_progress' => 0,
'pending_approval' => 0,
'completed_today' => 0,
'delivered_today' => 0,
'overdue' => 0,
];
protected $queryString = [
'search' => ['except' => ''],
'statusFilter' => ['except' => ''],
'branchFilter' => ['except' => ''],
'priorityFilter' => ['except' => ''],
'serviceAdvisorFilter' => ['except' => ''],
'dateRange' => ['except' => ''],
'sortBy' => ['except' => 'created_at'],
'sortDirection' => ['except' => 'desc'],
];
public function boot()
{
// Ensure properties are properly initialized
$this->selectedJobCards = $this->selectedJobCards ?? [];
$this->statistics = $this->statistics ?? [
'total' => 0,
'received' => 0,
'in_progress' => 0,
'pending_approval' => 0,
'completed_today' => 0,
'delivered_today' => 0,
'overdue' => 0,
];
}
public function mount()
{
$this->boot(); // Ensure properties are initialized
$this->authorize('viewAny', JobCard::class);
// Add debug information to debugbar
if (app()->bound('debugbar')) {
debugbar()->info('JobCard Index component mounted');
debugbar()->addMessage('User: ' . auth()->user()->name, 'user');
debugbar()->addMessage('User permissions checked for JobCard access', 'auth');
}
$this->loadStatistics();
}
public function updatingSearch()
{
$this->resetPage();
$this->selectedJobCards = [];
$this->selectAll = false;
$this->loadStatistics();
}
public function updatingStatusFilter()
{
$this->resetPage();
$this->selectedJobCards = [];
$this->selectAll = false;
$this->loadStatistics();
}
public function updatingBranchFilter()
{
$this->resetPage();
$this->selectedJobCards = [];
$this->selectAll = false;
$this->loadStatistics();
}
public function updatingPriorityFilter()
{
$this->resetPage();
$this->selectedJobCards = [];
$this->selectAll = false;
$this->loadStatistics();
}
public function updatingServiceAdvisorFilter()
{
$this->resetPage();
$this->selectedJobCards = [];
$this->selectAll = false;
$this->loadStatistics();
}
public function updatingDateRange()
{
$this->resetPage();
$this->selectedJobCards = [];
$this->selectAll = false;
$this->loadStatistics();
}
public function sortBy($field)
@ -135,380 +58,89 @@ class Index extends Component
$this->sortBy = $field;
$this->sortDirection = 'asc';
}
$this->resetPage();
}
public function refreshData()
public function getJobCardsProperty()
{
$this->loadStatistics();
$this->selectedJobCards = [];
$this->selectAll = false;
session()->flash('success', 'Data refreshed successfully.');
}
public function clearFilters()
{
$this->search = '';
$this->statusFilter = '';
$this->branchFilter = '';
$this->priorityFilter = '';
$this->serviceAdvisorFilter = '';
$this->dateRange = '';
$this->selectedJobCards = [];
$this->selectAll = false;
$this->resetPage();
$this->loadStatistics();
session()->flash('success', 'Filters cleared successfully.');
}
/**
* Get workflow progress percentage for a job card
*/
public function getWorkflowProgress($status)
{
$steps = [
JobCard::STATUS_RECEIVED => 1,
JobCard::STATUS_INSPECTED => 2,
JobCard::STATUS_ASSIGNED_FOR_DIAGNOSIS => 3,
JobCard::STATUS_IN_DIAGNOSIS => 4,
JobCard::STATUS_ESTIMATE_SENT => 5,
JobCard::STATUS_APPROVED => 6,
JobCard::STATUS_PARTS_PROCUREMENT => 7,
JobCard::STATUS_IN_PROGRESS => 8,
JobCard::STATUS_QUALITY_REVIEW_REQUIRED => 9,
JobCard::STATUS_COMPLETED => 10,
JobCard::STATUS_DELIVERED => 11,
];
$currentStep = $steps[$status] ?? 1;
return round(($currentStep / 11) * 100);
}
public function loadStatistics()
{
try {
if (app()->bound('debugbar')) {
debugbar()->startMeasure('statistics', 'Loading JobCard Statistics');
}
$user = auth()->user();
$query = JobCard::query();
// Apply branch filtering based on user permissions
if (!$user->hasPermission('job-cards.view-all')) {
if ($user->hasPermission('job-cards.view-own')) {
$query->where('service_advisor_id', $user->id);
} elseif ($user->hasPermission('job-cards.view')) {
$query->where('branch_code', $user->branch_code);
}
}
$this->statistics = [
'total' => $query->count(),
'received' => (clone $query)->where('status', JobCard::STATUS_RECEIVED)->count(),
'in_progress' => (clone $query)->whereIn('status', [
JobCard::STATUS_IN_DIAGNOSIS,
JobCard::STATUS_IN_PROGRESS,
JobCard::STATUS_PARTS_PROCUREMENT
])->count(),
'pending_approval' => (clone $query)->where('status', JobCard::STATUS_ESTIMATE_SENT)->count(),
'completed_today' => (clone $query)->where('status', JobCard::STATUS_COMPLETED)
->whereDate('completion_datetime', today())->count(),
'delivered_today' => (clone $query)->where('status', JobCard::STATUS_DELIVERED)
->whereDate('completion_datetime', today())->count(),
'overdue' => (clone $query)->where('expected_completion_date', '<', now())
->whereNotIn('status', [JobCard::STATUS_COMPLETED, JobCard::STATUS_DELIVERED])
->count(),
];
if (app()->bound('debugbar')) {
debugbar()->stopMeasure('statistics');
debugbar()->addMessage('Statistics loaded: ' . json_encode($this->statistics), 'statistics');
}
} catch (\Exception $e) {
// Fallback statistics if there's an error
$this->statistics = [
'total' => 0,
'received' => 0,
'in_progress' => 0,
'pending_approval' => 0,
'completed_today' => 0,
'delivered_today' => 0,
'overdue' => 0,
];
if (app()->bound('debugbar')) {
debugbar()->error('Error loading JobCard statistics: ' . $e->getMessage());
}
logger()->error('Error loading JobCard statistics: ' . $e->getMessage());
}
}
public function updatedSelectAll()
{
if ($this->selectAll) {
try {
$this->selectedJobCards = $this->getJobCards()->pluck('id')->toArray();
} catch (\Exception $e) {
$this->selectedJobCards = [];
$this->selectAll = false;
session()->flash('error', 'Unable to select all job cards. Please try again.');
}
} else {
$this->selectedJobCards = [];
}
}
public function processBulkAction()
{
if (empty($this->selectedJobCards) || empty($this->bulkAction)) {
session()->flash('error', 'Please select job cards and an action.');
return;
}
$successCount = 0;
$errorCount = 0;
foreach ($this->selectedJobCards as $jobCardId) {
try {
$jobCard = JobCard::find($jobCardId);
if (!$jobCard) continue;
switch ($this->bulkAction) {
case 'export_csv':
return $this->exportSelected();
break;
}
} catch (\Exception $e) {
$errorCount++;
}
}
$this->selectedJobCards = [];
$this->selectAll = false;
$this->bulkAction = '';
$this->loadStatistics(); // Refresh statistics after bulk operations
if ($successCount > 0) {
session()->flash('success', "{$successCount} job cards processed successfully.");
}
if ($errorCount > 0) {
session()->flash('error', "{$errorCount} job cards failed to process.");
}
}
public function exportSelected()
{
if (empty($this->selectedJobCards)) {
session()->flash('error', 'Please select job cards to export.');
return;
}
$jobCards = JobCard::with(['customer', 'vehicle', 'serviceAdvisor'])
->whereIn('id', $this->selectedJobCards)
->get();
$csv = "Job Card Number,Customer,Vehicle,Service Advisor,Status,Priority,Created Date,Expected Completion\n";
foreach ($jobCards as $jobCard) {
$csv .= sprintf(
"%s,%s,%s,%s,%s,%s,%s,%s\n",
$jobCard->job_card_number,
$jobCard->customer->full_name ?? '',
$jobCard->vehicle->display_name ?? '',
$jobCard->serviceAdvisor->name ?? '',
$jobCard->status,
$jobCard->priority,
$jobCard->created_at->format('Y-m-d'),
$jobCard->expected_completion_date ? $jobCard->expected_completion_date->format('Y-m-d') : ''
);
}
return response()->streamDownload(function () use ($csv) {
echo $csv;
}, 'job-cards-' . date('Y-m-d') . '.csv', [
'Content-Type' => 'text/csv',
]);
}
protected function getJobCards()
{
try {
$user = auth()->user();
$query = JobCard::query()
->with(['customer', 'vehicle', 'serviceAdvisor']);
// Apply permission-based filtering
if (!$user->hasPermission('job-cards.view-all')) {
if ($user->hasPermission('job-cards.view-own')) {
$query->where('service_advisor_id', $user->id);
} elseif ($user->hasPermission('job-cards.view')) {
$query->where('branch_code', $user->branch_code);
}
}
// Apply filters
if ($this->search) {
$user = auth()->user();
$query = JobCard::with(['customer', 'vehicle'])
->when($this->search, function ($query) {
$query->where(function ($q) {
$q->where('job_card_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 . '%');
})
->orWhereHas('vehicle', function ($vehicleQuery) {
$vehicleQuery->where('license_plate', 'like', '%' . $this->search . '%')
->orWhere('vin', 'like', '%' . $this->search . '%');
});
$q->where('job_card_number', 'like', '%'.$this->search.'%')
->orWhereHas('customer', function ($customerQuery) {
$customerQuery->where('name', 'like', '%'.$this->search.'%')
->orWhere('phone', 'like', '%'.$this->search.'%');
})
->orWhereHas('vehicle', function ($vehicleQuery) {
$vehicleQuery->where('license_plate', 'like', '%'.$this->search.'%')
->orWhere('make', 'like', '%'.$this->search.'%')
->orWhere('model', 'like', '%'.$this->search.'%');
});
});
}
if ($this->statusFilter) {
})
->when($this->statusFilter, function ($query) {
$query->where('status', $this->statusFilter);
}
if ($this->branchFilter) {
})
->when($this->branchFilter, function ($query) {
$query->where('branch_code', $this->branchFilter);
}
});
if ($this->priorityFilter) {
$query->where('priority', $this->priorityFilter);
// Apply permissions
if (! $user->hasPermission('job-cards.view-all')) {
if ($user->hasPermission('job-cards.view-own')) {
$query->where('service_advisor_id', $user->id);
} elseif ($user->branch_code) {
$query->where('branch_code', $user->branch_code);
}
if ($this->serviceAdvisorFilter) {
$query->where('service_advisor_id', $this->serviceAdvisorFilter);
}
if ($this->dateRange) {
switch ($this->dateRange) {
case 'today':
$query->whereDate('created_at', today());
break;
case 'week':
$query->whereBetween('created_at', [now()->startOfWeek(), now()->endOfWeek()]);
break;
case 'month':
$query->whereMonth('created_at', now()->month)
->whereYear('created_at', now()->year);
break;
case 'overdue':
$query->where('expected_completion_date', '<', now())
->whereNotIn('status', [JobCard::STATUS_COMPLETED, JobCard::STATUS_DELIVERED]);
break;
}
}
return $query->orderBy($this->sortBy, $this->sortDirection);
} catch (\Exception $e) {
logger()->error('Error in getJobCards query: ' . $e->getMessage());
// Return empty query as fallback
return JobCard::query()->whereRaw('1 = 0'); // Returns empty result set
}
return $query->orderBy($this->sortBy, $this->sortDirection);
}
public function getStatisticsProperty()
{
$user = auth()->user();
$baseQuery = JobCard::query();
// Apply same permission filters as main query
if (! $user->hasPermission('job-cards.view-all')) {
if ($user->hasPermission('job-cards.view-own')) {
$baseQuery->where('service_advisor_id', $user->id);
} elseif ($user->branch_code) {
$baseQuery->where('branch_code', $user->branch_code);
}
}
return [
'total' => (clone $baseQuery)->count(),
'received' => (clone $baseQuery)->where('status', JobCard::STATUS_RECEIVED)->count(),
'in_progress' => (clone $baseQuery)->whereIn('status', [
JobCard::STATUS_IN_DIAGNOSIS,
JobCard::STATUS_IN_PROGRESS,
JobCard::STATUS_PARTS_PROCUREMENT,
])->count(),
'completed_today' => (clone $baseQuery)->where('status', JobCard::STATUS_COMPLETED)
->whereDate('updated_at', today())->count(),
'pending_approval' => (clone $baseQuery)->where('status', JobCard::STATUS_ESTIMATE_SENT)->count(),
];
}
public function render()
{
try {
// Ensure statistics are always fresh and available
if (empty($this->statistics) || !isset($this->statistics['total'])) {
$this->loadStatistics();
}
$jobCards = $this->getJobCards()->paginate(20);
$jobCards = $this->jobCards->paginate(15);
$statusOptions = JobCard::getStatusOptions();
$priorityOptions = [
'low' => 'Low',
'medium' => 'Medium',
'high' => 'High',
'urgent' => 'Urgent',
];
$statusOptions = JobCard::getStatusOptions();
$branchOptions = Branch::active()
->orderBy('name')
->pluck('name', 'code')
->toArray();
$serviceAdvisorOptions = User::whereHas('roles', function ($query) {
$query->whereIn('name', ['service_advisor', 'service_supervisor']);
})
->where('status', 'active')
$branchOptions = Branch::active()
->orderBy('name')
->pluck('name', 'id')
->pluck('name', 'code')
->toArray();
$dateRangeOptions = [
'today' => 'Today',
'week' => 'This Week',
'month' => 'This Month',
'overdue' => 'Overdue',
];
return view('livewire.job-cards.index', compact(
'jobCards',
'statusOptions',
'priorityOptions',
'branchOptions',
'serviceAdvisorOptions',
'dateRangeOptions'
))->with([
'statistics' => $this->statistics,
'selectedJobCards' => $this->selectedJobCards ?? [],
'selectAll' => $this->selectAll ?? false,
'bulkAction' => $this->bulkAction ?? '',
'search' => $this->search ?? '',
'statusFilter' => $this->statusFilter ?? '',
'branchFilter' => $this->branchFilter ?? '',
'priorityFilter' => $this->priorityFilter ?? '',
'serviceAdvisorFilter' => $this->serviceAdvisorFilter ?? '',
'dateRange' => $this->dateRange ?? '',
'sortBy' => $this->sortBy ?? 'created_at',
'sortDirection' => $this->sortDirection ?? 'desc'
]);
} catch (\Exception $e) {
logger()->error('Error rendering JobCard Index: ' . $e->getMessage());
// Provide fallback data
$jobCards = collect()->paginate(20);
$statusOptions = [];
$priorityOptions = [];
$branchOptions = [];
$serviceAdvisorOptions = [];
$dateRangeOptions = [];
$statistics = $this->statistics ?? [];
session()->flash('error', 'There was an error loading the job cards. Please try again.');
return view('livewire.job-cards.index', compact(
'jobCards',
'statusOptions',
'priorityOptions',
'branchOptions',
'serviceAdvisorOptions',
'dateRangeOptions'
))->with([
'statistics' => $statistics,
'selectedJobCards' => $this->selectedJobCards ?? [],
'selectAll' => $this->selectAll ?? false,
'bulkAction' => $this->bulkAction ?? '',
'search' => $this->search ?? '',
'statusFilter' => $this->statusFilter ?? '',
'branchFilter' => $this->branchFilter ?? '',
'priorityFilter' => $this->priorityFilter ?? '',
'serviceAdvisorFilter' => $this->serviceAdvisorFilter ?? '',
'dateRange' => $this->dateRange ?? ''
]);
}
}
/**
* Handle the component invocation for route compatibility
*/
public function __invoke()
{
return $this->render();
return view('livewire.job-cards.index', [
'jobCards' => $jobCards,
'statistics' => $this->statistics,
'statusOptions' => $statusOptions,
'branchOptions' => $branchOptions,
]);
}
}

View File

@ -2,26 +2,111 @@
namespace App\Livewire\JobCards;
use Livewire\Component;
use App\Models\JobCard;
use App\Models\User;
use Livewire\Component;
class Show extends Component
{
public JobCard $jobCard;
public $availableTechnicians = [];
public $selectedTechnicianId = null;
public $showAssignmentModal = false;
public function mount(JobCard $jobCard)
{
$this->jobCard = $jobCard->load([
'customer',
'vehicle',
'serviceAdvisor',
'branch',
'incomingInspection',
'outgoingInspection',
'diagnosis',
'estimates',
'workOrders',
'timesheets'
'timesheets',
]);
// Load available technicians for assignment
$this->availableTechnicians = User::where('status', 'active')
->whereHas('roles', function ($query) {
$query->whereIn('name', ['technician', 'service_coordinator']);
})
->get(['id', 'name', 'email']);
}
public function assignForDiagnosis()
{
if (! $this->selectedTechnicianId) {
session()->flash('error', 'Please select a technician for diagnosis assignment.');
return;
}
if ($this->jobCard->status !== JobCard::STATUS_INSPECTED) {
session()->flash('error', 'Job card must be inspected before assignment for diagnosis.');
return;
}
try {
$this->jobCard->update([
'status' => JobCard::STATUS_ASSIGNED_FOR_DIAGNOSIS,
'service_advisor_id' => $this->selectedTechnicianId,
]);
$this->showAssignmentModal = false;
$this->selectedTechnicianId = null;
session()->flash('success', 'Job card assigned for diagnosis successfully.');
// Refresh the job card data
$this->jobCard->refresh();
} catch (\Exception $e) {
session()->flash('error', 'Failed to assign job card: '.$e->getMessage());
}
}
public function startDiagnosis()
{
if ($this->jobCard->status !== JobCard::STATUS_ASSIGNED_FOR_DIAGNOSIS) {
session()->flash('error', 'Job card must be assigned for diagnosis first.');
return;
}
try {
$this->jobCard->update([
'status' => JobCard::STATUS_IN_DIAGNOSIS,
]);
session()->flash('success', 'Diagnosis started. You can now create a diagnosis record.');
// Refresh the job card data
$this->jobCard->refresh();
// Redirect to diagnosis creation
return redirect()->route('diagnosis.create', $this->jobCard);
} catch (\Exception $e) {
session()->flash('error', 'Failed to start diagnosis: '.$e->getMessage());
}
}
public function openAssignmentModal()
{
$this->showAssignmentModal = true;
}
public function closeAssignmentModal()
{
$this->showAssignmentModal = false;
$this->selectedTechnicianId = null;
}
public function render()

View File

@ -4,8 +4,8 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Customer extends Model
{
@ -60,6 +60,11 @@ class Customer extends Model
return "{$this->first_name} {$this->last_name}";
}
public function getNameAttribute(): string
{
return $this->getFullNameAttribute();
}
public function getFormattedAddressAttribute(): string
{
return "{$this->address}, {$this->city}, {$this->state} {$this->zip_code}";

View File

@ -6,6 +6,7 @@ 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\Database\Eloquent\Relations\HasOne;
class Diagnosis extends Model
{

View File

@ -14,6 +14,8 @@ class Estimate extends Model
protected $fillable = [
'job_card_id',
'diagnosis_id',
'customer_id',
'vehicle_id',
'estimate_number',
'prepared_by_id',
'labor_cost',
@ -44,7 +46,7 @@ class Estimate extends Model
'parts_cost' => 'decimal:2',
'miscellaneous_cost' => 'decimal:2',
'subtotal' => 'decimal:2',
'tax_rate' => 'decimal:4',
'tax_rate' => 'decimal:2',
'tax_amount' => 'decimal:2',
'discount_amount' => 'decimal:2',
'total_amount' => 'decimal:2',
@ -57,22 +59,17 @@ class Estimate extends Model
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
);
$estimate->estimate_number = 'EST'.str_pad(rand(1, 9999), 4, '0', STR_PAD_LEFT);
}
});
}
public function jobCard(): BelongsTo
{
return $this->belongsTo(JobCard::class);
return $this->belongsTo(JobCard::class, 'job_card_id');
}
public function diagnosis(): BelongsTo
@ -85,6 +82,16 @@ class Estimate extends Model
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);
@ -100,16 +107,61 @@ class Estimate extends Model
return $this->hasMany(Estimate::class, 'original_estimate_id');
}
public function calculateTotals(): void
public function workOrders(): HasMany
{
$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();
return $this->hasMany(WorkOrder::class);
}
public function getValidUntilAttribute()
{
return $this->created_at->addDays($this->validity_period_days ?? 30);
}
public function getIsExpiredAttribute()
{
return now()->isAfter($this->valid_until);
}
public function getFormattedValidUntilAttribute()
{
return $this->valid_until->format('M d, Y');
}
public function scopeExpired($query)
{
return $query->whereDate('created_at', '<', now()->subDays(30));
}
public function scopeValid($query)
{
return $query->whereDate('created_at', '>=', now()->subDays(30));
}
public function scopeByStatus($query, $status)
{
return $query->where('status', $status);
}
public function scopeByBranch($query, $branchId)
{
return $query->whereHas('jobCard', function ($q) use ($branchId) {
$q->where('branch_id', $branchId);
});
}
/**
* Get the customer for this estimate (either direct or through job card)
*/
public function getEstimateCustomerAttribute()
{
return $this->customer_id ? $this->customer : $this->jobCard?->customer;
}
/**
* Get the vehicle for this estimate (either direct or through job card)
*/
public function getEstimateVehicleAttribute()
{
return $this->vehicle_id ? $this->vehicle : $this->jobCard?->vehicle;
}
}

View File

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

View File

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

View File

@ -26,6 +26,11 @@ class JobCard extends Model
'status',
'priority',
'service_advisor_id',
'assigned_technician_id',
'assignment_notes',
'assigned_at',
'diagnosis_started_at',
'diagnosis_completed_at',
'notes',
'customer_reported_issues',
'vehicle_condition_notes',
@ -45,15 +50,25 @@ class JobCard extends Model
// Enhanced status constants following the 11-step workflow
public const STATUS_RECEIVED = 'received';
public const STATUS_INSPECTED = 'inspected';
public const STATUS_ASSIGNED_FOR_DIAGNOSIS = 'assigned_for_diagnosis';
public const STATUS_IN_DIAGNOSIS = 'in_diagnosis';
public const STATUS_ESTIMATE_SENT = 'estimate_sent';
public const STATUS_APPROVED = 'approved';
public const STATUS_PARTS_PROCUREMENT = 'parts_procurement';
public const STATUS_IN_PROGRESS = 'in_progress';
public const STATUS_QUALITY_REVIEW_REQUIRED = 'quality_review_required';
public const STATUS_COMPLETED = 'completed';
public const STATUS_DELIVERED = 'delivered';
public static function getStatusOptions(): array
@ -77,12 +92,12 @@ class JobCard extends Model
'arrival_datetime' => 'datetime',
'expected_completion_date' => 'datetime',
'completion_datetime' => 'datetime',
'assigned_at' => 'datetime',
'diagnosis_started_at' => 'datetime',
'diagnosis_completed_at' => 'datetime',
'archived_at' => 'datetime',
'personal_items_removed' => 'boolean',
'photos_taken' => 'boolean',
'mileage_in' => 'integer',
'mileage_out' => 'integer',
'customer_satisfaction_rating' => 'integer',
'incoming_inspection_data' => 'array',
'outgoing_inspection_data' => 'array',
];
@ -90,15 +105,15 @@ class JobCard extends Model
protected static function boot()
{
parent::boot();
static::creating(function ($jobCard) {
if (empty($jobCard->job_card_number)) {
$branchCode = $jobCard->branch_code ?? config('app.default_branch_code', 'ACC');
$nextNumber = static::where('branch_code', $branchCode)
->whereYear('created_at', now()->year)
->count() + 1;
$jobCard->job_card_number = $branchCode . '/' . str_pad($nextNumber, 5, '0', STR_PAD_LEFT);
$jobCard->job_card_number = $branchCode.'/'.str_pad($nextNumber, 5, '0', STR_PAD_LEFT);
}
});
}
@ -118,6 +133,16 @@ class JobCard extends Model
return $this->belongsTo(User::class, 'service_advisor_id');
}
public function assignedTechnician(): BelongsTo
{
return $this->belongsTo(User::class, 'assigned_technician_id');
}
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class, 'branch_code', 'code');
}
public function incomingInspection(): HasOne
{
return $this->hasOne(VehicleInspection::class)->incoming();

View File

@ -31,8 +31,10 @@ class VehicleInspection extends Model
'signature_inspector',
'signature_customer',
'notes',
'additional_comments',
'follow_up_required',
'quality_rating',
'damage_diagram_data',
];
protected $casts = [
@ -41,6 +43,7 @@ class VehicleInspection extends Model
'videos' => 'array',
'recommendations' => 'array',
'discrepancies_found' => 'array',
'damage_diagram_data' => 'array',
'inspection_date' => 'datetime',
'follow_up_required' => 'boolean',
];
@ -78,16 +81,16 @@ class VehicleInspection extends Model
public function compareWithOtherInspection(VehicleInspection $otherInspection): array
{
$differences = [];
if ($this->overall_condition !== $otherInspection->overall_condition) {
$differences['overall_condition'] = [
'before' => $otherInspection->overall_condition,
'after' => $this->overall_condition
'after' => $this->overall_condition,
];
}
// Add more comparison logic as needed
return $differences;
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace App\Policies;
use App\Models\Estimate;
use App\Models\User;
class EstimatePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
// Super admin has global access
if ($user->hasRole('super_admin')) {
return true;
}
// Service coordinators, supervisors, and admins can view all estimates in their branch
return $user->hasAnyRole(['service_coordinator', 'service_supervisor', 'admin'], $user->branch_code);
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Estimate $estimate): bool
{
return false;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
// Super admin has global access
if ($user->hasRole('super_admin')) {
return true;
}
// Service coordinators, supervisors, and admins can create estimates in their branch
return $user->hasAnyRole(['service_coordinator', 'service_supervisor', 'admin'], $user->branch_code);
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Estimate $estimate): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Estimate $estimate): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Estimate $estimate): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Estimate $estimate): bool
{
return false;
}
}

View File

@ -2,14 +2,13 @@
namespace App\Providers;
use App\Models\Estimate;
use App\Models\JobCard;
use App\Models\User;
use App\Policies\EstimatePolicy;
use App\Policies\JobCardPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use App\Models\JobCard;
use App\Models\Customer;
use App\Models\Vehicle;
use App\Models\ServiceOrder;
use App\Models\User;
use App\Policies\JobCardPolicy;
class AuthServiceProvider extends ServiceProvider
{
@ -20,6 +19,7 @@ class AuthServiceProvider extends ServiceProvider
*/
protected $policies = [
JobCard::class => JobCardPolicy::class,
Estimate::class => EstimatePolicy::class,
];
/**
@ -44,7 +44,7 @@ class AuthServiceProvider extends ServiceProvider
return $user->hasAnyPermission([
'reports.view',
'reports.financial',
'reports.operational'
'reports.operational',
], $user->branch_code);
});
@ -54,7 +54,7 @@ class AuthServiceProvider extends ServiceProvider
'inventory.update',
'inventory.delete',
'inventory.stock-movements',
'inventory.purchase-orders'
'inventory.purchase-orders',
], $user->branch_code);
});
@ -62,7 +62,7 @@ class AuthServiceProvider extends ServiceProvider
return $user->hasAnyRole([
'service_supervisor',
'service_coordinator',
'manager'
'manager',
], $user->branch_code);
});

View File

@ -2,12 +2,11 @@
namespace App\Services;
use App\Models\JobCard;
use App\Models\Estimate;
use App\Models\WorkOrder;
use App\Models\Diagnosis;
use App\Models\Estimate;
use App\Models\JobCard;
use App\Models\VehicleInspection;
use App\Models\User;
use App\Models\WorkOrder;
use Illuminate\Support\Facades\DB;
class WorkflowService
@ -24,18 +23,18 @@ class WorkflowService
public function createJobCard(array $data): JobCard
{
return DB::transaction(function () use ($data) {
$jobCard = JobCard::create([
'customer_id' => $data['customer_id'],
'vehicle_id' => $data['vehicle_id'],
'service_advisor_id' => $data['service_advisor_id'],
'branch_code' => $data['branch_code'] ?? config('app.default_branch_code', 'ACC'),
'arrival_datetime' => $data['arrival_datetime'] ?? now(),
'mileage_in' => $data['mileage_in'] ?? null,
'fuel_level_in' => $data['fuel_level_in'] ?? null,
'customer_reported_issues' => $data['customer_reported_issues'] ?? '',
'vehicle_condition_notes' => $data['vehicle_condition_notes'] ?? '',
'keys_location' => $data['keys_location'] ?? 'service_desk',
'personal_items_removed' => $data['personal_items_removed'] ?? false,
$jobCard = JobCard::create([
'customer_id' => $data['customer_id'],
'vehicle_id' => $data['vehicle_id'],
'service_advisor_id' => $data['service_advisor_id'],
'branch_code' => $data['branch_code'] ?? config('app.default_branch_code', 'ACC'),
'arrival_datetime' => $data['arrival_datetime'] ?? now(),
'mileage_in' => $data['mileage_in'] ?? null,
'fuel_level_in' => $data['fuel_level_in'] ?? null,
'customer_reported_issues' => $data['customer_reported_issues'] ?? '',
'vehicle_condition_notes' => $data['vehicle_condition_notes'] ?? '',
'keys_location' => $data['keys_location'] ?? 'service_desk',
'personal_items_removed' => $data['personal_items_removed'] ?? false,
'photos_taken' => $data['photos_taken'] ?? false,
'expected_completion_date' => $data['expected_completion_date'] ?? null,
'priority' => $data['priority'] ?? 'medium',
@ -52,7 +51,7 @@ class WorkflowService
});
}
/**
/**
* STEP 2: Initial Inspection by Service Supervisor
* Perform arrival inspection checklist
*/
@ -66,6 +65,9 @@ class WorkflowService
'incoming_inspection_data' => $inspectionData['inspection_checklist'],
]);
// Send notification that inspection is complete and ready for diagnosis assignment
$this->notificationService->sendInspectionCompletedNotification($jobCard);
return $jobCard->fresh();
}
@ -75,9 +77,14 @@ class WorkflowService
*/
public function assignToServiceCoordinator(JobCard $jobCard, int $serviceCoordinatorId): Diagnosis
{
// Validate workflow progression
// Validate workflow progression - must complete initial inspection first
if ($jobCard->status !== JobCard::STATUS_INSPECTED) {
throw new \InvalidArgumentException('Job card must be inspected before assignment to service coordinator');
throw new \InvalidArgumentException('Initial vehicle inspection must be completed before assignment to service coordinator');
}
// Ensure incoming inspection exists
if (! $jobCard->incomingInspection) {
throw new \InvalidArgumentException('Incoming inspection record is required before proceeding to diagnosis');
}
$diagnosis = Diagnosis::create([
@ -152,7 +159,7 @@ class WorkflowService
'total_amount' => $estimateItems['total_amount'],
'status' => 'sent',
'notes' => $estimateItems['notes'] ?? null,
'valid_until' => $estimateItems['valid_until'] ?? now()->addDays(30),
'validity_period_days' => $estimateItems['validity_period_days'] ?? 30,
]);
// Create estimate line items
@ -170,7 +177,7 @@ class WorkflowService
});
}
/**
/**
* STEP 6: Handle estimate approval and notify team
*/
public function approveEstimate(Estimate $estimate, string $approvalMethod = 'portal'): WorkOrder
@ -201,12 +208,12 @@ class WorkflowService
public function initiatePartsProcurement(Estimate $estimate): array
{
$procurementStatus = [];
foreach ($estimate->lineItems()->where('type', 'part')->get() as $item) {
// Check inventory availability
$part = Part::find($item->part_id);
if (!$part || $part->current_stock < $item->quantity) {
if (! $part || $part->current_stock < $item->quantity) {
// Create purchase order if parts are out of stock
$procurementStatus[] = [
'part_id' => $item->part_id,
@ -215,19 +222,19 @@ class WorkflowService
'available_stock' => $part->current_stock ?? 0,
'shortage' => $item->quantity - ($part->current_stock ?? 0),
'status' => 'procurement_required',
'action' => 'create_purchase_order'
'action' => 'create_purchase_order',
];
} else {
// Reserve parts from inventory
$part->decrement('current_stock', $item->quantity);
$part->increment('reserved_stock', $item->quantity);
$procurementStatus[] = [
'part_id' => $item->part_id,
'part_name' => $item->description,
'required_quantity' => $item->quantity,
'status' => 'reserved',
'action' => 'reserved_from_stock'
'action' => 'reserved_from_stock',
];
}
}
@ -288,20 +295,21 @@ class WorkflowService
$incomingInspection = $jobCard->incomingInspection;
if ($incomingInspection) {
$differences = $this->inspectionService->compareInspections($incomingInspection, $outgoingInspection);
if (!empty($differences)) {
if (! empty($differences)) {
// Generate quality alert for significant discrepancies
$qualityAlert = $this->inspectionService->generateQualityAlert($jobCard, $differences);
if (!empty($qualityAlert)) {
if (! empty($qualityAlert)) {
$this->notificationService->sendQualityAlert($jobCard, $differences);
$outgoingInspection->update([
'discrepancies_found' => $differences,
'follow_up_required' => true,
]);
$jobCard->update(['status' => 'quality_review_required']);
return;
}
}
@ -370,7 +378,7 @@ class WorkflowService
// This would typically move documents to long-term storage
// For now, we'll just mark them as archived
$jobCard->update(['archived_at' => now()]);
// Update related records
$jobCard->inspections()->update(['archived' => true]);
$jobCard->timesheets()->update(['archived' => true]);

View File

@ -20,6 +20,7 @@
"require-dev": {
"barryvdh/laravel-debugbar": "^3.16",
"fakerphp/faker": "^1.23",
"laravel/boost": "^1.0",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.18",
"laravel/sail": "^1.41",

192
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6522b02012f8730cbe38cf382d5a5a45",
"content-hash": "86ce39c32f1ba24deda1b62e36de786b",
"packages": [
{
"name": "brick/math",
@ -6764,6 +6764,135 @@
},
"time": "2025-04-30T06:54:44+00:00"
},
{
"name": "laravel/boost",
"version": "v1.0.14",
"source": {
"type": "git",
"url": "https://github.com/laravel/boost.git",
"reference": "89dcf8a8cac9dcdfc9c452115e215c9fdfa3efcf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/boost/zipball/89dcf8a8cac9dcdfc9c452115e215c9fdfa3efcf",
"reference": "89dcf8a8cac9dcdfc9c452115e215c9fdfa3efcf",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "^7.9",
"illuminate/console": "^10.0|^11.0|^12.0",
"illuminate/contracts": "^10.0|^11.0|^12.0",
"illuminate/routing": "^10.0|^11.0|^12.0",
"illuminate/support": "^10.0|^11.0|^12.0",
"laravel/mcp": "^0.1.0",
"laravel/prompts": "^0.1.9|^0.3",
"laravel/roster": "^0.2",
"php": "^8.1|^8.2"
},
"require-dev": {
"laravel/pint": "^1.14|^1.23",
"mockery/mockery": "^1.6",
"orchestra/testbench": "^8.22.0|^9.0|^10.0",
"pestphp/pest": "^2.0|^3.0",
"phpstan/phpstan": "^2.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Boost\\BoostServiceProvider"
]
},
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Laravel\\Boost\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Laravel Boost accelerates AI-assisted development to generate high-quality, Laravel-specific code.",
"homepage": "https://github.com/laravel/boost",
"keywords": [
"ai",
"dev",
"laravel"
],
"support": {
"issues": "https://github.com/laravel/boost/issues",
"source": "https://github.com/laravel/boost"
},
"time": "2025-08-14T13:42:14+00:00"
},
{
"name": "laravel/mcp",
"version": "v0.1.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/mcp.git",
"reference": "417890c0d8032af9a46a86d16651bbe13946cddf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/mcp/zipball/417890c0d8032af9a46a86d16651bbe13946cddf",
"reference": "417890c0d8032af9a46a86d16651bbe13946cddf",
"shasum": ""
},
"require": {
"illuminate/console": "^10.0|^11.0|^12.0",
"illuminate/contracts": "^10.0|^11.0|^12.0",
"illuminate/http": "^10.0|^11.0|^12.0",
"illuminate/routing": "^10.0|^11.0|^12.0",
"illuminate/support": "^10.0|^11.0|^12.0",
"illuminate/validation": "^10.0|^11.0|^12.0",
"php": "^8.1|^8.2"
},
"require-dev": {
"laravel/pint": "^1.14",
"orchestra/testbench": "^8.22.0|^9.0|^10.0",
"phpstan/phpstan": "^2.0"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp"
},
"providers": [
"Laravel\\Mcp\\Server\\McpServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Mcp\\": "src/",
"Workbench\\App\\": "workbench/app/",
"Laravel\\Mcp\\Tests\\": "tests/",
"Laravel\\Mcp\\Server\\": "src/Server/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "The easiest way to add MCP servers to your Laravel app.",
"homepage": "https://github.com/laravel/mcp",
"keywords": [
"dev",
"laravel",
"mcp"
],
"support": {
"issues": "https://github.com/laravel/mcp/issues",
"source": "https://github.com/laravel/mcp"
},
"time": "2025-08-12T07:09:39+00:00"
},
{
"name": "laravel/pail",
"version": "v1.2.3",
@ -6912,6 +7041,67 @@
},
"time": "2025-07-10T18:09:32+00:00"
},
{
"name": "laravel/roster",
"version": "v0.2.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/roster.git",
"reference": "caeed7609b02c00c3f1efec52812d8d87c5d4096"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/roster/zipball/caeed7609b02c00c3f1efec52812d8d87c5d4096",
"reference": "caeed7609b02c00c3f1efec52812d8d87c5d4096",
"shasum": ""
},
"require": {
"illuminate/console": "^10.0|^11.0|^12.0",
"illuminate/contracts": "^10.0|^11.0|^12.0",
"illuminate/routing": "^10.0|^11.0|^12.0",
"illuminate/support": "^10.0|^11.0|^12.0",
"php": "^8.1|^8.2",
"symfony/yaml": "^6.4|^7.2"
},
"require-dev": {
"laravel/pint": "^1.14",
"mockery/mockery": "^1.6",
"orchestra/testbench": "^8.22.0|^9.0|^10.0",
"pestphp/pest": "^2.0|^3.0",
"phpstan/phpstan": "^2.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Roster\\RosterServiceProvider"
]
},
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Laravel\\Roster\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Detect packages & approaches in use within a Laravel project",
"homepage": "https://github.com/laravel/roster",
"keywords": [
"dev",
"laravel"
],
"support": {
"issues": "https://github.com/laravel/roster/issues",
"source": "https://github.com/laravel/roster"
},
"time": "2025-08-13T15:00:25+00:00"
},
{
"name": "laravel/sail",
"version": "v1.43.1",

View File

@ -0,0 +1,28 @@
<?php
namespace Database\Factories;
use App\Models\JobCard;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Diagnosis>
*/
class DiagnosisFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'job_card_id' => JobCard::factory(),
'service_coordinator_id' => User::factory(),
'diagnostic_findings' => fake()->paragraph(),
'diagnosis_status' => fake()->randomElement(['in_progress', 'completed', 'pending_approval', 'approved']),
];
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Database\Factories;
use App\Models\Diagnosis;
use App\Models\JobCard;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Estimate>
*/
class EstimateFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'estimate_number' => 'MAIN/EST'.str_pad(fake()->unique()->numberBetween(1, 9999), 4, '0', STR_PAD_LEFT),
'job_card_id' => JobCard::factory(),
'diagnosis_id' => Diagnosis::factory(),
'prepared_by_id' => User::factory(),
'labor_cost' => fake()->randomFloat(2, 100, 1000),
'parts_cost' => fake()->randomFloat(2, 50, 500),
'miscellaneous_cost' => fake()->randomFloat(2, 0, 100),
'subtotal' => fake()->randomFloat(2, 200, 1500),
'tax_rate' => 8.25,
'tax_amount' => fake()->randomFloat(2, 10, 100),
'discount_amount' => fake()->randomFloat(2, 0, 50),
'total_amount' => fake()->randomFloat(2, 210, 1600),
'terms_and_conditions' => fake()->paragraph(),
'validity_period_days' => fake()->numberBetween(7, 90),
'notes' => fake()->optional()->paragraph(),
'internal_notes' => fake()->optional()->paragraph(),
];
}
}

View File

@ -0,0 +1,28 @@
<?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::table('vehicle_inspections', function (Blueprint $table) {
$table->text('additional_comments')->nullable()->after('notes');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('vehicle_inspections', function (Blueprint $table) {
$table->dropColumn('additional_comments');
});
}
};

View File

@ -0,0 +1,29 @@
<?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::table('vehicle_inspections', function (Blueprint $table) {
// Add vehicle damage diagram field - this is the only missing field
$table->json('damage_diagram_data')->nullable()->after('quality_rating');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('vehicle_inspections', function (Blueprint $table) {
$table->dropColumn('damage_diagram_data');
});
}
};

View File

@ -0,0 +1,28 @@
<?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::table('vehicle_inspections', function (Blueprint $table) {
$table->unsignedBigInteger('service_order_id')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('vehicle_inspections', function (Blueprint $table) {
$table->unsignedBigInteger('service_order_id')->nullable(false)->change();
});
}
};

View File

@ -0,0 +1,31 @@
<?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
{
// For SQLite compatibility, we'll use a different approach
Schema::table('job_cards', function (Blueprint $table) {
// In SQLite, we can't modify enum directly, so we'll just add a comment
// The model validation will handle the constraint
$table->string('status')->default('received')->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('job_cards', function (Blueprint $table) {
$table->string('status')->default('received')->change();
});
}
};

View File

@ -0,0 +1,39 @@
<?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::table('job_cards', function (Blueprint $table) {
$table->foreignId('assigned_technician_id')->nullable()->after('service_advisor_id')->constrained('users')->onDelete('set null');
$table->text('assignment_notes')->nullable()->after('notes');
$table->timestamp('assigned_at')->nullable()->after('assignment_notes');
$table->timestamp('diagnosis_started_at')->nullable()->after('assigned_at');
$table->timestamp('diagnosis_completed_at')->nullable()->after('diagnosis_started_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('job_cards', function (Blueprint $table) {
$table->dropForeign(['assigned_technician_id']);
$table->dropColumn([
'assigned_technician_id',
'assignment_notes',
'assigned_at',
'diagnosis_started_at',
'diagnosis_completed_at',
]);
});
}
};

View File

@ -0,0 +1,40 @@
<?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::table('estimates', function (Blueprint $table) {
// Add customer and vehicle foreign keys for standalone estimates
$table->foreignId('customer_id')->nullable()->constrained()->onDelete('cascade');
$table->foreignId('vehicle_id')->nullable()->constrained()->onDelete('cascade');
// Make job_card_id and diagnosis_id nullable for standalone estimates
$table->foreignId('job_card_id')->nullable()->change();
$table->foreignId('diagnosis_id')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('estimates', function (Blueprint $table) {
$table->dropForeign(['customer_id']);
$table->dropForeign(['vehicle_id']);
$table->dropColumn(['customer_id', 'vehicle_id']);
// Restore job_card_id and diagnosis_id as required
$table->foreignId('job_card_id')->nullable(false)->change();
$table->foreignId('diagnosis_id')->nullable(false)->change();
});
}
};

View File

@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" width="800" height="600">
<!-- Left Side View -->
<g id="left-side" transform="translate(50, 50)">
<rect x="0" y="50" width="200" height="80" fill="#f0f0f0" stroke="#333" stroke-width="2" id="left-body"/>
<circle cx="30" cy="140" r="25" fill="#ddd" stroke="#333" stroke-width="2" id="left-front-wheel"/>
<circle cx="170" cy="140" r="25" fill="#ddd" stroke="#333" stroke-width="2" id="left-rear-wheel"/>
<rect x="0" y="30" width="40" height="20" fill="#f0f0f0" stroke="#333" stroke-width="2" id="left-front"/>
<rect x="160" y="30" width="40" height="20" fill="#f0f0f0" stroke="#333" stroke-width="2" id="left-rear"/>
<text x="100" y="20" text-anchor="middle" class="vehicle-label">Left Side</text>
</g>
<!-- Top View -->
<g id="top-view" transform="translate(350, 50)">
<rect x="0" y="0" width="80" height="200" fill="#f0f0f0" stroke="#333" stroke-width="2" id="top-body"/>
<rect x="-10" y="30" width="20" height="25" fill="#ddd" stroke="#333" stroke-width="2" id="top-left-front-wheel"/>
<rect x="70" y="30" width="20" height="25" fill="#ddd" stroke="#333" stroke-width="2" id="top-right-front-wheel"/>
<rect x="-10" y="145" width="20" height="25" fill="#ddd" stroke="#333" stroke-width="2" id="top-left-rear-wheel"/>
<rect x="70" y="145" width="20" height="25" fill="#ddd" stroke="#333" stroke-width="2" id="top-right-rear-wheel"/>
<text x="40" y="-10" text-anchor="middle" class="vehicle-label">Top View</text>
</g>
<!-- Right Side View -->
<g id="right-side" transform="translate(500, 50)">
<rect x="0" y="50" width="200" height="80" fill="#f0f0f0" stroke="#333" stroke-width="2" id="right-body"/>
<circle cx="30" cy="140" r="25" fill="#ddd" stroke="#333" stroke-width="2" id="right-front-wheel"/>
<circle cx="170" cy="140" r="25" fill="#ddd" stroke="#333" stroke-width="2" id="right-rear-wheel"/>
<rect x="0" y="30" width="40" height="20" fill="#f0f0f0" stroke="#333" stroke-width="2" id="right-front"/>
<rect x="160" y="30" width="40" height="20" fill="#f0f0f0" stroke="#333" stroke-width="2" id="right-rear"/>
<text x="100" y="20" text-anchor="middle" class="vehicle-label">Right Side</text>
</g>
<!-- Front View -->
<g id="front-view" transform="translate(200, 300)">
<rect x="0" y="0" width="100" height="80" fill="#f0f0f0" stroke="#333" stroke-width="2" id="front-body"/>
<rect x="-10" y="70" width="25" height="25" fill="#ddd" stroke="#333" stroke-width="2" id="front-left-wheel"/>
<rect x="85" y="70" width="25" height="25" fill="#ddd" stroke="#333" stroke-width="2" id="front-right-wheel"/>
<rect x="20" y="-10" width="60" height="10" fill="#f0f0f0" stroke="#333" stroke-width="2" id="front-windshield"/>
<text x="50" y="-20" text-anchor="middle" class="vehicle-label">Front View</text>
</g>
<!-- Rear View -->
<g id="rear-view" transform="translate(400, 300)">
<rect x="0" y="0" width="100" height="80" fill="#f0f0f0" stroke="#333" stroke-width="2" id="rear-body"/>
<rect x="-10" y="70" width="25" height="25" fill="#ddd" stroke="#333" stroke-width="2" id="rear-left-wheel"/>
<rect x="85" y="70" width="25" height="25" fill="#ddd" stroke="#333" stroke-width="2" id="rear-right-wheel"/>
<rect x="20" y="80" width="60" height="10" fill="#f0f0f0" stroke="#333" stroke-width="2" id="rear-bumper"/>
<text x="50" y="-10" text-anchor="middle" class="vehicle-label">Rear View</text>
</g>
<style>
.vehicle-label {
font-family: Arial, sans-serif;
font-size: 14px;
font-weight: bold;
fill: #333;
}
.damage-marker {
fill: red;
stroke: darkred;
stroke-width: 2;
opacity: 0.7;
}
.clickable-area {
cursor: pointer;
opacity: 0;
}
.clickable-area:hover {
opacity: 0.1;
fill: blue;
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vehicle Inspection Report</title>
<style>
@media print {
@page {
margin: 0.5in;
size: A4;
}
body {
-webkit-print-color-adjust: exact;
margin: 0;
padding: 20px;
font-size: 12px;
}
.no-print { display: none !important; }
.page-break { page-break-before: always; }
table { page-break-inside: avoid; }
.avoid-break { page-break-inside: avoid; }
}
@media screen {
body {
background-color: #f5f5f5;
padding: 20px;
}
.print-container {
background: white;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
margin: 0 auto;
padding: 40px;
max-width: 210mm; /* A4 width */
min-height: 297mm; /* A4 height */
}
}
.print-header {
text-align: center;
border-bottom: 3px solid #333;
padding-bottom: 20px;
margin-bottom: 30px;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
margin-bottom: 25px;
}
.info-section {
border: 1px solid #ddd;
padding: 12px;
border-radius: 5px;
}
.info-section h3 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: bold;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 4px;
}
.info-row {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
padding: 3px 0;
font-size: 11px;
}
.info-row:not(:last-child) {
border-bottom: 1px dotted #ddd;
}
.label {
font-weight: 600;
color: #555;
}
.value {
color: #333;
}
.checklist-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 30px;
align-items: start;
}
.checklist-section {
border: 1px solid #ddd;
padding: 12px;
border-radius: 5px;
break-inside: avoid;
}
}
.checklist-section h4 {
margin: 0 0 12px 0;
font-size: 13px;
font-weight: bold;
color: #333;
background: #f8f9fa;
padding: 6px;
border-radius: 3px;
text-align: center;
}
.checklist-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 3px 0;
border-bottom: 1px dotted #eee;
font-size: 10px;
}
.checklist-item:last-child {
border-bottom: none;
}
.checklist-item span:first-child {
flex: 1;
margin-right: 8px;
}
.status-badge {
padding: 1px 6px;
border-radius: 3px;
font-size: 9px;
font-weight: bold;
white-space: nowrap;
min-width: 35px;
text-align: center;
}
.status-pass { background: #d4edda; color: #155724; }
.status-fail { background: #f8d7da; color: #721c24; }
.status-na { background: #e2e3e5; color: #6c757d; }
.status-good { background: #d4edda; color: #155724; }
.status-poor { background: #f8d7da; color: #721c24; }
.status-fair { background: #fff3cd; color: #856404; }
.notes-section {
margin-top: 30px;
page-break-inside: avoid;
}
.note-box {
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 15px;
border-radius: 5px;
}
.note-box h5 {
margin: 0 0 10px 0;
font-size: 13px;
font-weight: bold;
}
.damage-report {
margin-top: 30px;
page-break-inside: avoid;
}
.damage-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 15px;
}
.damage-item {
border: 1px solid #ddd;
padding: 10px;
border-radius: 5px;
text-align: center;
}
.print-actions {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
</style>
</head>
<body>
<div class="print-actions no-print">
<button onclick="window.print()" style="background: #2563eb; color: white; padding: 8px 16px; border: none; border-radius: 4px; margin-right: 8px; cursor: pointer;">
🖨️ Print
</button>
<button onclick="window.close()" style="background: #6b7280; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;">
Close
</button>
</div>
<div class="print-container">
{{ $slot }}
</div>
</body>
</html>

View File

@ -1,3 +1,196 @@
<div>
{{-- Stop trying to control. --}}
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<!-- Page Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100">Diagnoses</h1>
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
Manage vehicle diagnostic records and analysis
</p>
</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">
<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
</button>
</div>
</div>
</div>
<!-- Filters -->
<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>
<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">
</div>
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Status</label>
<select wire:model.live="statusFilter"
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">
<option value="">All Statuses</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
<option value="pending_approval">Pending Approval</option>
<option value="approved">Approved</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Priority</label>
<select wire:model.live="priorityFilter"
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">
<option value="">All Priorities</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Date Range</label>
<input type="date" wire:model.live="dateFrom"
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">
</div>
</div>
</div>
</div>
<!-- Diagnoses List -->
<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">
Diagnosis Records
<span class="text-sm font-normal text-zinc-500 dark:text-zinc-400 ml-2">({{ $diagnoses->total() }} total)</span>
</h2>
</div>
@if($diagnoses->count() > 0)
<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-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Job Card</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Customer</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Vehicle</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Priority</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Technician</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-200 dark:divide-zinc-700">
@foreach($diagnoses as $diagnosis)
<tr class="hover:bg-zinc-50 dark:hover:bg-zinc-700/50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{{ $diagnosis->jobCard->job_card_number }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-zinc-900 dark:text-zinc-100">{{ $diagnosis->jobCard->customer->name }}</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">{{ $diagnosis->jobCard->customer->phone }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-zinc-900 dark:text-zinc-100">
{{ $diagnosis->jobCard->vehicle->year }} {{ $diagnosis->jobCard->vehicle->make }} {{ $diagnosis->jobCard->vehicle->model }}
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400">{{ $diagnosis->jobCard->vehicle->license_plate }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full
@switch($diagnosis->diagnosis_status)
@case('in_progress')
bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200
@break
@case('completed')
bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200
@break
@case('pending_approval')
bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200
@break
@case('approved')
bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200
@break
@default
bg-zinc-100 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200
@endswitch
">
{{ str_replace('_', ' ', $diagnosis->diagnosis_status) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full
@switch($diagnosis->priority_level)
@case('low')
bg-zinc-100 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200
@break
@case('medium')
bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200
@break
@case('high')
bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200
@break
@case('critical')
bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200
@break
@endswitch
">
{{ ucfirst($diagnosis->priority_level) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-900 dark:text-zinc-100">
{{ $diagnosis->serviceCoordinator->name }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">
{{ $diagnosis->diagnosis_date?->format('M j, Y') ?? $diagnosis->created_at->format('M j, Y') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
<a href="{{ route('diagnosis.show', $diagnosis) }}" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">
View
</a>
<a href="{{ route('diagnosis.edit', $diagnosis) }}" class="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-300">
Edit
</a>
@if(!$diagnosis->estimate)
<a href="{{ route('estimates.create', $diagnosis) }}" class="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300">
Estimate
</a>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="px-6 py-4 border-t border-zinc-200 dark:border-zinc-700">
{{ $diagnoses->links() }}
</div>
@else
<div class="p-12 text-center">
<svg class="mx-auto h-12 w-12 text-zinc-400 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"/>
</svg>
<h3 class="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-2">No diagnoses found</h3>
<p class="text-sm text-zinc-500 dark:text-zinc-400 mb-4">
@if($search || $statusFilter || $priorityFilter || $dateFrom)
No diagnoses match your current filters.
@else
Get started by creating a diagnosis from a job card.
@endif
</p>
@if($search || $statusFilter || $priorityFilter || $dateFrom)
<button wire:click="clearFilters" 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">
Clear Filters
</button>
@endif
</div>
@endif
</div>
</div>

View File

@ -1,3 +1,210 @@
<div>
{{-- Success is as dangerous as failure. --}}
<div class="max-w-6xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<!-- Page Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100">Diagnosis Details</h1>
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
Diagnostic analysis for Job Card #{{ $diagnosis->jobCard->job_card_number }}
</p>
</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">
<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">
<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
</a>
</div>
</div>
</div>
<!-- Vehicle & Job Information -->
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 shadow-sm mb-8">
<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">Vehicle Information</h2>
</div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="space-y-1">
<label class="text-sm font-medium text-zinc-700 dark:text-zinc-300">Customer</label>
<p class="text-sm text-zinc-900 dark:text-zinc-100">{{ $diagnosis->jobCard->customer->name }}</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $diagnosis->jobCard->customer->phone }}</p>
</div>
<div class="space-y-1">
<label class="text-sm font-medium text-zinc-700 dark:text-zinc-300">Vehicle</label>
<p class="text-sm text-zinc-900 dark:text-zinc-100">{{ $diagnosis->jobCard->vehicle->year }} {{ $diagnosis->jobCard->vehicle->make }} {{ $diagnosis->jobCard->vehicle->model }}</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $diagnosis->jobCard->vehicle->license_plate }}</p>
</div>
<div class="space-y-1">
<label class="text-sm font-medium text-zinc-700 dark:text-zinc-300">Service Coordinator</label>
<p class="text-sm text-zinc-900 dark:text-zinc-100">{{ $diagnosis->serviceCoordinator->name }}</p>
</div>
<div class="space-y-1">
<label class="text-sm font-medium text-zinc-700 dark:text-zinc-300">Diagnosis Date</label>
<p class="text-sm text-zinc-900 dark:text-zinc-100">{{ $diagnosis->diagnosis_date?->format('M j, Y g:i A') ?? 'Not set' }}</p>
</div>
</div>
</div>
</div>
<!-- Status and Priority -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 shadow-sm p-6">
<div class="text-center">
<div class="mx-auto h-12 w-12 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mb-4">
<svg class="h-6 w-6 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>
<h3 class="text-lg font-medium text-zinc-900 dark:text-zinc-100">Status</h3>
<p class="text-sm text-zinc-500 dark:text-zinc-400 capitalize">{{ str_replace('_', ' ', $diagnosis->diagnosis_status) }}</p>
</div>
</div>
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 shadow-sm p-6">
<div class="text-center">
<div class="mx-auto h-12 w-12 rounded-full bg-yellow-100 dark:bg-yellow-900 flex items-center justify-center mb-4">
<svg class="h-6 w-6 text-yellow-600 dark:text-yellow-400" 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 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
</div>
<h3 class="text-lg font-medium text-zinc-900 dark:text-zinc-100">Priority</h3>
<p class="text-sm text-zinc-500 dark:text-zinc-400 capitalize">{{ $diagnosis->priority_level }}</p>
</div>
</div>
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 shadow-sm p-6">
<div class="text-center">
<div class="mx-auto h-12 w-12 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center mb-4">
<svg class="h-6 w-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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<h3 class="text-lg font-medium text-zinc-900 dark:text-zinc-100">Estimated Time</h3>
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ $diagnosis->estimated_repair_time }} hours</p>
</div>
</div>
</div>
<!-- Diagnostic Analysis -->
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 shadow-sm mb-8">
<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">Diagnostic Analysis</h2>
</div>
<div class="p-6 space-y-6">
@if($diagnosis->customer_reported_issues)
<div>
<h3 class="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Customer Reported Issues</h3>
<p class="text-sm text-zinc-900 dark:text-zinc-100 bg-zinc-50 dark:bg-zinc-900 p-3 rounded-lg">{{ $diagnosis->customer_reported_issues }}</p>
</div>
@endif
<div>
<h3 class="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Diagnostic Findings</h3>
<p class="text-sm text-zinc-900 dark:text-zinc-100 bg-zinc-50 dark:bg-zinc-900 p-3 rounded-lg">{{ $diagnosis->diagnostic_findings }}</p>
</div>
<div>
<h3 class="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Root Cause Analysis</h3>
<p class="text-sm text-zinc-900 dark:text-zinc-100 bg-zinc-50 dark:bg-zinc-900 p-3 rounded-lg">{{ $diagnosis->root_cause_analysis }}</p>
</div>
<div>
<h3 class="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Recommended Repairs</h3>
<p class="text-sm text-zinc-900 dark:text-zinc-100 bg-zinc-50 dark:bg-zinc-900 p-3 rounded-lg">{{ $diagnosis->recommended_repairs }}</p>
</div>
@if($diagnosis->additional_issues_found)
<div>
<h3 class="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Additional Issues Found</h3>
<p class="text-sm text-zinc-900 dark:text-zinc-100 bg-amber-50 dark:bg-amber-900/20 p-3 rounded-lg border border-amber-200 dark:border-amber-800">{{ $diagnosis->additional_issues_found }}</p>
</div>
@endif
@if($diagnosis->safety_concerns)
<div>
<h3 class="text-sm font-medium text-red-700 dark:text-red-300 mb-2">Safety Concerns</h3>
<p class="text-sm text-red-900 dark:text-red-100 bg-red-50 dark:bg-red-900/20 p-3 rounded-lg border border-red-200 dark:border-red-800">{{ $diagnosis->safety_concerns }}</p>
</div>
@endif
</div>
</div>
<!-- Parts and Labor -->
@if($diagnosis->parts_required || $diagnosis->labor_operations)
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
@if($diagnosis->parts_required)
<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">Parts Required</h2>
</div>
<div class="p-6">
<div class="space-y-3">
@foreach($diagnosis->parts_required as $part)
<div class="p-3 bg-zinc-50 dark:bg-zinc-900 rounded-lg">
<div class="flex justify-between items-start">
<div>
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $part['name'] ?? 'Part Name' }}</p>
@if(isset($part['part_number']))
<p class="text-xs text-zinc-500 dark:text-zinc-400">Part #: {{ $part['part_number'] }}</p>
@endif
</div>
@if(isset($part['quantity']))
<span class="text-sm text-zinc-500 dark:text-zinc-400">Qty: {{ $part['quantity'] }}</span>
@endif
</div>
</div>
@endforeach
</div>
</div>
</div>
@endif
@if($diagnosis->labor_operations)
<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">Labor Operations</h2>
</div>
<div class="p-6">
<div class="space-y-3">
@foreach($diagnosis->labor_operations as $operation)
<div class="p-3 bg-zinc-50 dark:bg-zinc-900 rounded-lg">
<div class="flex justify-between items-start">
<div>
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $operation['operation'] ?? 'Operation' }}</p>
@if(isset($operation['description']))
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $operation['description'] }}</p>
@endif
</div>
@if(isset($operation['time']))
<span class="text-sm text-zinc-500 dark:text-zinc-400">{{ $operation['time'] }}h</span>
@endif
</div>
</div>
@endforeach
</div>
</div>
</div>
@endif
</div>
@endif
@if($diagnosis->notes)
<!-- Notes -->
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 shadow-sm mb-8">
<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">Additional Notes</h2>
</div>
<div class="p-6">
<p class="text-sm text-zinc-900 dark:text-zinc-100">{{ $diagnosis->notes }}</p>
</div>
</div>
@endif
</div>

View File

@ -0,0 +1,340 @@
<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">Create New Estimate</h1>
<p class="text-gray-600 dark:text-gray-400">Create a standalone estimate for parts sales or services</p>
</div>
<div class="flex space-x-3 mt-4 lg:mt-0">
<a href="{{ route('estimates.index') }}" class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg 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-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="M15 19l-7-7 7-7"></path>
</svg>
Back to Estimates
</a>
</div>
</div>
</div>
<!-- Customer and Vehicle Selection -->
<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 & Vehicle Information</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<flux:field>
<flux:label>Customer</flux:label>
<flux:select wire:model.live="customerId" placeholder="Select a customer...">
<option value="">Select a customer...</option>
@foreach($customers as $customer)
<option value="{{ $customer->id }}">{{ $customer->name }} - {{ $customer->email }}</option>
@endforeach
</flux:select>
<flux:error name="customerId" />
</flux:field>
@if($selectedCustomer)
<div class="mt-2 p-3 bg-blue-50 dark:bg-blue-900/30 rounded-lg">
<div class="text-sm text-blue-800 dark:text-blue-200">
<p><strong>{{ $selectedCustomer->name }}</strong></p>
<p>{{ $selectedCustomer->email }}</p>
<p>{{ $selectedCustomer->phone }}</p>
</div>
</div>
@endif
</div>
<div>
<flux:field>
<flux:label>Vehicle</flux:label>
<flux:select wire:model.live="vehicleId" placeholder="Select a vehicle..." :disabled="!$customerId">
<option value="">Select a vehicle...</option>
@foreach($customerVehicles as $vehicle)
<option value="{{ $vehicle->id }}">
{{ $vehicle->year }} {{ $vehicle->make }} {{ $vehicle->model }} - {{ $vehicle->license_plate }}
</option>
@endforeach
</flux:select>
<flux:error name="vehicleId" />
</flux:field>
@if($selectedVehicle)
<div class="mt-2 p-3 bg-green-50 dark:bg-green-900/30 rounded-lg">
<div class="text-sm text-green-800 dark:text-green-200">
<p><strong>{{ $selectedVehicle->year }} {{ $selectedVehicle->make }} {{ $selectedVehicle->model }}</strong></p>
<p>License Plate: {{ $selectedVehicle->license_plate }}</p>
<p>VIN: {{ $selectedVehicle->vin }}</p>
</div>
</div>
@endif
</div>
</div>
</div>
<!-- Line Items -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<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">
<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 class="space-y-4">
@foreach($lineItems as $index => $item)
<div class="grid grid-cols-12 gap-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg" wire:key="item-{{ $index }}">
<div class="col-span-2">
<flux:field>
<flux:label>Type</flux:label>
<flux:select wire:model.live="lineItems.{{ $index }}.type">
<option value="parts">Parts</option>
<option value="labour">Labour</option>
<option value="miscellaneous">Miscellaneous</option>
</flux:select>
</flux:field>
</div>
<div class="col-span-4">
@if($item['type'] === 'parts')
<flux:field>
<flux:label>Parts Search</flux:label>
<div class="relative">
@if(!empty($item['part_id']))
<!-- Selected Part Display -->
<div class="flex items-center justify-between p-2 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 rounded-lg">
<div class="flex-1">
<div class="text-sm font-medium text-green-800 dark:text-green-200">
{{ $item['part_number'] }} - {{ $item['description'] }}
</div>
@if(isset($item['stock_available']))
<div class="text-xs text-green-600 dark:text-green-300">
Stock: {{ $item['stock_available'] }} available
</div>
@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">
<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>
@else
<!-- Parts Search Input -->
<flux:input
wire:model.live.debounce.300ms="partSearch"
wire:keyup="searchParts($event.target.value)"
placeholder="Search parts by name, part number, or description..."
autocomplete="off"
/>
@if($showPartsDropdown && count($availableParts) > 0)
<div class="absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-y-auto">
@foreach($availableParts as $part)
<button
wire:click="selectPart({{ $index }}, {{ $part->id }})"
type="button"
class="w-full px-4 py-3 text-left hover:bg-gray-100 dark:hover:bg-gray-700 border-b border-gray-200 dark:border-gray-600 last:border-b-0"
>
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="font-medium text-gray-900 dark:text-gray-100">
{{ $part->part_number }} - {{ $part->name }}
</div>
<div class="text-sm text-gray-600 dark:text-gray-400">
{{ $part->description }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-500 mt-1">
Stock: {{ $part->quantity_on_hand }} | Price: ${{ number_format($part->sell_price, 2) }}
</div>
</div>
</div>
</button>
@endforeach
</div>
@elseif($showPartsDropdown && strlen($partSearch) >= 2)
<div class="absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg p-4">
<div class="text-gray-500 dark:text-gray-400 text-sm">
No parts found matching "{{ $partSearch }}"
</div>
</div>
@endif
@endif
</div>
</flux:field>
@else
<flux:field>
<flux:label>Description</flux:label>
<flux:input wire:model.live="lineItems.{{ $index }}.description" placeholder="Item description..." />
</flux:field>
@endif
</div>
<div class="col-span-2">
<flux:field>
<flux:label>Quantity</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
/>
@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
</flux:field>
</div>
<div class="col-span-2">
<flux:field>
<flux:label>Unit Price</flux:label>
<flux:input wire:model.live="lineItems.{{ $index }}.unit_price" type="number" step="0.01" min="0" />
</flux:field>
</div>
<div class="col-span-1 flex items-end">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
${{ number_format($item['subtotal'] ?? 0, 2) }}
</div>
</div>
<div class="col-span-1 flex items-end">
@if(count($lineItems) > 1)
<button wire:click="removeLineItem({{ $index }})" type="button" class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 mb-2">
<svg class="w-5 h-5" 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>
@endif
</div>
</div>
@error("lineItems.{$index}.type")
<p class="text-red-600 text-sm">{{ $message }}</p>
@enderror
@error("lineItems.{$index}.description")
<p class="text-red-600 text-sm">{{ $message }}</p>
@enderror
@error("lineItems.{$index}.quantity")
<p class="text-red-600 text-sm">{{ $message }}</p>
@enderror
@error("lineItems.{$index}.unit_price")
<p class="text-red-600 text-sm">{{ $message }}</p>
@enderror
@endforeach
</div>
</div>
<!-- Estimate Details & Totals -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Estimate Details -->
<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 Details</h3>
<div class="space-y-4">
<div>
<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>
</div>
<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:error name="tax_rate" />
</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:error name="discount_amount" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Customer Notes</flux:label>
<flux:textarea wire:model="notes" placeholder="Notes visible to customer..." rows="3" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Internal Notes</flux:label>
<flux:textarea wire:model="internal_notes" placeholder="Internal notes (not visible to customer)..." rows="3" />
</flux:field>
</div>
</div>
</div>
<!-- Totals -->
<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">Totals</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">-${{ 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-600 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>
</div>
<!-- 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 and Conditions</h3>
<flux:field>
<flux:textarea wire:model="terms_and_conditions" rows="4" placeholder="Enter terms and conditions..." />
<flux:error name="terms_and_conditions" />
</flux:field>
</div>
<!-- Action Buttons -->
<div class="flex justify-end space-x-4">
<a href="{{ route('estimates.index') }}" class="inline-flex items-center px-6 py-3 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
Cancel
</a>
<button wire:click="save" type="button" class="inline-flex items-center px-6 py-3 bg-accent hover:bg-accent-content text-accent-foreground font-medium rounded-lg 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="M5 13l4 4L19 7"></path>
</svg>
Create Estimate
</button>
</div>
</div>

View File

@ -1,3 +1,217 @@
<div>
{{-- The whole world belongs to you. --}}
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">Create Estimate</h1>
<p class="mt-1 text-gray-600 dark:text-gray-400">
Create a detailed estimate for {{ $diagnosis->jobCard->customer->name }} -
{{ $diagnosis->jobCard->vehicle->year }} {{ $diagnosis->jobCard->vehicle->make }} {{ $diagnosis->jobCard->vehicle->model }}
</p>
</div>
<div class="flex space-x-3">
<flux:button variant="ghost" href="{{ route('estimates.index') }}">
Cancel
</flux:button>
<flux:button wire:click="save" variant="primary">
Save Estimate
</flux:button>
</div>
</div>
<!-- Job Card 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">Job Information</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<flux:field>
<flux:label>Job Card Number</flux:label>
<flux:input value="{{ $diagnosis->jobCard->job_number }}" readonly />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Customer</flux:label>
<flux:input value="{{ $diagnosis->jobCard->customer->name }}" readonly />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Vehicle</flux:label>
<flux:input value="{{ $diagnosis->jobCard->vehicle->year }} {{ $diagnosis->jobCard->vehicle->make }} {{ $diagnosis->jobCard->vehicle->model }}" readonly />
</flux:field>
</div>
</div>
</div>
<!-- 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">Estimate Settings</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<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>
</div>
<div>
<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>
</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:error name="discount_amount" />
</flux:field>
</div>
</div>
</div>
<!-- Line Items -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="px-6 py-4 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="addLineItem" 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="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Add Item
</flux:button>
</div>
</div>
<div class="overflow-x-auto">
<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>
<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">
@forelse($lineItems as $index => $item)
<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="parts">Parts</option>
<option value="miscellaneous">Miscellaneous</option>
</flux:select>
</td>
<td class="px-6 py-4">
<flux:input wire:model.live="lineItems.{{ $index }}.description" placeholder="Item description" class="w-full" />
</td>
<td class="px-6 py-4 whitespace-nowrap">
<flux:input wire:model.live="lineItems.{{ $index }}.quantity" type="number" step="0.01" min="0" class="w-24" />
</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-32" />
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">
${{ number_format($item['total_amount'] ?? 0, 2) }}
</span>
</td>
<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">
<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>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
No line items added yet. Click "Add Item" to get started.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<!-- Totals Section -->
<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 Totals</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<!-- Terms and Conditions -->
<flux:field>
<flux:label>Terms and Conditions</flux:label>
<flux:textarea wire:model="terms_and_conditions" rows="4" placeholder="Enter terms and conditions..." />
<flux:error name="terms_and_conditions" />
</flux:field>
</div>
<div>
<!-- Financial Summary -->
<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>
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">Discount:</span>
<span class="font-medium text-gray-900 dark:text-gray-100">-${{ number_format($discount_amount, 2) }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">Tax ({{ number_format($tax_rate, 2) }}%):</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-xl font-bold text-accent">${{ number_format($total_amount, 2) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Notes Section -->
<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="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<flux:field>
<flux:label>Customer Notes</flux:label>
<flux:textarea wire:model="notes" rows="4" placeholder="Notes visible to customer..." />
<flux:error name="notes" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Internal Notes</flux:label>
<flux:textarea wire:model="internal_notes" rows="4" placeholder="Internal notes (not visible to customer)..." />
<flux:error name="internal_notes" />
</flux:field>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-end space-x-3">
<flux:button variant="ghost" href="{{ route('estimates.index') }}">
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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Create Estimate
</flux:button>
</div>
</div>

View File

@ -1,3 +1,410 @@
<div>
{{-- Do your work, then step back. --}}
<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>

View File

@ -1,134 +1,574 @@
<div>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div class="space-y-6">
<!-- Advanced Header with Stats Dashboard -->
<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-zinc-900 dark:text-zinc-100">Estimates</h1>
<p class="text-zinc-600 dark:text-zinc-400">Manage service estimates and quotes</p>
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">Estimates</h1>
<p class="text-gray-600 dark:text-gray-400">Manage service estimates, quotes, and customer approvals</p>
</div>
<div class="flex space-x-3 mt-4 lg:mt-0">
<a href="{{ route('estimates.create-standalone') }}" class="inline-flex items-center px-4 py-2 bg-accent hover:bg-accent-content text-accent-foreground font-medium rounded-lg 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="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>
New Estimate
</a>
@if($availableDiagnoses->count() > 0)
<div class="relative" x-data="{ open: false }">
<button @click="open = !open" class="inline-flex items-center px-4 py-2 border border-accent text-accent bg-white dark:bg-gray-800 hover:bg-accent/10 dark:hover:bg-accent/20 font-medium rounded-lg transition-all duration-200">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<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">
<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>
<div x-show="open" @click.away="open = false" x-transition class="absolute top-full left-0 mt-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-10">
<div class="p-3">
<p class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Create estimate from diagnosis:</p>
<div class="space-y-2 max-h-40 overflow-y-auto">
@foreach($availableDiagnoses as $diagnosis)
<a href="{{ route('estimates.create', $diagnosis) }}" class="block p-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $diagnosis->jobCard?->customer?->name ?? 'Unknown Customer' }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ $diagnosis->jobCard?->vehicle?->year }} {{ $diagnosis->jobCard?->vehicle?->make }} {{ $diagnosis->jobCard?->vehicle?->model }}
</div>
</a>
@endforeach
</div>
</div>
</div>
</div>
@endif
<button wire:click="toggleBulkMode" class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg 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-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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
{{ $bulkMode ? 'Exit Bulk Mode' : 'Bulk Actions' }}
</button>
</div>
</div>
</div>
<!-- Stats Dashboard -->
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-4">
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow">
<div class="flex items-center">
<div class="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Total</p>
<p class="text-lg font-bold text-gray-900 dark:text-gray-100">{{ number_format($stats['total']) }}</p>
</div>
</div>
</div>
<!-- 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>
<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 estimates, 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">
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow">
<div class="flex items-center">
<div class="p-2 bg-gray-100 dark:bg-gray-700 rounded-lg">
<svg class="w-6 h-6 text-gray-600 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M4 4a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2H4zm0 2h12v8H4V6z"></path>
</svg>
</div>
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Status</label>
<select wire:model.live="statusFilter" 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">
<div class="ml-3">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Draft</p>
<p class="text-lg font-bold text-gray-700">{{ number_format($stats['draft']) }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow">
<div class="flex items-center">
<div class="p-2 bg-blue-100 dark:bg-blue-900/50 rounded-lg">
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"></path>
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Sent</p>
<p class="text-lg font-bold text-blue-600 dark:text-blue-400">{{ number_format($stats['sent']) }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow">
<div class="flex items-center">
<div class="p-2 bg-green-100 dark:bg-green-900/50 rounded-lg">
<svg class="w-6 h-6 text-green-600 dark:text-green-400" 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>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Approved</p>
<p class="text-lg font-bold text-green-600 dark:text-green-400">{{ number_format($stats['approved']) }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow">
<div class="flex items-center">
<div class="p-2 bg-yellow-100 dark:bg-yellow-900/50 rounded-lg">
<svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Pending</p>
<p class="text-lg font-bold text-yellow-600 dark:text-yellow-400">{{ number_format($stats['pending']) }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow">
<div class="flex items-center">
<div class="p-2 bg-red-100 dark:bg-red-900/50 rounded-lg">
<svg class="w-6 h-6 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Expired</p>
<p class="text-lg font-bold text-red-600 dark:text-red-400">{{ number_format($stats['expired']) }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow">
<div class="flex items-center">
<div class="p-2 bg-green-100 dark:bg-green-900/50 rounded-lg">
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M8.433 7.418c.155-.103.346-.196.567-.267v1.698a2.305 2.305 0 01-.567-.267C8.07 8.34 8 8.114 8 8c0-.114.07-.34.433-.582z"></path>
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Value</p>
<p class="text-lg font-bold text-green-600 dark:text-green-400">${{ number_format($stats['total_value'], 0) }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow">
<div class="flex items-center">
<div class="p-2 bg-purple-100 dark:bg-purple-900/50 rounded-lg">
<svg class="w-6 h-6 text-purple-600 dark:text-purple-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Avg Value</p>
<p class="text-lg font-bold text-purple-600 dark:text-purple-400">${{ number_format($stats['avg_value'], 0) }}</p>
</div>
</div>
</div>
</div>
<!-- Advanced Filters & Search -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Filters & Search</h3>
@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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
Clear Filters
</button>
</div>
@endif
</div>
<!-- Basic Filters Row -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
<div>
<flux:field>
<flux:label>Search</flux:label>
<flux:input wire:model.live.debounce.300ms="search" placeholder="Estimate #, customer name, vehicle..." />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Status</flux:label>
<flux:select wire:model.live="statusFilter" placeholder="All Statuses">
<option value="">All Statuses</option>
<option value="draft">Draft</option>
<option value="sent">Sent</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
<option value="expired">Expired</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Approval Status</label>
<select wire:model.live="approvalStatusFilter" 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">
<option value="pending_approval">Pending Approval</option>
</flux:select>
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Approval Status</flux:label>
<flux:select wire:model.live="approvalStatusFilter" placeholder="All Approvals">
<option value="">All Approvals</option>
<option value="pending">Pending</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
</select>
</div>
</flux:select>
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Customer</flux:label>
<flux:select wire:model.live="customerFilter" placeholder="All Customers">
<option value="">All Customers</option>
@foreach($customers as $customer)
<option value="{{ $customer->id }}">{{ $customer->name }}</option>
@endforeach
</flux:select>
</flux:field>
</div>
</div>
<!-- Estimates List -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg">
@if($estimates->count() > 0)
<div class="overflow-x-auto">
<table class="w-full divide-y divide-zinc-200 dark:divide-zinc-700">
<thead class="bg-zinc-50 dark:bg-zinc-900">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Estimate #</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Customer</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Vehicle</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Total Amount</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Approval</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Created</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-zinc-800 divide-y divide-zinc-200 dark:divide-zinc-700">
@foreach($estimates as $estimate)
<tr class="hover:bg-zinc-50 dark:hover:bg-zinc-700">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-zinc-900 dark:text-zinc-100">
{{ $estimate->estimate_number }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-900 dark:text-zinc-100">
{{ $estimate->jobCard->customer->first_name }} {{ $estimate->jobCard->customer->last_name }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-900 dark:text-zinc-100">
{{ $estimate->jobCard->vehicle->year }} {{ $estimate->jobCard->vehicle->make }} {{ $estimate->jobCard->vehicle->model }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-zinc-900 dark:text-zinc-100">
${{ number_format($estimate->total_amount, 2) }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full
@if($estimate->status === 'approved') bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200
@elseif($estimate->status === 'sent') bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200
@elseif($estimate->status === 'draft') bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200
@elseif($estimate->status === 'rejected') bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200
@else bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 @endif">
{{ ucfirst($estimate->status) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full
@if($estimate->customer_approval_status === 'approved') bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200
@elseif($estimate->customer_approval_status === 'pending') bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200
@else bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 @endif">
{{ ucfirst($estimate->customer_approval_status) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-900 dark:text-zinc-100">
{{ $estimate->created_at->format('M j, Y') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex space-x-2">
<a href="{{ route('estimates.show', $estimate) }}" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">
View
</a>
<a href="{{ route('estimates.edit', $estimate) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
Edit
</a>
<a href="{{ route('estimates.pdf', $estimate) }}" class="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300">
PDF
<!-- Advanced Filters (Collapsible) -->
@if($showAdvancedFilters)
<div class="border-t border-gray-200 pt-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<flux:field>
<flux:label>Date From</flux:label>
<flux:input wire:model.live="dateFrom" type="date" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Date To</flux:label>
<flux:input wire:model.live="dateTo" type="date" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Min Amount ($)</flux:label>
<flux:input wire:model.live="totalAmountMin" type="number" step="0.01" placeholder="0.00" />
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Max Amount ($)</flux:label>
<flux:input wire:model.live="totalAmountMax" type="number" step="0.01" placeholder="999999.99" />
</flux:field>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<flux:field>
<flux:label>Validity Status</flux:label>
<flux:select wire:model.live="validityFilter" placeholder="All Validity">
<option value="">All Validity</option>
<option value="valid">Valid</option>
<option value="expiring_soon">Expiring Soon (7 days)</option>
<option value="expired">Expired</option>
</flux:select>
</flux:field>
</div>
<div>
<flux:field>
<flux:label>Branch</flux:label>
<flux:select wire:model.live="branchFilter" placeholder="All Branches">
<option value="">All Branches</option>
@foreach($branches as $branch)
<option value="{{ $branch->code }}">{{ $branch->name }}</option>
@endforeach
</flux:select>
</flux:field>
</div>
</div>
</div>
@endif
</div>
<!-- Bulk Actions Bar (Show when bulk mode is active and items selected) -->
@if($bulkMode && !empty($selectedEstimates))
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<span class="text-sm font-medium text-blue-900">{{ count($selectedEstimates) }} estimate(s) selected</span>
</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">
<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">
<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">
<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
</button>
</div>
</div>
</div>
@endif
<!-- Main Estimates Table -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gradient-to-r from-accent/10 to-accent/5 dark:from-accent/20 dark:to-accent/10">
<tr>
@if($bulkMode)
<th class="px-6 py-3 text-left">
<flux:checkbox wire:model.live="selectAll" />
</th>
@endif
<th class="px-6 py-3 text-left text-xs font-medium text-gray-900 dark:text-gray-100 uppercase tracking-wider cursor-pointer hover:bg-accent/20 dark:hover:bg-accent/30 transition-colors rounded-tl-lg" wire:click="sortBy('estimate_number')">
<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">
@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
<path fill-rule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clip-rule="evenodd"></path>
@endif
</svg>
@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 cursor-pointer hover:bg-accent/20 dark:hover:bg-accent/30 transition-colors" wire:click="sortBy('created_at')">
<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">
@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
<path fill-rule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clip-rule="evenodd"></path>
@endif
</svg>
@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">Customer</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-900 dark:text-gray-100 uppercase tracking-wider">Vehicle</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-900 dark:text-gray-100 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-900 dark:text-gray-100 uppercase tracking-wider">Approval</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-900 dark:text-gray-100 uppercase tracking-wider cursor-pointer hover:bg-accent/20 dark:hover:bg-accent/30 transition-colors" wire:click="sortBy('total_amount')">
<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">
@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
<path fill-rule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clip-rule="evenodd"></path>
@endif
</svg>
@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 }}">
@if($bulkMode)
<td class="px-6 py-4 whitespace-nowrap">
<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="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $estimate->estimate_number }}</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900 dark:text-gray-100">{{ $estimate->created_at->format('M j, Y') }}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">{{ $estimate->created_at->format('g:i A') }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $estimate->customer_id ? $estimate->customer?->name : $estimate->jobCard?->customer?->name ?? 'Unknown Customer' }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ $estimate->customer_id ? $estimate->customer?->email : $estimate->jobCard?->customer?->email ?? 'No email' }}
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900 dark:text-gray-100">
@if($estimate->vehicle_id)
{{ $estimate->vehicle?->year }} {{ $estimate->vehicle?->make }} {{ $estimate->vehicle?->model }}
@else
{{ $estimate->jobCard?->vehicle?->year }} {{ $estimate->jobCard?->vehicle?->make }} {{ $estimate->jobCard?->vehicle?->model }}
@endif
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ $estimate->vehicle_id ? $estimate->vehicle?->license_plate : $estimate->jobCard?->vehicle?->license_plate }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@php
$statusColors = [
'draft' => 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
'sent' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300',
'viewed' => 'bg-purple-100 text-purple-800 dark:bg-purple-900/50 dark:text-purple-300',
'approved' => 'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300',
'rejected' => 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300',
'expired' => 'bg-accent/20 text-accent dark:bg-accent/30 dark:text-accent-foreground',
];
@endphp
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $statusColors[$estimate->status] ?? 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' }}">
{{ ucfirst($estimate->status) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@php
$approvalColors = [
'pending' => 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300',
'approved' => 'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300',
'rejected' => 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300',
];
@endphp
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $approvalColors[$estimate->customer_approval_status] ?? 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' }}">
{{ ucfirst(str_replace('_', ' ', $estimate->customer_approval_status)) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
${{ 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">
<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>
</svg>
<div class="text-gray-500">
<p class="text-lg font-medium">No estimates found</p>
<p class="text-sm">Get started by creating your first estimate</p>
</div>
@if($availableDiagnoses->count() > 0)
<div class="relative" x-data="{ open: false }">
<button @click="open = !open" class="inline-flex items-center px-4 py-2 bg-accent hover:bg-accent-content text-accent-foreground font-medium rounded-lg transition-all duration-200">
<svg class="w-5 h-5 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>
Create New Estimate
<svg class="w-4 h-4 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>
<div x-show="open" @click.away="open = false" x-transition class="absolute bottom-full left-0 mb-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-10">
<div class="p-3">
<p class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Select a diagnosis to create estimate for:</p>
<div class="space-y-2 max-h-40 overflow-y-auto">
@foreach($availableDiagnoses as $diagnosis)
<a href="{{ route('estimates.create', $diagnosis) }}" class="block p-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $diagnosis->jobCard?->customer?->name ?? 'Unknown Customer' }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ $diagnosis->jobCard?->vehicle?->year }} {{ $diagnosis->jobCard?->vehicle?->make }} {{ $diagnosis->jobCard?->vehicle?->model }}
</div>
</a>
@endforeach
</div>
</div>
</div>
</div>
@else
<div class="text-center">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-3">No diagnoses available for estimate creation.</p>
<a href="{{ route('job-cards.index') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-all duration-200">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 110 2h-3a1 1 0 01-1-1v-2a1 1 0 00-1-1H9a1 1 0 00-1 1v2a1 1 0 01-1 1H4a1 1 0 110-2V4z" clip-rule="evenodd"></path>
</svg>
Go to Job Cards
</a>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="px-6 py-4 border-t border-zinc-200 dark:border-zinc-700">
{{ $estimates->links() }}
</div>
@else
<div class="p-12 text-center">
<svg class="mx-auto h-12 w-12 text-zinc-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"/>
</svg>
<h3 class="mt-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">No estimates found</h3>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
@if($search || $statusFilter || $approvalStatusFilter)
Try adjusting your search criteria.
@else
Estimates will appear here once job cards have diagnoses.
@endif
</p>
</div>
@endif
@endif
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
@if($estimates->hasPages())
<div class="mt-6">
{{ $estimates->links() }}
</div>
@endif
</div>

View File

@ -0,0 +1,384 @@
<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="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">
<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">
Estimate #{{ $estimate->estimate_number }}
</h1>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mt-1">
Job Card #{{ $estimate->jobCard->job_card_number }} • {{ $estimate->created_at->format('M j, Y') }}
</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
">
{{ 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
">
Customer: {{ ucfirst($estimate->customer_approval_status) }}
</span>
@if($estimate->validity_period_days)
<span class="text-xs text-zinc-500 dark:text-zinc-400">
Valid until {{ $estimate->created_at->addDays($estimate->validity_period_days)->format('M j, Y') }}
</span>
@endif
</div>
</div>
</div>
<!-- Advanced Action Menu -->
<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">
<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>
@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>
<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>
<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>
</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>
<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>
</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>
<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>
</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>
</div>
</div>
@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>
</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>
</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>
@endif
</div>
</div>
</div>
</div>
</div>

View File

@ -1,3 +1,421 @@
<div>
{{-- Stop trying to control. --}}
<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="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">
<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">
Estimate #{{ $estimate->estimate_number }}
</h1>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mt-1">
@if($estimate->jobCard)
Job Card #{{ $estimate->jobCard->job_card_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
">
{{ 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
">
Customer: {{ ucfirst($estimate->customer_approval_status) }}
</span>
@if($estimate->validity_period_days)
<span class="text-xs text-zinc-500 dark:text-zinc-400">
Valid until {{ $estimate->valid_until->format('M j, Y') }}
</span>
@endif
</div>
</div>
</div>
<!-- Advanced Action Menu -->
<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">
<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>
@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>
<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
</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>
</div>
</div>
@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>
</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>
</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>
@endif
</div>
</div>
</div>
</div>
</div>

View File

@ -1,3 +1,750 @@
<div>
{{-- Close your eyes. Count to one. That is how long forever feels. --}}
<div class="max-w-6xl mx-auto space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ ucfirst($type) }} Vehicle Inspection</flux:heading>
<flux:subheading>Job Card: {{ $jobCard->job_card_number }} - {{ $jobCard->customer->name ?? 'Unknown Customer' }}</flux:subheading>
</div>
<flux:button href="{{ route('job-cards.show', $jobCard) }}" variant="ghost" size="sm" icon="arrow-left">
Back to Job Card
</flux:button>
</div>
<!-- Workflow Progress -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<!-- Step 1: Vehicle Reception -->
<div class="flex items-center">
<div class="flex items-center justify-center w-8 h-8 bg-green-500 text-white rounded-full text-sm font-semibold">
</div>
<span class="ml-2 text-sm font-medium text-green-600 dark:text-green-400">Vehicle Reception</span>
</div>
<div class="w-8 h-0.5 bg-green-500"></div>
<!-- Step 2: Initial Inspection (Current) -->
<div class="flex items-center">
<div class="flex items-center justify-center w-8 h-8 bg-blue-500 text-white rounded-full text-sm font-semibold animate-pulse">
2
</div>
<span class="ml-2 text-sm font-medium text-blue-600 dark:text-blue-400">{{ ucfirst($type) }} Inspection</span>
</div>
<div class="w-8 h-0.5 bg-zinc-300 dark:bg-zinc-600"></div>
<!-- Step 3: Diagnosis -->
<div class="flex items-center">
<div class="flex items-center justify-center w-8 h-8 bg-zinc-300 dark:bg-zinc-600 text-zinc-600 dark:text-zinc-400 rounded-full text-sm font-semibold">
3
</div>
<span class="ml-2 text-sm text-zinc-500 dark:text-zinc-400">Diagnosis</span>
</div>
</div>
</div>
</div>
<!-- Vehicle Information -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-4 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="M9 12a1 1 0 01-1-1V7a1 1 0 011-1h6a1 1 0 011 1v4a1 1 0 01-1 1m-6 0h6m-6 0v4a1 1 0 001 1h4a1 1 0 001-1v-4"></path>
</svg>
Vehicle Information
</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 text-sm">
<div>
<span class="font-medium text-zinc-700 dark:text-zinc-300">Customer:</span>
<div class="text-zinc-900 dark:text-zinc-100">{{ $jobCard->customer->name ?? 'Unknown Customer' }}</div>
@if($jobCard->customer->phone)
<div class="text-zinc-500 dark:text-zinc-400">{{ $jobCard->customer->phone }}</div>
@endif
</div>
<div>
<span class="font-medium text-zinc-700 dark:text-zinc-300">Vehicle:</span>
<div class="text-zinc-900 dark:text-zinc-100">{{ $jobCard->vehicle->year ?? '' }} {{ $jobCard->vehicle->make ?? '' }} {{ $jobCard->vehicle->model ?? '' }}</div>
</div>
<div>
<span class="font-medium text-zinc-700 dark:text-zinc-300">License Plate:</span>
<div class="text-zinc-900 dark:text-zinc-100">{{ $jobCard->vehicle->license_plate ?? '' }}</div>
</div>
<div>
<span class="font-medium text-zinc-700 dark:text-zinc-300">VIN:</span>
<div class="text-zinc-900 dark:text-zinc-100 text-xs break-all">{{ $jobCard->vehicle->vin ?? 'N/A' }}</div>
</div>
</div>
@if($jobCard->customer_reported_issues)
<div class="mt-4 p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<h5 class="font-medium text-amber-800 dark:text-amber-200 mb-2">Customer Reported Issues:</h5>
<p class="text-amber-700 dark:text-amber-300 text-sm">{{ $jobCard->customer_reported_issues }}</p>
</div>
@endif
</div>
<!-- Inspection Form -->
<form wire:submit="save" class="space-y-6">
<!-- Basic Information -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6">Basic Information</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
@if($jobCard->mileage_in)
<flux:input
wire:model="current_mileage"
label="Current Mileage (km)"
type="number"
readonly
required
/>
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
<svg class="w-3 h-3 inline mr-1" 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>
Pulled from Job Card
</p>
@else
<flux:input
wire:model="current_mileage"
label="Current Mileage (km)"
type="number"
placeholder="Enter current mileage"
required
/>
@endif
@error('current_mileage')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<div>
@if($jobCard->fuel_level_in)
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Fuel Level</label>
<select disabled class="w-full px-3 py-2 border border-zinc-300 dark:border-zinc-600 rounded-md bg-zinc-50 dark:bg-zinc-700 text-zinc-900 dark:text-zinc-100">
<option value="{{ $fuel_level }}">{{ ucwords(str_replace('_', ' ', $fuel_level)) }}</option>
</select>
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
<svg class="w-3 h-3 inline mr-1" 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>
Pulled from Job Card
</p>
</div>
@else
<flux:select wire:model="fuel_level" label="Fuel Level" required>
<option value="">Select fuel level...</option>
<option value="empty">Empty (0-10%)</option>
<option value="low">Low (10-25%)</option>
<option value="quarter">Quarter (25-40%)</option>
<option value="half">Half (40-60%)</option>
<option value="three_quarter">Three Quarter (60-85%)</option>
<option value="full">Full (85-100%)</option>
</flux:select>
@endif
@error('fuel_level')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<div>
<flux:select wire:model="overall_condition" label="Overall Condition" required>
<option value="">Select condition...</option>
<option value="excellent">Excellent</option>
<option value="good">Good</option>
<option value="fair">Fair</option>
<option value="poor">Poor</option>
</flux:select>
@error('overall_condition')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
</div>
</div>
<!-- Inspection Checklist -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6">Vehicle Inspection Checklist</flux:heading>
@error('checklist')
<div class="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<div class="flex">
<svg class="w-5 h-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">Checklist Incomplete</h3>
<p class="mt-1 text-sm text-red-700 dark:text-red-300">{{ $message }}</p>
</div>
</div>
</div>
@enderror
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left Column -->
<div class="space-y-8">
<!-- Documentation -->
<div>
<h4 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-4 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="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>
Documentation
</h4>
<div class="space-y-3">
@foreach([
'manufacturers_handbook' => "Manufacturer's handbook",
'service_record_book' => 'Service record book (up-to-date)',
'company_drivers_handbook' => "Company driver's handbook",
'accident_report_form' => 'Accident report form',
'safety_inspection_sticker' => 'Copy of annual safety inspection sticker or form'
] as $key => $label)
<div class="flex items-center justify-between py-2 border-b border-zinc-100 dark:border-zinc-700">
<span class="text-sm text-zinc-700 dark:text-zinc-300">{{ $label }}</span>
<div class="flex space-x-4">
<label class="flex items-center">
<input type="radio" wire:model="checklist.documentation.{{ $key }}" value="yes" class="text-green-600 focus:ring-green-500">
<span class="ml-1 text-xs text-green-600">Yes</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="checklist.documentation.{{ $key }}" value="no" class="text-red-600 focus:ring-red-500">
<span class="ml-1 text-xs text-red-600">No</span>
</label>
</div>
</div>
@endforeach
</div>
</div>
<!-- Vehicle Interior -->
<div>
<h4 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-4 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="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
</svg>
Vehicle Interior
</h4>
<div class="space-y-3">
@foreach([
'heating' => 'Heating',
'air_conditioning' => 'Air Conditioning',
'windshield_defrosting_system' => 'Windshield defrosting system',
'window_operation' => 'Window operation',
'door_handles_locks' => 'Door handles / locks',
'alarm' => 'Alarm',
'signals' => 'Signals',
'seat_belts_work' => 'Seat belts work and free of damage / excessive wear',
'interior_lights' => 'Interior Lights',
'mirrors_good_position' => 'Mirrors are in good position and properly adjusted',
'warning_lights' => 'No warning lights are on',
'fuel_levels' => 'Fuel levels',
'oil_level_sufficient' => 'Oil level is sufficiently high',
'washer_fluids_sufficient' => 'Washer fluids levels are sufficiently high',
'radiator_fluid_sufficient' => 'Radiator fluid levels are sufficient',
'emergency_roadside_supplies' => 'Emergency roadside supplies are properly stocked and located in the trunk of vehicle'
] as $key => $label)
<div class="flex items-center justify-between py-2 border-b border-zinc-100 dark:border-zinc-700">
<span class="text-sm text-zinc-700 dark:text-zinc-300">{{ $label }}</span>
<div class="flex space-x-4">
<label class="flex items-center">
<input type="radio" wire:model="checklist.interior.{{ $key }}" value="yes" class="text-green-600 focus:ring-green-500">
<span class="ml-1 text-xs text-green-600">Yes</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="checklist.interior.{{ $key }}" value="no" class="text-red-600 focus:ring-red-500">
<span class="ml-1 text-xs text-red-600">No</span>
</label>
</div>
</div>
@endforeach
</div>
</div>
<!-- Engine Compartment -->
<div>
<h4 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-4 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="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
Engine Compartment
</h4>
<div class="space-y-3">
@foreach([
'engine_oil_level' => 'Engine oil level',
'coolant_level_antifreeze' => 'Coolant level (anti-freeze)',
'battery_secured' => 'Battery secured',
'brake_fluid_level' => 'Brake fluid level',
'air_filter_clean' => 'Air filter is clean',
'belts_hoses_good' => 'Belts and hoses are in good condition - no cracks, full with adequate tension'
] as $key => $label)
<div class="flex items-center justify-between py-2 border-b border-zinc-100 dark:border-zinc-700">
<span class="text-sm text-zinc-700 dark:text-zinc-300">{{ $label }}</span>
<div class="flex space-x-4">
<label class="flex items-center">
<input type="radio" wire:model="checklist.engine.{{ $key }}" value="yes" class="text-green-600 focus:ring-green-500">
<span class="ml-1 text-xs text-green-600">Yes</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="checklist.engine.{{ $key }}" value="no" class="text-red-600 focus:ring-red-500">
<span class="ml-1 text-xs text-red-600">No</span>
</label>
</div>
</div>
@endforeach
</div>
</div>
</div>
<!-- Right Column -->
<div class="space-y-8">
<!-- Vehicle Exterior -->
<div>
<h4 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-4 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="M8 14v3m4-3v3m4-3v3M3 21h18M3 10h18M3 7l9-4 9 4M4 10h16v11H4V10z"></path>
</svg>
Vehicle Exterior
</h4>
<div class="space-y-3">
@foreach([
'windshield_not_cracked' => 'Windows/windshield not severely cracked',
'windshield_wipers_functional' => 'Functional Windshield wipers',
'headlights_high_low' => 'Headlights (high/low beam)',
'tail_lights_brake_lights' => 'Tail lights / brake lights',
'emergency_brake_working' => 'Emergency brake in good working order',
'power_brakes_working' => 'Power brakes are in good working order',
'horn_works' => 'Horn works',
'tires_good_shape' => 'Tires in good shape (no damages and adequately inflated)',
'no_air_leaks' => 'No air leaks (walk around the vehicle and listen for air leaks while driver applies the brakes)',
'no_oil_grease_leaks' => 'No oil / grease leaks (oil wheel seats or under the vehicle)',
'no_fuel_leaks' => 'No fuel leaks or odour of gasoline detected',
'mirrors_good_position' => 'Mirrors are in good position and properly adjusted',
'exhaust_system_working' => 'Exhaust system is in good working order',
'wheels_fasteners_tight' => 'Wheels and fasteners are fitted tightly',
'turn_signals' => 'Turn signals',
'vehicle_free_damage' => 'Vehicle is free of excessive damage',
'loads_fastened' => 'All loads are fastened / secured',
'spare_tire_good' => 'Spare tire is in good condition',
'vehicle_condition_satisfactory' => 'Vehicle condition is satisfactory',
'defects_recorded' => 'Defects recorded'
] as $key => $label)
<div class="flex items-center justify-between py-2 border-b border-zinc-100 dark:border-zinc-700">
<span class="text-sm text-zinc-700 dark:text-zinc-300">{{ $label }}</span>
<div class="flex space-x-4">
<label class="flex items-center">
<input type="radio" wire:model="checklist.exterior.{{ $key }}" value="yes" class="text-green-600 focus:ring-green-500">
<span class="ml-1 text-xs text-green-600">Yes</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="checklist.exterior.{{ $key }}" value="no" class="text-red-600 focus:ring-red-500">
<span class="ml-1 text-xs text-red-600">No</span>
</label>
</div>
</div>
@endforeach
</div>
</div>
<!-- Vehicle Damage Diagram -->
<div>
<h4 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-4 flex items-center">
<svg class="w-5 h-5 mr-2 text-red-600" 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 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
Vehicle Damage Diagram
</h4>
<div class="bg-zinc-50 dark:bg-zinc-700 rounded-lg p-4">
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4">Click on any area of the vehicle to mark damage, dents, or scratches</p>
<!-- Vehicle Diagram Container -->
<div class="border-2 border-dashed border-zinc-300 dark:border-zinc-600 rounded-lg bg-white dark:bg-zinc-800 p-4">
<div id="vehicle-diagram-container" class="relative">
<!-- Vehicle SVG Diagram -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450" class="w-full h-auto max-h-96">
<!-- Left Side View -->
<g id="left-side" transform="translate(50, 50)">
<rect x="0" y="50" width="180" height="70" fill="#f8fafc" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="left-body"/>
<circle cx="25" cy="130" r="20" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="left-front-wheel"/>
<circle cx="155" cy="130" r="20" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="left-rear-wheel"/>
<rect x="0" y="30" width="35" height="20" fill="#f8fafc" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="left-front-bumper"/>
<rect x="145" y="30" width="35" height="20" fill="#f8fafc" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="left-rear-bumper"/>
<text x="90" y="20" text-anchor="middle" class="text-xs fill-zinc-700 dark:fill-zinc-300">Left Side</text>
</g>
<!-- Top View -->
<g id="top-view" transform="translate(320, 50)">
<rect x="0" y="0" width="70" height="180" fill="#f8fafc" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="roof"/>
<rect x="-8" y="25" width="16" height="20" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="front-left-wheel"/>
<rect x="62" y="25" width="16" height="20" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="front-right-wheel"/>
<rect x="-8" y="135" width="16" height="20" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="rear-left-wheel"/>
<rect x="62" y="135" width="16" height="20" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="rear-right-wheel"/>
<rect x="10" y="-5" width="50" height="10" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="windshield"/>
<rect x="10" y="175" width="50" height="10" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="rear-window"/>
<text x="35" y="-15" text-anchor="middle" class="text-xs fill-zinc-700 dark:fill-zinc-300">Top View</text>
</g>
<!-- Right Side View -->
<g id="right-side" transform="translate(450, 50)">
<rect x="0" y="50" width="180" height="70" fill="#f8fafc" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="right-body"/>
<circle cx="25" cy="130" r="20" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="right-front-wheel"/>
<circle cx="155" cy="130" r="20" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="right-rear-wheel"/>
<rect x="0" y="30" width="35" height="20" fill="#f8fafc" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="right-front-bumper"/>
<rect x="145" y="30" width="35" height="20" fill="#f8fafc" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="right-rear-bumper"/>
<text x="90" y="20" text-anchor="middle" class="text-xs fill-zinc-700 dark:fill-zinc-300">Right Side</text>
</g>
<!-- Front View -->
<g id="front-view" transform="translate(150, 250)">
<rect x="0" y="0" width="90" height="70" fill="#f8fafc" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="front"/>
<rect x="-8" y="60" width="20" height="20" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="front-left-tire"/>
<rect x="78" y="60" width="20" height="20" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="front-right-tire"/>
<rect x="15" y="-8" width="60" height="8" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="hood"/>
<text x="45" y="-15" text-anchor="middle" class="text-xs fill-zinc-700 dark:fill-zinc-300">Front View</text>
</g>
<!-- Rear View -->
<g id="rear-view" transform="translate(350, 250)">
<rect x="0" y="0" width="90" height="70" fill="#f8fafc" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="rear"/>
<rect x="-8" y="60" width="20" height="20" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="rear-left-tire"/>
<rect x="78" y="60" width="20" height="20" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="rear-right-tire"/>
<rect x="15" y="70" width="60" height="8" fill="#e2e8f0" stroke="#64748b" stroke-width="2" class="clickable-area" data-area="trunk"/>
<text x="45" y="-10" text-anchor="middle" class="text-xs fill-zinc-700 dark:fill-zinc-300">Rear View</text>
</g>
<!-- Damage markers will be added here dynamically -->
<g id="damage-markers"></g>
</svg>
</div>
</div>
<!-- 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>
<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>
<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>
<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>
<span class="text-zinc-700 dark:text-zinc-300">Other</span>
</div>
</div>
<!-- Selected Damage List -->
<div id="damage-list" class="mt-4">
<h5 class="font-medium text-zinc-900 dark:text-zinc-100 mb-2">Marked Damage:</h5>
<div id="damage-items" class="space-y-2"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Additional Comments Section -->
<div class="mt-8 pt-6 border-t border-zinc-200 dark:border-zinc-700">
<h4 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-4">Additional Comments</h4>
<flux:textarea
wire:model="additional_comments"
placeholder="Record any additional observations, defects, or comments..."
rows="4"
class="w-full"
/>
</div>
<!-- Inspection Signature Section -->
<div class="mt-8 pt-6 border-t border-zinc-200 dark:border-zinc-700">
<h4 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-4">Inspection Performed By</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Employee Name</label>
<input type="text" value="{{ auth()->user()->name }}" readonly
class="w-full px-3 py-2 border border-zinc-300 dark:border-zinc-600 rounded-md bg-zinc-50 dark:bg-zinc-700 text-zinc-900 dark:text-zinc-100">
</div>
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Date & Time</label>
<input type="text" value="{{ now()->format('M d, Y \a\t g:i A') }}" readonly
class="w-full px-3 py-2 border border-zinc-300 dark:border-zinc-600 rounded-md bg-zinc-50 dark:bg-zinc-700 text-zinc-900 dark:text-zinc-100">
</div>
</div>
<div class="mt-4">
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Digital Signature</label>
<div class="px-3 py-2 border border-zinc-300 dark:border-zinc-600 rounded-md bg-zinc-50 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-400 italic">
By completing this inspection, {{ auth()->user()->name }} confirms the accuracy of all recorded information.
</div>
</div>
</div>
</div>
<!-- Additional Notes -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6">Additional Information</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<flux:textarea
wire:model="damage_notes"
label="Damage Notes"
placeholder="Note any visible damage, scratches, dents, etc..."
rows="4"
/>
@error('damage_notes')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<div>
<flux:textarea
wire:model="recommendations"
label="Recommendations"
placeholder="Any recommendations for maintenance or repairs..."
rows="4"
/>
@error('recommendations')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
</div>
<div class="mt-6">
<flux:textarea
wire:model="notes"
label="Additional Notes"
placeholder="Any other observations or notes..."
rows="3"
/>
@error('notes')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Cleanliness Rating</label>
<flux:select wire:model="cleanliness_rating">
<option value="1">1 - Very Poor</option>
<option value="2">2 - Poor</option>
<option value="3">3 - Fair</option>
<option value="4">4 - Good</option>
<option value="5">5 - Excellent</option>
</flux:select>
</div>
<div class="flex items-center">
<input
type="checkbox"
wire:model="follow_up_required"
id="follow_up_required"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-zinc-300 rounded"
>
<label for="follow_up_required" class="ml-2 block text-sm text-zinc-700 dark:text-zinc-300">
Follow-up inspection required
</label>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="flex justify-between items-center">
<flux:button href="{{ route('job-cards.show', $jobCard) }}" variant="ghost" size="sm">
Cancel
</flux:button>
<flux:button type="submit" variant="primary" size="sm" wire:loading.attr="disabled" wire:target="save">
<span wire:loading.remove wire:target="save">Complete {{ ucfirst($type) }} Inspection</span>
<span wire:loading wire:target="save">Processing...</span>
</flux:button>
</div>
</form>
</div>
<!-- Vehicle Damage Diagram Script -->
<script>
document.addEventListener('DOMContentLoaded', function() {
let damageData = @this.get('damage_diagram_data');
let currentDamageType = 'damage';
// Add damage type selector
const diagramContainer = document.querySelector('#vehicle-diagram-container');
if (diagramContainer) {
const typeSelector = document.createElement('div');
typeSelector.className = 'mb-4 flex gap-2 flex-wrap';
typeSelector.innerHTML = `
<button type="button" onclick="setDamageType('damage')" class="damage-type-btn active px-3 py-1 text-xs rounded-full bg-red-500 text-white" data-type="damage">Damage</button>
<button type="button" onclick="setDamageType('dent')" class="damage-type-btn px-3 py-1 text-xs rounded-full bg-orange-500 text-white" data-type="dent">Dent</button>
<button type="button" onclick="setDamageType('scratch')" class="damage-type-btn px-3 py-1 text-xs rounded-full bg-yellow-500 text-white" data-type="scratch">Scratch</button>
<button type="button" onclick="setDamageType('other')" class="damage-type-btn px-3 py-1 text-xs rounded-full bg-blue-500 text-white" data-type="other">Other</button>
`;
diagramContainer.insertBefore(typeSelector, diagramContainer.firstChild);
}
// Set damage type function
window.setDamageType = function(type) {
currentDamageType = type;
document.querySelectorAll('.damage-type-btn').forEach(btn => {
btn.classList.remove('active', 'ring-2', 'ring-offset-2');
if (btn.dataset.type === type) {
btn.classList.add('active', 'ring-2', 'ring-offset-2');
}
});
};
// Add click handlers to clickable areas
document.querySelectorAll('.clickable-area').forEach(area => {
area.style.cursor = 'pointer';
area.addEventListener('click', function(e) {
const areaName = this.dataset.area;
const rect = this.getBoundingClientRect();
const svg = document.querySelector('#vehicle-diagram-container svg');
const svgRect = svg.getBoundingClientRect();
// Calculate relative position within the SVG
const x = ((e.clientX - svgRect.left) / svgRect.width) * 800;
const y = ((e.clientY - svgRect.top) / svgRect.height) * 450;
addDamageMarker(areaName, x, y, currentDamageType);
});
// Add hover effect
area.addEventListener('mouseenter', function() {
this.style.fill = 'rgba(59, 130, 246, 0.2)';
});
area.addEventListener('mouseleave', function() {
this.style.fill = '';
});
});
// Add damage marker function
function addDamageMarker(area, x, y, type) {
const colors = {
damage: '#ef4444',
dent: '#f97316',
scratch: '#eab308',
other: '#3b82f6'
};
const markerId = Date.now();
const damageMarkers = document.querySelector('#damage-markers');
// Create marker circle
const marker = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
marker.setAttribute('cx', x);
marker.setAttribute('cy', y);
marker.setAttribute('r', '8');
marker.setAttribute('fill', colors[type]);
marker.setAttribute('stroke', '#fff');
marker.setAttribute('stroke-width', '2');
marker.setAttribute('class', 'damage-marker');
marker.setAttribute('data-id', markerId);
marker.style.cursor = 'pointer';
// Add click to remove
marker.addEventListener('click', function(e) {
e.stopPropagation();
removeDamageMarker(markerId);
});
damageMarkers.appendChild(marker);
// Update damage data
const newDamage = {
id: markerId,
area: area,
x: x,
y: y,
type: type,
description: `${type} on ${area.replace(/-/g, ' ')}`
};
damageData.push(newDamage);
updateDamageList();
}
// Remove damage marker function
function removeDamageMarker(id) {
const marker = document.querySelector(`[data-id="${id}"]`);
if (marker) {
marker.remove();
}
const index = damageData.findIndex(item => item.id == id);
if (index > -1) {
damageData.splice(index, 1);
}
updateDamageList();
}
// Update damage list display
function updateDamageList() {
const damageItems = document.querySelector('#damage-items');
damageItems.innerHTML = '';
damageData.forEach(damage => {
const colors = {
damage: 'bg-red-100 text-red-800',
dent: 'bg-orange-100 text-orange-800',
scratch: 'bg-yellow-100 text-yellow-800',
other: 'bg-blue-100 text-blue-800'
};
const item = document.createElement('div');
item.className = `flex items-center justify-between p-2 rounded ${colors[damage.type]}`;
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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
`;
damageItems.appendChild(item);
});
}
// Initialize with existing damage data
if (damageData && damageData.length > 0) {
damageData.forEach(damage => {
const colors = {
damage: '#ef4444',
dent: '#f97316',
scratch: '#eab308',
other: '#3b82f6'
};
const damageMarkers = document.querySelector('#damage-markers');
const marker = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
marker.setAttribute('cx', damage.x);
marker.setAttribute('cy', damage.y);
marker.setAttribute('r', '8');
marker.setAttribute('fill', colors[damage.type]);
marker.setAttribute('stroke', '#fff');
marker.setAttribute('stroke-width', '2');
marker.setAttribute('class', 'damage-marker');
marker.setAttribute('data-id', damage.id);
marker.style.cursor = 'pointer';
marker.addEventListener('click', function(e) {
e.stopPropagation();
removeDamageMarker(damage.id);
});
damageMarkers.appendChild(marker);
});
updateDamageList();
}
// Make remove function global
window.removeDamageMarker = removeDamageMarker;
});
</script>
</div>

View File

@ -0,0 +1,328 @@
<div>
<div class="print-header">
<h1 style="margin: 0; font-size: 24px; color: #333;">Vehicle Inspection Report</h1>
<p style="margin: 5px 0 0 0; color: #666; font-size: 14px;">
{{ app(\App\Settings\GeneralSettings::class)->shop_name ?? 'Car Repairs Shop' }}
</p>
<p style="margin: 0; color: #666; font-size: 12px;">
{{ app(\App\Settings\GeneralSettings::class)->shop_address ?? '' }}
</p>
<p style="margin: 0; color: #666; font-size: 12px;">
Phone: {{ app(\App\Settings\GeneralSettings::class)->shop_phone ?? '' }} |
Email: {{ app(\App\Settings\GeneralSettings::class)->shop_email ?? '' }}
</p>
</div>
<!-- Inspection Summary -->
@php
$checklist = $inspection->inspection_checklist ?? [];
$totalItems = 0;
$passedItems = 0;
$failedItems = 0;
foreach($checklist as $section => $items) {
if(is_array($items)) {
foreach($items as $key => $value) {
$totalItems++;
$lowerValue = strtolower($value ?? '');
if(in_array($lowerValue, ['pass', 'good', 'excellent', 'satisfactory', 'yes'])) {
$passedItems++;
} elseif(in_array($lowerValue, ['fail', 'poor', 'unsatisfactory', 'no', 'damaged'])) {
$failedItems++;
}
}
}
}
$passRate = $totalItems > 0 ? round(($passedItems / $totalItems) * 100) : 0;
@endphp
@if($totalItems > 0)
<div style="background: #f8f9fa; border: 1px solid #dee2e6; padding: 15px; margin-bottom: 25px; border-radius: 5px;">
<h3 style="margin: 0 0 10px 0; font-size: 16px; color: #333;">Inspection Summary</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 15px; text-align: center;">
<div>
<strong style="font-size: 18px; color: #333;">{{ $totalItems }}</strong><br>
<span style="font-size: 11px; color: #666;">Total Items</span>
</div>
<div>
<strong style="font-size: 18px; color: #28a745;">{{ $passedItems }}</strong><br>
<span style="font-size: 11px; color: #666;">Passed</span>
</div>
<div>
<strong style="font-size: 18px; color: #dc3545;">{{ $failedItems }}</strong><br>
<span style="font-size: 11px; color: #666;">Failed</span>
</div>
<div>
<strong style="font-size: 18px; color: {{ $passRate >= 80 ? '#28a745' : ($passRate >= 60 ? '#ffc107' : '#dc3545') }};">{{ $passRate }}%</strong><br>
<span style="font-size: 11px; color: #666;">Pass Rate</span>
</div>
</div>
</div>
@endif
<div class="info-grid">
<!-- Job Card Information -->
<div class="info-section">
<h3>Job Card Information</h3>
<div class="info-row">
<span class="label">Job Card No:</span>
<span class="value">{{ $jobCard->job_card_number }}</span>
</div>
<div class="info-row">
<span class="label">Date Created:</span>
<span class="value">{{ $jobCard->created_at->format('d M Y') }}</span>
</div>
<div class="info-row">
<span class="label">Status:</span>
<span class="value">{{ ucwords(str_replace('_', ' ', $jobCard->status)) }}</span>
</div>
<div class="info-row">
<span class="label">Branch:</span>
<span class="value">{{ $jobCard->branch->name ?? 'N/A' }}</span>
</div>
</div>
<!-- Customer Information -->
<div class="info-section">
<h3>Customer Information</h3>
<div class="info-row">
<span class="label">Name:</span>
<span class="value">{{ $jobCard->customer->name }}</span>
</div>
<div class="info-row">
<span class="label">Email:</span>
<span class="value">{{ $jobCard->customer->email }}</span>
</div>
<div class="info-row">
<span class="label">Phone:</span>
<span class="value">{{ $jobCard->customer->phone }}</span>
</div>
<div class="info-row">
<span class="label">Address:</span>
<span class="value">{{ $jobCard->customer->address ?: 'N/A' }}</span>
</div>
</div>
<!-- Vehicle Information -->
<div class="info-section">
<h3>Vehicle Information</h3>
<div class="info-row">
<span class="label">Make & Model:</span>
<span class="value">{{ $jobCard->vehicle->make }} {{ $jobCard->vehicle->model }}</span>
</div>
<div class="info-row">
<span class="label">Year:</span>
<span class="value">{{ $jobCard->vehicle->year }}</span>
</div>
<div class="info-row">
<span class="label">License Plate:</span>
<span class="value">{{ $jobCard->vehicle->license_plate }}</span>
</div>
<div class="info-row">
<span class="label">VIN:</span>
<span class="value">{{ $jobCard->vehicle->vin ?: 'N/A' }}</span>
</div>
</div>
</div>
<!-- Inspection Details -->
<div class="info-grid" style="grid-template-columns: 1fr 1fr;">
<div class="info-section">
<h3>Inspection Details</h3>
<div class="info-row">
<span class="label">Type:</span>
<span class="value">{{ ucwords(str_replace('_', ' ', $inspection->inspection_type)) }}</span>
</div>
<div class="info-row">
<span class="label">Date:</span>
<span class="value">{{ $inspection->created_at->format('d M Y H:i') }}</span>
</div>
<div class="info-row">
<span class="label">Inspector:</span>
<span class="value">{{ $inspection->inspector->name ?? 'N/A' }}</span>
</div>
<div class="info-row">
<span class="label">Current Mileage:</span>
<span class="value">{{ number_format($inspection->current_mileage) }} km</span>
</div>
</div>
<div class="info-section">
<h3>Vehicle Condition</h3>
<div class="info-row">
<span class="label">Fuel Level:</span>
<span class="value">{{ $inspection->fuel_level }}%</span>
</div>
<div class="info-row">
<span class="label">Overall Condition:</span>
<span class="value">{{ ucfirst($inspection->overall_condition ?? 'Good') }}</span>
</div>
<div class="info-row">
<span class="label">Cleanliness:</span>
<span class="value">{{ ucfirst($inspection->cleanliness_rating ?? 'Clean') }}</span>
</div>
@if($inspection->quality_rating)
<div class="info-row">
<span class="label">Quality Rating:</span>
<span class="value">{{ $inspection->quality_rating }}/10</span>
</div>
@endif
</div>
</div>
<!-- Inspection Checklist -->
<div style="margin-bottom: 30px;">
<h3 style="margin-bottom: 20px; font-size: 18px; color: #333; border-bottom: 2px solid #333; padding-bottom: 10px;">
Inspection Checklist
</h3>
@php
$checklist = $inspection->inspection_checklist ?? [];
@endphp
<div class="checklist-grid">
<!-- Documentation Section -->
@if(isset($checklist['documentation']))
<div class="checklist-section">
<h4>📋 Documentation</h4>
@foreach($checklist['documentation'] as $key => $value)
<div class="checklist-item">
<span>{{ ucwords(str_replace('_', ' ', $key)) }}</span>
<span class="status-badge status-{{ strtolower($value ?: 'na') }}">{{ strtoupper($value ?: 'N/A') }}</span>
</div>
@endforeach
</div>
@endif
<!-- Exterior Section -->
@if(isset($checklist['exterior']))
<div class="checklist-section">
<h4>🚗 Exterior</h4>
@foreach($checklist['exterior'] as $key => $value)
<div class="checklist-item">
<span>{{ ucwords(str_replace('_', ' ', $key)) }}</span>
<span class="status-badge status-{{ strtolower($value ?: 'na') }}">{{ strtoupper($value ?: 'N/A') }}</span>
</div>
@endforeach
</div>
@endif
<!-- Interior Section -->
@if(isset($checklist['interior']))
<div class="checklist-section">
<h4>🏠 Interior</h4>
@foreach($checklist['interior'] as $key => $value)
<div class="checklist-item">
<span>{{ ucwords(str_replace('_', ' ', $key)) }}</span>
<span class="status-badge status-{{ strtolower($value ?: 'na') }}">{{ strtoupper($value ?: 'N/A') }}</span>
</div>
@endforeach
</div>
@endif
<!-- Engine Section -->
@if(isset($checklist['engine']))
<div class="checklist-section">
<h4><EFBFBD> Engine & Mechanical</h4>
@foreach($checklist['engine'] as $key => $value)
<div class="checklist-item">
<span>{{ ucwords(str_replace('_', ' ', $key)) }}</span>
<span class="status-badge status-{{ strtolower($value ?: 'na') }}">{{ strtoupper($value ?: 'N/A') }}</span>
</div>
@endforeach
</div>
@endif
<!-- Under Hood Section -->
@if(isset($checklist['under_hood']))
<div class="checklist-section">
<h4> Under Hood</h4>
@foreach($checklist['under_hood'] as $key => $value)
<div class="checklist-item">
<span>{{ ucwords(str_replace('_', ' ', $key)) }}</span>
<span class="status-badge status-{{ strtolower($value ?: 'na') }}">{{ strtoupper($value ?: 'N/A') }}</span>
</div>
@endforeach
</div>
@endif
<!-- Under Vehicle Section -->
@if(isset($checklist['under_vehicle']))
<div class="checklist-section">
<h4>🔍 Under Vehicle</h4>
@foreach($checklist['under_vehicle'] as $key => $value)
<div class="checklist-item">
<span>{{ ucwords(str_replace('_', ' ', $key)) }}</span>
<span class="status-badge status-{{ strtolower($value ?: 'na') }}">{{ strtoupper($value ?: 'N/A') }}</span>
</div>
@endforeach
</div>
@endif
</div>
</div>
<!-- Damage Report -->
@if($inspection->damage_diagram_data)
<div class="damage-report page-break">
<h3 style="margin-bottom: 20px; font-size: 18px; color: #333; border-bottom: 2px solid #333; padding-bottom: 10px;">
Vehicle Damage Report
</h3>
@php
$damageData = is_string($inspection->damage_diagram_data)
? json_decode($inspection->damage_diagram_data, true)
: $inspection->damage_diagram_data;
@endphp
@if($damageData && is_array($damageData) && count($damageData) > 0)
<div class="damage-grid">
@foreach($damageData as $index => $damage)
<div class="damage-item">
<strong>Damage {{ $index + 1 }}</strong><br>
<span style="font-size: 11px; color: #666;">
Position: {{ $damage['x'] ?? 'N/A' }}, {{ $damage['y'] ?? 'N/A' }}
</span><br>
<span style="font-size: 11px; color: #888;">
Type: {{ $damage['type'] ?? 'Unknown' }}
</span>
</div>
@endforeach
</div>
@else
<p style="color: #666; font-style: italic;">No damage recorded during inspection.</p>
@endif
</div>
@endif
<!-- Signature Section -->
<div style="margin-top: 50px; border-top: 2px solid #ddd; padding-top: 30px;">
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 40px;">
<div style="text-align: center;">
<div style="border-bottom: 1px solid #333; margin-bottom: 10px; height: 40px;"></div>
<strong>Inspector Signature</strong><br>
<span style="font-size: 11px; color: #666;">{{ $inspection->inspector->name ?? 'N/A' }}</span><br>
<span style="font-size: 11px; color: #666;">Date: {{ $inspection->created_at->format('d/m/Y') }}</span>
</div>
<div style="text-align: center;">
<div style="border-bottom: 1px solid #333; margin-bottom: 10px; height: 40px;"></div>
<strong>Customer Signature</strong><br>
<span style="font-size: 11px; color: #666;">{{ $jobCard->customer->name }}</span><br>
<span style="font-size: 11px; color: #666;">Date: ________________</span>
</div>
<div style="text-align: center;">
<div style="border-bottom: 1px solid #333; margin-bottom: 10px; height: 40px;"></div>
<strong>Service Manager</strong><br>
<span style="font-size: 11px; color: #666;">Name: ________________</span><br>
<span style="font-size: 11px; color: #666;">Date: ________________</span>
</div>
</div>
</div>
<!-- Footer -->
<div style="margin-top: 40px; text-align: center; border-top: 1px solid #ddd; padding-top: 20px; color: #666; font-size: 10px;">
<p style="margin: 5px 0;">This report was generated on {{ now()->format('d M Y \a\t H:i') }}</p>
<p style="margin: 5px 0;">{{ app(\App\Settings\GeneralSettings::class)->shop_name ?? 'Car Repairs Shop' }} - Professional Vehicle Inspection Services</p>
<p style="margin: 5px 0;">Report ID: {{ $inspection->id }} | Job Card: {{ $jobCard->job_card_number }}</p>
</div>
</div>

View File

@ -1,3 +1,310 @@
<div>
{{-- To attain knowledge, add things every day; To attain wisdom, subtract things every day. --}}
<div class="max-w-7xl mx-auto space-y-8">
<!-- Print Styles -->
<style>
@media print {
body { -webkit-print-color-adjust: exact; }
.no-print { display: none !important; }
.print-break { page-break-before: always; }
.max-w-7xl { max-width: none; }
.space-y-8 > * + * { margin-top: 1.5rem; }
.grid { display: block; }
.grid > * { margin-bottom: 1rem; }
.bg-white, .dark\\:bg-zinc-800 { background-color: white !important; }
.border { border: 1px solid #ccc !important; }
.text-zinc-600, .dark\\:text-zinc-400 { color: #666 !important; }
.text-zinc-900, .dark\\:text-zinc-100 { color: #000 !important; }
}
</style>
<!-- Print Header (only visible when printing) -->
<div class="hidden print:block">
<div class="text-center border-b-2 border-gray-300 pb-4 mb-6">
<h1 class="text-2xl font-bold">Vehicle Inspection Report</h1>
<p class="text-gray-600 mt-2">Generated on {{ now()->format('F d, Y \a\t g:i A') }}</p>
</div>
</div>
<!-- Header -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl" class="text-zinc-900 dark:text-zinc-100">Inspection Details</flux:heading>
<p class="text-zinc-600 dark:text-zinc-400 mt-2 text-lg">
{{ ucfirst($inspection->inspection_type) }} inspection for {{ $inspection->jobCard->vehicle->year }} {{ $inspection->jobCard->vehicle->make }} {{ $inspection->jobCard->vehicle->model }}
</p>
</div>
<div class="flex space-x-3 no-print">
<flux:button href="{{ route('job-cards.show', $inspection->jobCard) }}" variant="ghost" size="sm" icon="arrow-left">
Back to Job Card
</flux:button>
<flux:button
size="sm"
variant="outline"
icon="printer"
onclick="window.open('{{ route('inspections.print', $inspection) }}', '_blank', 'width=800,height=600,scrollbars=yes,resizable=yes')"
>
Print Report
</flux:button>
</div>
</div>
</div>
<!-- Quick Stats -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6 text-center">
<div class="text-3xl font-bold text-blue-600 dark:text-blue-400">{{ ucfirst($inspection->overall_condition) }}</div>
<div class="text-sm text-zinc-600 dark:text-zinc-400 mt-1">Overall Condition</div>
</div>
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6 text-center">
<div class="text-3xl font-bold text-green-600 dark:text-green-400">{{ number_format($inspection->current_mileage) }}</div>
<div class="text-sm text-zinc-600 dark:text-zinc-400 mt-1">Kilometers</div>
</div>
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6 text-center">
<div class="text-3xl font-bold text-orange-600 dark:text-orange-400">{{ $inspection->cleanliness_rating }}/10</div>
<div class="text-sm text-zinc-600 dark:text-zinc-400 mt-1">Cleanliness Rating</div>
</div>
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6 text-center">
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400">{{ ucwords(str_replace('_', ' ', $inspection->fuel_level)) }}</div>
<div class="text-sm text-zinc-600 dark:text-zinc-400 mt-1">Fuel Level</div>
</div>
</div>
<!-- Vehicle & Customer Info -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Vehicle Information -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6 text-zinc-900 dark:text-zinc-100">Vehicle Information</flux:heading>
<div class="space-y-4">
<div class="flex justify-between py-3 border-b border-zinc-100 dark:border-zinc-700">
<span class="font-medium text-zinc-900 dark:text-zinc-100">Vehicle</span>
<span class="text-zinc-600 dark:text-zinc-400">{{ $inspection->jobCard->vehicle->year }} {{ $inspection->jobCard->vehicle->make }} {{ $inspection->jobCard->vehicle->model }}</span>
</div>
<div class="flex justify-between py-3 border-b border-zinc-100 dark:border-zinc-700">
<span class="font-medium text-zinc-900 dark:text-zinc-100">License Plate</span>
<span class="text-zinc-600 dark:text-zinc-400 font-mono">{{ $inspection->jobCard->vehicle->license_plate }}</span>
</div>
<div class="flex justify-between py-3 border-b border-zinc-100 dark:border-zinc-700">
<span class="font-medium text-zinc-900 dark:text-zinc-100">VIN</span>
<span class="text-zinc-600 dark:text-zinc-400 font-mono text-sm">{{ $inspection->jobCard->vehicle->vin }}</span>
</div>
<div class="flex justify-between py-3">
<span class="font-medium text-zinc-900 dark:text-zinc-100">Color</span>
<span class="text-zinc-600 dark:text-zinc-400">{{ $inspection->jobCard->vehicle->color ?? 'Not specified' }}</span>
</div>
</div>
</div>
<!-- Customer Information -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6 text-zinc-900 dark:text-zinc-100">Customer Information</flux:heading>
<div class="space-y-4">
<div class="flex justify-between py-3 border-b border-zinc-100 dark:border-zinc-700">
<span class="font-medium text-zinc-900 dark:text-zinc-100">Name</span>
<span class="text-zinc-600 dark:text-zinc-400">{{ $inspection->jobCard->customer->first_name }} {{ $inspection->jobCard->customer->last_name }}</span>
</div>
<div class="flex justify-between py-3 border-b border-zinc-100 dark:border-zinc-700">
<span class="font-medium text-zinc-900 dark:text-zinc-100">Phone</span>
<span class="text-zinc-600 dark:text-zinc-400 font-mono">{{ $inspection->jobCard->customer->phone ?? 'Not provided' }}</span>
</div>
<div class="flex justify-between py-3 border-b border-zinc-100 dark:border-zinc-700">
<span class="font-medium text-zinc-900 dark:text-zinc-100">Email</span>
<span class="text-zinc-600 dark:text-zinc-400 text-sm">{{ $inspection->jobCard->customer->email ?? 'Not provided' }}</span>
</div>
<div class="flex justify-between py-3">
<span class="font-medium text-zinc-900 dark:text-zinc-100">Customer ID</span>
<span class="text-zinc-600 dark:text-zinc-400">#{{ $inspection->jobCard->customer->id }}</span>
</div>
</div>
</div>
<!-- Inspection Information -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6 text-zinc-900 dark:text-zinc-100">Inspection Information</flux:heading>
<div class="space-y-4">
<div class="flex justify-between py-3 border-b border-zinc-100 dark:border-zinc-700">
<span class="font-medium text-zinc-900 dark:text-zinc-100">Type</span>
<flux:badge color="blue" size="sm">{{ ucfirst($inspection->inspection_type) }}</flux:badge>
</div>
<div class="flex justify-between py-3 border-b border-zinc-100 dark:border-zinc-700">
<span class="font-medium text-zinc-900 dark:text-zinc-100">Date & Time</span>
<span class="text-zinc-600 dark:text-zinc-400">{{ $inspection->inspection_date->format('M d, Y \a\t g:i A') }}</span>
</div>
<div class="flex justify-between py-3 border-b border-zinc-100 dark:border-zinc-700">
<span class="font-medium text-zinc-900 dark:text-zinc-100">Inspector</span>
<span class="text-zinc-600 dark:text-zinc-400">{{ $inspection->inspector->name }}</span>
</div>
<div class="flex justify-between py-3">
<span class="font-medium text-zinc-900 dark:text-zinc-100">Job Card</span>
<span class="text-zinc-600 dark:text-zinc-400">#{{ $inspection->jobCard->job_card_number }}</span>
</div>
</div>
</div>
</div>
<!-- Inspection Checklist -->
@if($inspection->inspection_checklist)
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6 text-zinc-900 dark:text-zinc-100">Inspection Checklist</flux:heading>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-8">
@foreach($inspection->inspection_checklist as $section => $items)
<div class="border border-zinc-200 dark:border-zinc-600 rounded-lg p-5">
<h4 class="font-semibold text-lg text-zinc-900 dark:text-zinc-100 mb-4 pb-2 border-b border-zinc-200 dark:border-zinc-600 capitalize">
{{ str_replace('_', ' ', $section) }}
</h4>
<div class="space-y-3">
@foreach($items as $item => $status)
<div class="flex items-center justify-between py-2">
<span class="text-sm text-zinc-700 dark:text-zinc-300 flex-1">{{ ucwords(str_replace('_', ' ', $item)) }}</span>
<div class="ml-4">
@if($status === 'yes')
<div class="flex items-center space-x-2">
<div class="w-3 h-3 rounded-full bg-green-500"></div>
<flux:badge color="green" size="sm">Pass</flux:badge>
</div>
@elseif($status === 'no')
<div class="flex items-center space-x-2">
<div class="w-3 h-3 rounded-full bg-red-500"></div>
<flux:badge color="red" size="sm">Fail</flux:badge>
</div>
@else
<div class="flex items-center space-x-2">
<div class="w-3 h-3 rounded-full bg-zinc-400"></div>
<flux:badge color="zinc" size="sm">N/A</flux:badge>
</div>
@endif
</div>
</div>
@endforeach
</div>
</div>
@endforeach
</div>
</div>
@endif
<!-- Notes and Comments -->
@if($inspection->additional_comments || $inspection->damage_notes || $inspection->recommendations || $inspection->notes)
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
@if($inspection->additional_comments || $inspection->notes)
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6 text-zinc-900 dark:text-zinc-100">Comments & Notes</flux:heading>
<div class="space-y-6">
@if($inspection->additional_comments)
<div>
<h5 class="font-medium text-zinc-900 dark:text-zinc-100 mb-3 flex items-center">
<svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-1.586l-4 4z"></path>
</svg>
Additional Comments
</h5>
<div class="bg-zinc-50 dark:bg-zinc-900 rounded-lg p-4">
<p class="text-zinc-700 dark:text-zinc-300 leading-relaxed">{{ $inspection->additional_comments }}</p>
</div>
</div>
@endif
@if($inspection->notes)
<div>
<h5 class="font-medium text-zinc-900 dark:text-zinc-100 mb-3 flex items-center">
<svg class="w-5 h-5 mr-2 text-green-500" 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>
Inspector Notes
</h5>
<div class="bg-zinc-50 dark:bg-zinc-900 rounded-lg p-4">
<p class="text-zinc-700 dark:text-zinc-300 leading-relaxed">{{ $inspection->notes }}</p>
</div>
</div>
@endif
</div>
</div>
@endif
@if($inspection->damage_notes || $inspection->recommendations)
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6 text-zinc-900 dark:text-zinc-100">Damage & Recommendations</flux:heading>
<div class="space-y-6">
@if($inspection->damage_notes)
<div>
<h5 class="font-medium text-zinc-900 dark:text-zinc-100 mb-3 flex items-center">
<svg class="w-5 h-5 mr-2 text-red-500" 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.732-.833-2.5 0L4.268 19.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
Damage Notes
</h5>
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<p class="text-red-800 dark:text-red-300 leading-relaxed">{{ $inspection->damage_notes }}</p>
</div>
</div>
@endif
@if($inspection->recommendations)
<div>
<h5 class="font-medium text-zinc-900 dark:text-zinc-100 mb-3 flex items-center">
<svg class="w-5 h-5 mr-2 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
</svg>
Recommendations
</h5>
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
<p class="text-amber-800 dark:text-amber-300 leading-relaxed">{{ $inspection->recommendations }}</p>
</div>
</div>
@endif
</div>
</div>
@endif
</div>
@endif
<!-- Damage Diagram -->
@if($inspection->damage_diagram_data && !empty($inspection->damage_diagram_data))
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6 text-zinc-900 dark:text-zinc-100 flex items-center">
<svg class="w-6 h-6 mr-2 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
</svg>
Vehicle Damage Report
</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@foreach($inspection->damage_diagram_data as $index => $damage)
<div class="border border-zinc-200 dark:border-zinc-600 rounded-lg p-4 hover:shadow-md transition-shadow">
<div class="flex items-center justify-between mb-3">
<h5 class="font-medium text-zinc-900 dark:text-zinc-100">Damage #{{ $index + 1 }}</h5>
@php
$colorClass = match($damage['type']) {
'damage' => 'bg-red-500',
'dent' => 'bg-orange-500',
'scratch' => 'bg-yellow-500',
default => 'bg-blue-500'
};
$textColorClass = match($damage['type']) {
'damage' => 'text-red-700 dark:text-red-300',
'dent' => 'text-orange-700 dark:text-orange-300',
'scratch' => 'text-yellow-700 dark:text-yellow-300',
default => 'text-blue-700 dark:text-blue-300'
};
@endphp
<div class="flex items-center space-x-2">
<span class="inline-block w-4 h-4 rounded-full {{ $colorClass }}"></span>
<span class="text-sm font-medium {{ $textColorClass }} capitalize">{{ $damage['type'] }}</span>
</div>
</div>
@if(!empty($damage['description']))
<p class="text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed">{{ $damage['description'] }}</p>
@else
<p class="text-sm text-zinc-400 dark:text-zinc-500 italic">No description provided</p>
@endif
</div>
@endforeach
</div>
</div>
@endif
</div>

View File

@ -1,98 +1,25 @@
<div>
<div class="max-w-4xl mx-auto space-y-6">
<!-- Page Header -->
<!-- Clean Header -->
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">Create Job Card</flux:heading>
<flux:subheading>Steps 1-2: Vehicle Reception & Initial Inspection</flux:subheading>
<flux:heading size="xl">New Job Card</flux:heading>
<flux:subheading>Vehicle Reception & Service Assignment</flux:subheading>
</div>
<flux:button href="{{ route('job-cards.index') }}" variant="ghost" size="sm" icon="arrow-left">
Back to Job Cards
Back
</flux:button>
</div>
<!-- 11-Step Workflow Progress -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<div class="mb-6">
<flux:heading size="lg">11-Step Automotive Workflow</flux:heading>
<flux:subheading>Track progress through the complete service process</flux:subheading>
</div>
<!-- Progress Steps - Using simple flex layout instead of grid -->
<div class="flex flex-wrap gap-4 mb-6">
<!-- Step 1: Vehicle Reception (Current) -->
<div class="flex flex-col items-center">
<div class="w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center font-semibold text-sm mb-2">1</div>
<div class="text-xs font-medium text-blue-600 text-center">Vehicle<br>Reception</div>
</div>
<!-- Step 2: Initial Inspection (Current) -->
<div class="flex flex-col items-center">
<div class="w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center font-semibold text-sm mb-2">2</div>
<div class="text-xs font-medium text-blue-600 text-center">Initial<br>Inspection</div>
</div>
<!-- Steps 3-11 (Inactive) -->
<div class="flex flex-col items-center">
<div class="w-8 h-8 bg-zinc-300 text-zinc-600 rounded-full flex items-center justify-center font-semibold text-sm mb-2">3</div>
<div class="text-xs text-zinc-500 text-center">Service<br>Assignment</div>
</div>
<div class="flex flex-col items-center">
<div class="w-8 h-8 bg-zinc-300 text-zinc-600 rounded-full flex items-center justify-center font-semibold text-sm mb-2">4</div>
<div class="text-xs text-zinc-500 text-center">Diagnosis</div>
</div>
<div class="flex flex-col items-center">
<div class="w-8 h-8 bg-zinc-300 text-zinc-600 rounded-full flex items-center justify-center font-semibold text-sm mb-2">5</div>
<div class="text-xs text-zinc-500 text-center">Estimate</div>
</div>
<div class="flex flex-col items-center">
<div class="w-8 h-8 bg-zinc-300 text-zinc-600 rounded-full flex items-center justify-center font-semibold text-sm mb-2">6</div>
<div class="text-xs text-zinc-500 text-center">Approval</div>
</div>
<div class="flex flex-col items-center">
<div class="w-8 h-8 bg-zinc-300 text-zinc-600 rounded-full flex items-center justify-center font-semibold text-sm mb-2">7</div>
<div class="text-xs text-zinc-500 text-center">Parts<br>Procurement</div>
</div>
<div class="flex flex-col items-center">
<div class="w-8 h-8 bg-zinc-300 text-zinc-600 rounded-full flex items-center justify-center font-semibold text-sm mb-2">8</div>
<div class="text-xs text-zinc-500 text-center">Repairs</div>
</div>
<div class="flex flex-col items-center">
<div class="w-8 h-8 bg-zinc-300 text-zinc-600 rounded-full flex items-center justify-center font-semibold text-sm mb-2">9</div>
<div class="text-xs text-zinc-500 text-center">Final<br>Inspection</div>
</div>
<div class="flex flex-col items-center">
<div class="w-8 h-8 bg-zinc-300 text-zinc-600 rounded-full flex items-center justify-center font-semibold text-sm mb-2">10</div>
<div class="text-xs text-zinc-500 text-center">Delivery</div>
</div>
<div class="flex flex-col items-center">
<div class="w-8 h-8 bg-zinc-300 text-zinc-600 rounded-full flex items-center justify-center font-semibold text-sm mb-2">11</div>
<div class="text-xs text-zinc-500 text-center">Archival</div>
</div>
</div>
<!-- Current Step Info -->
<div class="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<div class="flex items-center space-x-3">
<div class="flex space-x-1">
<div class="w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center font-semibold text-sm">1</div>
<div class="w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center font-semibold text-sm">2</div>
</div>
<div>
<div class="font-medium text-blue-900 dark:text-blue-100">Vehicle Reception + Initial Inspection</div>
<div class="text-sm text-blue-700 dark:text-blue-300">Capture vehicle information, customer complaints, and perform incoming inspection</div>
</div>
</div>
</div>
</div>
<!-- Enhanced Form -->
<form wire:submit="save" class="space-y-6">
<!-- Customer & Vehicle Information -->
<!-- Customer & Vehicle Selection -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6">Customer & Vehicle Information</flux:heading>
<flux:heading size="lg" class="mb-6">Customer & Vehicle</flux:heading>
<div class="space-y-6">
<!-- Customer Selection -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Customer Selection - Half Width -->
<div>
<flux:select wire:model.live="customer_id" label="Customer" placeholder="Select customer..." required>
@if($customers && count($customers) > 0)
@ -106,7 +33,7 @@
@enderror
</div>
<!-- Vehicle Selection -->
<!-- Vehicle Selection - Half Width -->
<div>
<flux:select wire:model="vehicle_id" label="Vehicle" placeholder="Select vehicle..." required>
@if($vehicles && count($vehicles) > 0)
@ -126,8 +53,8 @@
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6">Service Assignment</flux:heading>
<div class="space-y-6">
<!-- Service Advisor -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Service Advisor - Half Width -->
<div>
<flux:select wire:model="service_advisor_id" label="Service Advisor" placeholder="Select service advisor..." required>
@if($serviceAdvisors && count($serviceAdvisors) > 0)
@ -141,7 +68,7 @@
@enderror
</div>
<!-- Branch Selection -->
<!-- Branch Selection - Half Width -->
<div>
<flux:select wire:model="branch_code" label="Branch" placeholder="Select branch..." required>
@if($branches && count($branches) > 0)
@ -154,6 +81,38 @@
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
</div>
</div>
<!-- Reception Details -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6">Reception Details</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Arrival DateTime -->
<div>
<flux:input
type="datetime-local"
wire:model="arrival_datetime"
label="Arrival Date & Time"
required
/>
@error('arrival_datetime')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<!-- Expected Completion -->
<div>
<flux:input
type="date"
wire:model="expected_completion_date"
label="Expected Completion"
/>
@error('expected_completion_date')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<!-- Priority -->
<div>
@ -168,321 +127,68 @@
@enderror
</div>
</div>
</div>
<!-- Vehicle Reception Details -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6">Vehicle Reception Details</flux:heading>
<div class="space-y-6">
<!-- Row 1: Dates and Mileage -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Arrival DateTime -->
<div>
<flux:input
type="datetime-local"
wire:model="arrival_datetime"
label="Arrival Date & Time"
required
/>
@error('arrival_datetime')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<!-- Expected Completion -->
<div>
<flux:input
type="date"
wire:model="expected_completion_date"
label="Expected Completion Date"
/>
@error('expected_completion_date')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<!-- Mileage -->
<div>
<flux:input
type="number"
wire:model="mileage_in"
label="Mileage (km)"
placeholder="e.g., 45000"
min="0"
step="1"
required
/>
@error('mileage_in')
<flux:error>{{ $message }}</flux:error>
@enderror
<p class="text-sm text-zinc-600 dark:text-zinc-400 mt-1">Enter the current odometer reading</p>
</div>
</div>
<!-- Row 2: Fuel Level and Keys -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Fuel Level -->
<div>
<flux:select wire:model="fuel_level_in" label="Fuel Level">
<option value="">Select fuel level...</option>
<option value="Empty">Empty (0-10%)</option>
<option value="Low">Low (10-25%)</option>
<option value="Quarter">Quarter (25-40%)</option>
<option value="Half">Half (40-60%)</option>
<option value="Three Quarters">Three Quarters (60-85%)</option>
<option value="Full">Full (85-100%)</option>
</flux:select>
@error('fuel_level_in')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<!-- Keys Location -->
<div>
<flux:input
wire:model="keys_location"
label="Keys Location"
placeholder="e.g., In vehicle, With service advisor, Key box"
/>
@error('keys_location')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
</div>
<!-- Row 3: Checkboxes -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Personal Items Removed -->
<div>
<flux:checkbox wire:model="personal_items_removed" label="Personal items removed from vehicle" />
@error('personal_items_removed')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<!-- Photos Taken -->
<div>
<flux:checkbox wire:model="photos_taken" label="Photos taken of vehicle condition" />
@error('photos_taken')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
</div>
</div>
</div>
<!-- Initial Inspection Section -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<div class="mb-6">
<flux:heading size="lg">Initial Vehicle Inspection</flux:heading>
<flux:subheading>Perform incoming inspection as part of vehicle reception</flux:subheading>
</div>
<div class="space-y-6">
<!-- Perform Inspection Toggle -->
<!-- Vehicle Details Row -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-6">
<!-- Mileage -->
<div>
<flux:checkbox wire:model.live="perform_inspection" label="Perform initial inspection during reception" checked />
<p class="text-sm text-zinc-600 dark:text-zinc-400 mt-1">Recommended for quality control and customer protection</p>
</div>
@if($perform_inspection)
<!-- Inspector Selection -->
<div>
<flux:select wire:model="inspector_id" label="Inspector" placeholder="Select inspector..." required>
@if($inspectors && count($inspectors) > 0)
@foreach($inspectors as $inspector)
<option value="{{ $inspector->id }}">{{ $inspector->name }}</option>
@endforeach
@endif
</flux:select>
@error('inspector_id')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<!-- Overall Condition -->
<div>
<flux:select wire:model="overall_condition" label="Overall Vehicle Condition" placeholder="Select overall condition..." required>
<option value="excellent">Excellent - Like new condition</option>
<option value="good">Good - Well maintained with minor wear</option>
<option value="fair">Fair - Normal wear, some issues present</option>
<option value="poor">Poor - Significant issues or damage</option>
</flux:select>
@error('overall_condition')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<!-- Inspection Questionnaire -->
<div>
<flux:field>
<flux:label>Inspection Questionnaire</flux:label>
<flux:description>Rate each vehicle component based on visual inspection</flux:description>
<div class="mt-4 space-y-6">
<!-- Exterior Condition -->
<div class="border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<h4 class="font-medium text-zinc-900 dark:text-zinc-100 mb-3">Exterior Condition</h4>
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.exterior_damage" value="excellent" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Excellent</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.exterior_damage" value="good" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Good</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.exterior_damage" value="fair" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Fair</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.exterior_damage" value="poor" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Poor</span>
</label>
</div>
</div>
<!-- Interior Condition -->
<div class="border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<h4 class="font-medium text-zinc-900 dark:text-zinc-100 mb-3">Interior Condition</h4>
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.interior_condition" value="excellent" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Excellent</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.interior_condition" value="good" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Good</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.interior_condition" value="fair" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Fair</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.interior_condition" value="poor" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Poor</span>
</label>
</div>
</div>
<!-- Tire Condition -->
<div class="border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<h4 class="font-medium text-zinc-900 dark:text-zinc-100 mb-3">Tire Condition</h4>
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.tire_condition" value="excellent" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Excellent</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.tire_condition" value="good" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Good</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.tire_condition" value="fair" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Fair</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.tire_condition" value="poor" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Poor</span>
</label>
</div>
</div>
<!-- Fluid Levels -->
<div class="border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<h4 class="font-medium text-zinc-900 dark:text-zinc-100 mb-3">Fluid Levels</h4>
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.fluid_levels" value="excellent" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Excellent</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.fluid_levels" value="good" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Good</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.fluid_levels" value="fair" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Fair</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.fluid_levels" value="poor" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Poor</span>
</label>
</div>
</div>
<!-- Lights Working -->
<div class="border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<h4 class="font-medium text-zinc-900 dark:text-zinc-100 mb-3">Lights & Electrical</h4>
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.lights_working" value="excellent" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Excellent</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.lights_working" value="good" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Good</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.lights_working" value="fair" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Fair</span>
</label>
<label class="flex items-center">
<input type="radio" wire:model="inspection_checklist.lights_working" value="poor" class="text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm">Poor</span>
</label>
</div>
</div>
</div>
</flux:field>
</div>
<!-- Inspection Notes -->
<div>
<flux:textarea
wire:model="inspection_notes"
label="Inspection Notes"
placeholder="Document any findings, damage, or areas of concern discovered during inspection..."
rows="4"
/>
@error('inspection_notes')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
@endif
</div>
</div>
<!-- Issues & Condition Assessment -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6">Issues & Condition Assessment</flux:heading>
<div class="space-y-6">
<!-- Customer Reported Issues -->
<div>
<flux:textarea
wire:model="customer_reported_issues"
label="Customer Reported Issues"
placeholder="What is the customer reporting? Be specific about symptoms, when they occur, and any relevant details..."
rows="4"
required
<flux:input
type="number"
wire:model="mileage_in"
label="Current Mileage (km)"
placeholder="e.g., 45000"
min="0"
/>
@error('customer_reported_issues')
@error('mileage_in')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<!-- Vehicle Condition Notes -->
<!-- Fuel Level -->
<div>
<flux:select wire:model="fuel_level_in" label="Fuel Level">
<option value="">Select fuel level...</option>
<option value="Empty">Empty (0-10%)</option>
<option value="Low">Low (10-25%)</option>
<option value="Quarter">Quarter (25-40%)</option>
<option value="Half">Half (40-60%)</option>
<option value="Three Quarters">Three Quarters (60-85%)</option>
<option value="Full">Full (85-100%)</option>
</flux:select>
@error('fuel_level_in')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
<!-- Keys Location -->
<div>
<flux:input
wire:model="keys_location"
label="Keys Location"
placeholder="e.g., In vehicle, Key box"
/>
@error('keys_location')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
</div>
</div>
<!-- Issues Assessment -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-6">
<flux:heading size="lg" class="mb-6">Issues Assessment</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Reported Issues -->
<div>
<flux:textarea
wire:model="vehicle_condition_notes"
label="Vehicle Condition Notes"
placeholder="Initial visual inspection findings, exterior/interior condition, existing damage..."
wire:model="customer_reported_issues"
label="Reported Issues"
placeholder="What is the customer reporting? Be specific about symptoms and when they occur..."
rows="4"
required
/>
@error('vehicle_condition_notes')
@error('customer_reported_issues')
<flux:error>{{ $message }}</flux:error>
@enderror
</div>
@ -492,8 +198,8 @@
<flux:textarea
wire:model="notes"
label="Additional Notes"
placeholder="Any special instructions, customer requests, or other relevant information..."
rows="3"
placeholder="Special instructions, customer requests, or other relevant information..."
rows="4"
/>
@error('notes')
<flux:error>{{ $message }}</flux:error>
@ -503,14 +209,30 @@
</div>
<!-- Form Actions -->
<div class="flex flex-col-reverse sm:flex-row sm:justify-end gap-3 pt-6 border-t border-zinc-200 dark:border-zinc-700">
<flux:button href="{{ route('job-cards.index') }}" variant="ghost" size="sm">
Cancel
</flux:button>
<flux:button type="submit" variant="primary" icon="check">
<span wire:loading.remove wire:target="save">Create Job Card</span>
<span wire:loading wire:target="save">Creating...</span>
</flux:button>
<div class="flex justify-between items-center">
<!-- Left Side: Inspection Button (shown after job card is created) -->
<div>
@if(isset($jobCardId))
<flux:button
href="{{ route('inspections.create', ['jobCard' => $jobCardId, 'type' => 'incoming']) }}"
variant="outline"
size="sm"
icon="clipboard-document-check"
>
Perform Initial Inspection
</flux:button>
@endif
</div>
<!-- Right Side: Form Actions -->
<div class="flex space-x-3">
<flux:button href="{{ route('job-cards.index') }}" variant="ghost" size="sm">
Cancel
</flux:button>
<flux:button type="submit" variant="primary" size="sm">
Create Job Card
</flux:button>
</div>
</div>
</form>
</div>

View File

@ -1,387 +1,258 @@
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">Job Cards</flux:heading>
<p class="text-zinc-600 dark:text-zinc-400">Manage vehicle service job cards following the 11-step workflow</p>
</div>
<flux:heading size="xl">Job Cards</flux:heading>
<flux:button href="{{ route('job-cards.create') }}" size="sm">
<flux:icon name="plus" class="size-4" />
New Job Card
</flux:button>
</div>
<!-- Statistics Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-7 gap-4">
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
<svg class="w-4 h-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 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"></path>
</svg>
</div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Total</p>
<p class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{{ $statistics['total'] }}</p>
</div>
<!-- Flash Messages -->
@if (session()->has('success'))
<div class="bg-green-50 border border-green-200 text-green-800 rounded-md p-4">
<div class="flex">
<flux:icon name="check-circle" class="h-5 w-5 text-green-400" />
<div class="ml-3">
<p class="text-sm font-medium">{{ session('success') }}</p>
</div>
</div>
</div>
@endif
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-lg 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Received</p>
<p class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{{ $statistics['received'] }}</p>
</div>
@if (session()->has('error'))
<div class="bg-red-50 border border-red-200 text-red-800 rounded-md p-4">
<div class="flex">
<flux:icon name="exclamation-circle" class="h-5 w-5 text-red-400" />
<div class="ml-3">
<p class="text-sm font-medium">{{ session('error') }}</p>
</div>
</div>
</div>
@endif
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-yellow-100 dark:bg-yellow-900 rounded-lg flex items-center justify-center">
<svg class="w-4 h-4 text-yellow-600 dark:text-yellow-400" 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"></path>
</svg>
</div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">In Progress</p>
<p class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{{ $statistics['in_progress'] }}</p>
<!-- Statistics Cards -->
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
<flux:icon name="document-text" class="size-4 text-blue-600 dark:text-blue-400" />
</div>
</div>
</div>
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-orange-100 dark:bg-orange-900 rounded-lg flex items-center justify-center">
<svg class="w-4 h-4 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Pending Approval</p>
<p class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{{ $statistics['pending_approval'] }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-emerald-100 dark:bg-emerald-900 rounded-lg flex items-center justify-center">
<svg class="w-4 h-4 text-emerald-600 dark:text-emerald-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"></path>
</svg>
</div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Completed Today</p>
<p class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{{ $statistics['completed_today'] }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center">
<svg class="w-4 h-4 text-purple-600 dark:text-purple-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>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Delivered Today</p>
<p class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{{ $statistics['delivered_today'] }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-red-100 dark:bg-red-900 rounded-lg flex items-center justify-center">
<svg class="w-4 h-4 text-red-600 dark:text-red-400" 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 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
</div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Overdue</p>
<p class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{{ $statistics['overdue'] }}</p>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Total</p>
<p class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{{ $statistics['total'] }}</p>
</div>
</div>
</div>
<!-- Enhanced 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 lg:grid-cols-6 gap-4">
<div>
<flux:input
wire:model.live="search"
label="Search"
placeholder="Search job cards..."
/>
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
<flux:icon name="check" class="size-4 text-green-600 dark:text-green-400" />
</div>
</div>
<div>
<flux:select wire:model.live="statusFilter" label="Status" placeholder="All statuses">
@foreach($statusOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</flux:select>
</div>
<div>
<flux:select wire:model.live="branchFilter" label="Branch" placeholder="All branches">
@foreach($branchOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</flux:select>
</div>
<div>
<flux:select wire:model.live="priorityFilter" label="Priority" placeholder="All priorities">
@foreach($priorityOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</flux:select>
</div>
<div>
<flux:select wire:model.live="serviceAdvisorFilter" label="Service Advisor" placeholder="All advisors">
@foreach($serviceAdvisorOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</flux:select>
</div>
<div>
<flux:select wire:model.live="dateRange" label="Date Range" placeholder="All dates">
@foreach($dateRangeOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</flux:select>
<div class="ml-3">
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Received</p>
<p class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{{ $statistics['received'] }}</p>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-end space-x-3">
<flux:button wire:click="refreshData" variant="ghost" size="sm">
<flux:icon name="arrow-path" class="size-4" />
Refresh
</flux:button>
<flux:button wire:click="clearFilters" variant="ghost" size="sm">
<flux:icon name="x-mark" class="size-4" />
Clear Filters
</flux:button>
</div>
<!-- Bulk Actions -->
@if(is_array($selectedJobCards) && count($selectedJobCards) > 0)
<div class="bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="text-blue-700 dark:text-blue-300 font-medium">{{ is_array($selectedJobCards) ? count($selectedJobCards) : 0 }} job card(s) selected</span>
</div>
<div class="flex items-center space-x-2">
<select wire:model="bulkAction" class="px-3 py-2 border border-blue-300 dark:border-blue-600 rounded-lg bg-white dark:bg-blue-800 text-blue-900 dark:text-blue-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">Select action...</option>
<option value="export_csv">Export to CSV</option>
</select>
<button wire:click="processBulkAction" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
Apply
</button>
<button wire:click="$set('selectedJobCards', []); $set('selectAll', false)" class="px-4 py-2 bg-zinc-600 hover:bg-zinc-700 text-white font-medium rounded-lg transition-colors">
Clear
</button>
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-yellow-100 dark:bg-yellow-900 rounded-lg flex items-center justify-center">
<flux:icon name="clock" class="size-4 text-yellow-600 dark:text-yellow-400" />
</div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">In Progress</p>
<p class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{{ $statistics['in_progress'] }}</p>
</div>
</div>
@endif
</div>
<!-- Job Cards List -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg">
@if($jobCards->count() > 0)
<div class="overflow-x-auto">
<table class="w-full divide-y divide-zinc-200 dark:divide-zinc-700">
<thead class="bg-zinc-50 dark:bg-zinc-900">
<tr>
<th class="px-6 py-3 text-left">
<input type="checkbox" wire:model.live="selectAll" class="rounded border-zinc-300 dark:border-zinc-600 text-blue-600 focus:ring-blue-500">
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer" wire:click="sortBy('job_card_number')">
Job Card #
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-orange-100 dark:bg-orange-900 rounded-lg flex items-center justify-center">
<flux:icon name="exclamation-triangle" class="size-4 text-orange-600 dark:text-orange-400" />
</div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Pending Approval</p>
<p class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{{ $statistics['pending_approval'] }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center">
<flux:icon name="check-badge" class="size-4 text-purple-600 dark:text-purple-400" />
</div>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Completed Today</p>
<p class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{{ $statistics['completed_today'] }}</p>
</div>
</div>
</div>
</div>
<!-- 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"
/>
<flux:select wire:model.live="statusFilter" placeholder="All Statuses">
@foreach($statusOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</flux:select>
<flux:select wire:model.live="branchFilter" placeholder="All Branches">
@foreach($branchOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</flux:select>
</div>
</div>
<!-- Job Cards Table -->
<div class="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
<flux:heading size="lg">Job Cards</flux:heading>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
Showing {{ $jobCards->count() }} of {{ $jobCards->total() }} job cards
</p>
</div>
@if($jobCards->count() > 0)
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-zinc-200 dark:border-zinc-700">
<th class="text-left py-3 px-4">
<button wire:click="sortBy('job_card_number')" class="flex items-center space-x-1 hover:text-blue-600">
<span>Job Card</span>
@if($sortBy === 'job_card_number')
<span class="ml-1">{{ $sortDirection === 'asc' ? '↑' : '↓' }}</span>
<flux:icon name="{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="size-3" />
@endif
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Customer</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Vehicle</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer" wire:click="sortBy('status')">
Status
@if($sortBy === 'status')
<span class="ml-1">{{ $sortDirection === 'asc' ? '↑' : '↓' }}</span>
</button>
</th>
<th class="text-left py-3 px-4">Customer</th>
<th class="text-left py-3 px-4">Vehicle</th>
<th class="text-left py-3 px-4">Status</th>
<th class="text-left py-3 px-4">
<button wire:click="sortBy('created_at')" class="flex items-center space-x-1 hover:text-blue-600">
<span>Date Created</span>
@if($sortBy === 'created_at')
<flux:icon name="{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="size-3" />
@endif
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer" wire:click="sortBy('priority')">
Priority
@if($sortBy === 'priority')
<span class="ml-1">{{ $sortDirection === 'asc' ? '↑' : '↓' }}</span>
@endif
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider cursor-pointer" wire:click="sortBy('arrival_datetime')">
Arrival Date
@if($sortBy === 'arrival_datetime')
<span class="ml-1">{{ $sortDirection === 'asc' ? '↑' : '↓' }}</span>
@endif
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Service Advisor</th>
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-zinc-800 divide-y divide-zinc-200 dark:divide-zinc-700">
@foreach($jobCards as $jobCard)
<tr class="hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors">
<td class="px-6 py-4">
<input type="checkbox" wire:model.live="selectedJobCards" value="{{ $jobCard->id }}" class="rounded border-zinc-300 dark:border-zinc-600 text-blue-600 focus:ring-blue-500">
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
<a href="{{ route('job-cards.show', $jobCard) }}" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200">
{{ $jobCard->job_card_number }}
</a>
</div>
</button>
</th>
<th class="text-left py-3 px-4">Actions</th>
</tr>
</thead>
<tbody>
@foreach($jobCards as $jobCard)
<tr class="border-b border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-700">
<td class="py-3 px-4">
<div>
<div class="font-medium text-zinc-900 dark:text-zinc-100">
#{{ $jobCard->job_card_number }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-zinc-900 dark:text-zinc-100">
{{ $jobCard->customer->first_name ?? '' }} {{ $jobCard->customer->last_name ?? '' }}
<div class="text-sm text-zinc-500 dark:text-zinc-400">
{{ $jobCard->branch_code }}
</div>
</div>
</td>
<td class="py-3 px-4">
<div>
<div class="text-zinc-900 dark:text-zinc-100">
{{ $jobCard->customer->name ?? 'Unknown Customer' }}
</div>
<div class="text-sm text-zinc-500 dark:text-zinc-400">
{{ $jobCard->customer->phone ?? '' }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-zinc-900 dark:text-zinc-100">
{{ $jobCard->vehicle->year ?? '' }} {{ $jobCard->vehicle->make ?? '' }} {{ $jobCard->vehicle->model ?? '' }}
</div>
</td>
<td class="py-3 px-4">
<div>
<div class="text-zinc-900 dark:text-zinc-100">
{{ $jobCard->vehicle->make ?? '' }} {{ $jobCard->vehicle->model ?? '' }}
</div>
<div class="text-sm text-zinc-500 dark:text-zinc-400">
{{ $jobCard->vehicle->license_plate ?? '' }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@php
$statusClasses = [
'received' => 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
'inspected' => 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
'assigned_for_diagnosis' => 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
'in_diagnosis' => 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
'estimate_sent' => 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
'approved' => 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200',
'parts_procurement' => 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200',
'in_progress' => 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
'completed' => 'bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200',
'delivered' => 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
];
$statusClass = $statusClasses[$jobCard->status] ?? 'bg-zinc-100 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200';
@endphp
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full {{ $statusClass }}">
{{ $statusOptions[$jobCard->status] ?? $jobCard->status }}
</span>
<!-- Workflow Progress Indicator -->
@php
$workflowSteps = [
'received' => 1,
'inspected' => 2,
'assigned_for_diagnosis' => 3,
'in_diagnosis' => 4,
'estimate_sent' => 5,
'approved' => 6,
'parts_procurement' => 7,
'in_progress' => 8,
'completed' => 9,
'delivered' => 10,
];
$currentStep = $workflowSteps[$jobCard->status] ?? 1;
$progress = ($currentStep / 10) * 100;
@endphp
<div class="mt-1 w-full bg-zinc-200 dark:bg-zinc-700 rounded-full h-1">
<div class="bg-blue-600 h-1 rounded-full" style="width: {{ $progress }}%"></div>
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Step {{ $currentStep }}/10</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@php
$priorityClasses = [
'low' => 'bg-zinc-100 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200',
'medium' => 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
'high' => 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
'urgent' => 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
];
$priorityClass = $priorityClasses[$jobCard->priority] ?? 'bg-zinc-100 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200';
@endphp
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full {{ $priorityClass }}">
{{ ucfirst($jobCard->priority) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">
{{ $jobCard->arrival_datetime ? $jobCard->arrival_datetime->format('M j, Y g:i A') : '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">
{{ $jobCard->serviceAdvisor->name ?? '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center space-x-2">
<a href="{{ route('job-cards.show', $jobCard) }}" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-200">
View
</a>
@can('update', $jobCard)
<a href="{{ route('job-cards.edit', $jobCard) }}" class="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200">
Edit
</a>
@endcan
<a href="{{ route('job-cards.workflow', $jobCard) }}" class="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-200">
Workflow
</a>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</td>
<td class="py-3 px-4">
<flux:badge
size="sm"
:color="match($jobCard->status) {
'received' => 'blue',
'in_diagnosis', 'in_progress', 'parts_procurement' => 'yellow',
'completed' => 'green',
'delivered' => 'purple',
default => 'zinc'
}"
>
{{ $statusOptions[$jobCard->status] ?? $jobCard->status }}
</flux:badge>
</td>
<td class="py-3 px-4">
<div class="text-sm text-zinc-500 dark:text-zinc-400">
{{ $jobCard->created_at->format('M j, Y') }}
</div>
</td>
<td class="py-3 px-4">
<div class="flex items-center gap-2">
<flux:button size="xs" variant="ghost" href="{{ route('job-cards.show', $jobCard) }}">
<flux:icon name="eye" class="size-4" />
View
</flux:button>
<flux:button size="xs" variant="ghost" href="{{ route('job-cards.edit', $jobCard) }}">
<flux:icon name="pencil" class="size-4" />
Edit
</flux:button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="px-6 py-4 border-t border-zinc-200 dark:border-zinc-700">
{{ $jobCards->links() }}
<!-- Pagination -->
<div class="px-6 py-4 border-t border-zinc-200 dark:border-zinc-700">
{{ $jobCards->links() }}
</div>
@else
<div class="text-center py-12">
<flux:icon name="document-text" class="mx-auto size-12 text-zinc-400" />
<flux:heading size="lg" class="mt-4">No job cards found</flux:heading>
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">Get started by creating your first job card.</p>
<div class="mt-6">
<flux:button href="{{ route('job-cards.create') }}">
<flux:icon name="plus" class="size-4" />
New Job Card
</flux:button>
</div>
@else
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-500" 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"></path>
</svg>
<h3 class="mt-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">No job cards found</h3>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">Try adjusting your search criteria or create a new job card.</p>
<div class="mt-6">
<a href="{{ route('job-cards.create') }}" 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
New Job Card
</a>
</div>
</div>
@endif
</div>

View File

@ -46,6 +46,34 @@
</svg>
Edit
</a>
<!-- 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" />
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" />
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">
<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">
<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
</a>
@endif
@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">
@ -372,6 +400,32 @@
<div class="p-6">
<div class="space-y-4">
@if($jobCard->status === 'received')
<a href="{{ route('inspections.create', ['jobCard' => $jobCard, 'type' => 'incoming']) }}" class="w-full inline-flex items-center justify-center px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-semibold rounded-lg transition-all duration-200 shadow-lg">
<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="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>
Perform Initial Inspection
</a>
<div class="bg-amber-50 dark:bg-amber-900/50 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-amber-400" viewBox="0 0 20 20" fill="currentColor">
<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" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-amber-800 dark:text-amber-200">
Initial Inspection Required
</h3>
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>Complete the initial vehicle inspection before proceeding to diagnosis.</p>
</div>
</div>
</div>
</div>
@endif
@if($jobCard->status === 'inspected')
<a href="{{ route('diagnosis.create', $jobCard) }}" class="w-full inline-flex items-center justify-center px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-semibold rounded-lg transition-all duration-200 shadow-lg">
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
@ -503,4 +557,43 @@
</div>
</div>
</div>
<!-- Technician Assignment Modal -->
@if($showAssignmentModal)
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-zinc-800 rounded-lg p-6 w-full max-w-md mx-4">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-zinc-900 dark:text-zinc-100">Assign Technician for Diagnosis</h3>
<button wire:click="closeAssignmentModal" class="text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300">
<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>
</div>
<form wire:submit="assignForDiagnosis">
<div class="mb-6">
<flux:field>
<flux:label>Select Technician</flux:label>
<flux:select wire:model="selectedTechnicianId" placeholder="Choose a technician...">
@foreach($availableTechnicians as $technician)
<option value="{{ $technician->id }}">{{ $technician->name }}</option>
@endforeach
</flux:select>
<flux:error name="selectedTechnicianId" />
</flux:field>
</div>
<div class="flex justify-end space-x-3">
<flux:button wire:click="closeAssignmentModal" variant="ghost">
Cancel
</flux:button>
<flux:button type="submit" variant="filled" color="blue">
Assign Technician
</flux:button>
</div>
</form>
</div>
</div>
@endif
</div>

View File

@ -1,15 +1,15 @@
<?php
use App\Http\Controllers\CustomerController;
use App\Http\Controllers\ServiceOrderController;
use App\Http\Controllers\VehicleController;
use Illuminate\Support\Facades\Route;
use Livewire\Volt\Volt;
use App\Http\Controllers\CustomerController;
use App\Http\Controllers\VehicleController;
use App\Http\Controllers\ServiceOrderController;
Route::get('/', function () {
if (auth()->check()) {
$user = auth()->user();
// Check if user is a customer
if ($user->isCustomer()) {
// Redirect customers to customer portal
@ -19,7 +19,7 @@ Route::get('/', function () {
return redirect('/dashboard');
}
}
// For guests, redirect to login page instead of showing welcome
return redirect('/login');
})->name('home');
@ -33,21 +33,21 @@ Route::middleware(['auth', 'admin.only'])->group(function () {
Route::redirect('settings', 'settings/general');
Volt::route('settings/profile', 'settings.profile')->name('settings.profile');
Volt::route('settings/password', 'settings.password')->name('settings.password');
// Application Settings Routes
Route::prefix('settings')->name('settings.')->group(function () {
Route::get('/general', [App\Http\Controllers\SettingsController::class, 'general'])->name('general');
Route::put('/general', [App\Http\Controllers\SettingsController::class, 'updateGeneral'])->name('general.update');
Route::get('/service', [App\Http\Controllers\SettingsController::class, 'service'])->name('service');
Route::put('/service', [App\Http\Controllers\SettingsController::class, 'updateService'])->name('service.update');
Route::get('/inventory', [App\Http\Controllers\SettingsController::class, 'inventory'])->name('inventory');
Route::put('/inventory', [App\Http\Controllers\SettingsController::class, 'updateInventory'])->name('inventory.update');
Route::get('/notifications', [App\Http\Controllers\SettingsController::class, 'notifications'])->name('notifications');
Route::put('/notifications', [App\Http\Controllers\SettingsController::class, 'updateNotifications'])->name('notifications.update');
Route::get('/security', [App\Http\Controllers\SettingsController::class, 'security'])->name('security');
Route::put('/security', [App\Http\Controllers\SettingsController::class, 'updateSecurity'])->name('security.update');
});
@ -57,33 +57,33 @@ Route::middleware(['auth', 'admin.only'])->group(function () {
Route::resource('customers', CustomerController::class)->middleware('permission:customers.view');
Route::resource('vehicles', VehicleController::class)->middleware('permission:vehicles.view');
Route::resource('service-orders', ServiceOrderController::class)->middleware('permission:service-orders.view');
// Additional Service Order Routes
Route::get('/service-orders/{serviceOrder}/invoice', [ServiceOrderController::class, 'invoice'])
->middleware('permission:service-orders.view')
->name('service-orders.invoice');
// Livewire Component Routes
Route::get('/customers-list', function () {
return view('customers.index');
})->middleware('permission:customers.view')->name('customers.list');
Route::get('/service-orders-list', function () {
return view('service-orders.index');
})->middleware('permission:service-orders.view')->name('service-orders.list');
Route::get('/vehicles', function () {
return view('vehicles.index');
})->middleware('permission:vehicles.view')->name('vehicles.index');
Route::get('/appointments', function () {
return view('appointments.index');
})->middleware('permission:appointments.view')->name('appointments.index');
Route::get('/service-items', function () {
return view('service-items.index');
})->middleware('permission:service-orders.view')->name('service-items.index');
// Appointments Management Routes
Route::prefix('appointments')->name('appointments.')->middleware('permission:appointments.view')->group(function () {
Route::get('/', \App\Livewire\Appointments\Index::class)->name('index');
@ -91,15 +91,15 @@ Route::middleware(['auth', 'admin.only'])->group(function () {
Route::get('/{appointment}/edit', \App\Livewire\Appointments\Form::class)->middleware('permission:appointments.update')->name('edit');
Route::get('/calendar', \App\Livewire\Appointments\Calendar::class)->name('calendar');
});
Route::get('/inventory', [App\Http\Controllers\InventoryController::class, 'index'])
->middleware('permission:inventory.view')
->name('inventory.index');
// Inventory Management Routes
Route::prefix('inventory')->name('inventory.')->middleware('permission:inventory.view')->group(function () {
Route::get('/dashboard', \App\Livewire\Inventory\Dashboard::class)->name('dashboard');
// Parts Routes
Route::prefix('parts')->name('parts.')->group(function () {
Route::get('/', \App\Livewire\Inventory\Parts\Index::class)->name('index');
@ -107,14 +107,14 @@ Route::middleware(['auth', 'admin.only'])->group(function () {
Route::get('/{part}', \App\Livewire\Inventory\Parts\Show::class)->name('show');
Route::get('/{part}/edit', \App\Livewire\Inventory\Parts\Edit::class)->middleware('permission:inventory.update')->name('edit');
});
// Suppliers Routes
Route::prefix('suppliers')->name('suppliers.')->group(function () {
Route::get('/', \App\Livewire\Inventory\Suppliers\Index::class)->name('index');
Route::get('/create', \App\Livewire\Inventory\Suppliers\Create::class)->middleware('permission:inventory.create')->name('create');
Route::get('/{supplier}/edit', \App\Livewire\Inventory\Suppliers\Edit::class)->middleware('permission:inventory.update')->name('edit');
});
// Purchase Orders Routes
Route::prefix('purchase-orders')->name('purchase-orders.')->middleware('permission:inventory.purchase-orders')->group(function () {
Route::get('/', \App\Livewire\Inventory\PurchaseOrders\Index::class)->name('index');
@ -122,42 +122,48 @@ Route::middleware(['auth', 'admin.only'])->group(function () {
Route::get('/{purchaseOrder}/edit', \App\Livewire\Inventory\PurchaseOrders\Edit::class)->name('edit');
Route::get('/{purchaseOrder}', \App\Livewire\Inventory\PurchaseOrders\Show::class)->name('show');
});
// Stock Movements Routes
Route::prefix('stock-movements')->name('stock-movements.')->middleware('permission:inventory.stock-movements')->group(function () {
Route::get('/', \App\Livewire\Inventory\StockMovements\Index::class)->name('index');
Route::get('/create', \App\Livewire\Inventory\StockMovements\Create::class)->name('create');
});
});
Route::get('/technicians', function () {
return view('technicians.index');
})->middleware('permission:technicians.view')->name('technicians.index');
// Technician Management Routes
Route::prefix('technician')->name('technician.')->middleware('permission:technicians.view')->group(function () {
Route::get('/management', function () {
return view('technician-management');
})->middleware('permission:technicians.update')->name('management');
Route::get('/skills', function () {
return view('technician-skills');
})->name('skills');
Route::get('/reports', function () {
return view('technician-reports');
})->middleware('permission:technicians.view-performance')->name('reports');
});
// Job Card Management Routes
Route::prefix('job-cards')->name('job-cards.')->middleware('permission:job-cards.view')->group(function () {
Route::get('/', \App\Livewire\JobCards\Index::class)->name('index');
Route::get('/test', function () {
$component = new \App\Livewire\JobCards\Index;
$component->mount();
return $component->render();
})->name('test');
Route::get('/create', \App\Livewire\JobCards\Create::class)->middleware('permission:job-cards.create')->name('create');
Route::get('/{jobCard}', \App\Livewire\JobCards\Show::class)->name('show');
Route::get('/{jobCard}/edit', \App\Livewire\JobCards\Edit::class)->middleware('permission:job-cards.update')->name('edit');
Route::get('/{jobCard}/workflow', \App\Livewire\JobCards\WorkflowStatus::class)->name('workflow');
});
// Diagnosis Routes
Route::prefix('diagnosis')->name('diagnosis.')->middleware('permission:job-cards.view')->group(function () {
Route::get('/', \App\Livewire\Diagnosis\Index::class)->name('index');
@ -165,16 +171,17 @@ Route::middleware(['auth', 'admin.only'])->group(function () {
Route::get('/{diagnosis}', \App\Livewire\Diagnosis\Show::class)->name('show');
Route::get('/{diagnosis}/edit', \App\Livewire\Diagnosis\Edit::class)->middleware('permission:job-cards.update')->name('edit');
});
// Estimates Routes
Route::prefix('estimates')->name('estimates.')->middleware('permission:service-orders.view')->group(function () {
Route::get('/', \App\Livewire\Estimates\Index::class)->name('index');
Route::get('/create', \App\Livewire\Estimates\CreateStandalone::class)->middleware('permission:service-orders.create')->name('create-standalone');
Route::get('/create/{diagnosis}', \App\Livewire\Estimates\Create::class)->middleware('permission:service-orders.create')->name('create');
Route::get('/{estimate}', \App\Livewire\Estimates\Show::class)->name('show');
Route::get('/{estimate}/edit', \App\Livewire\Estimates\Edit::class)->middleware('permission:service-orders.update')->name('edit');
Route::get('/{estimate}/pdf', \App\Livewire\Estimates\PDF::class)->name('pdf');
});
// Work Orders Routes
Route::prefix('work-orders')->name('work-orders.')->middleware('permission:service-orders.view')->group(function () {
Route::get('/', \App\Livewire\WorkOrders\Index::class)->name('index');
@ -182,15 +189,16 @@ Route::middleware(['auth', 'admin.only'])->group(function () {
Route::get('/{workOrder}', \App\Livewire\WorkOrders\Show::class)->name('show');
Route::get('/{workOrder}/edit', \App\Livewire\WorkOrders\Edit::class)->middleware('permission:service-orders.update')->name('edit');
});
// Inspections Routes
Route::prefix('inspections')->name('inspections.')->middleware('permission:job-cards.view')->group(function () {
Route::get('/', \App\Livewire\Inspections\Index::class)->name('index');
Route::get('/create/{jobCard}/{type}', \App\Livewire\Inspections\Create::class)->middleware('permission:job-cards.update')->name('create');
Route::get('/{inspection}', \App\Livewire\Inspections\Show::class)->name('show');
Route::get('/{inspection}/print', \App\Livewire\Inspections\PrintView::class)->name('print');
Route::get('/{inspection}/edit', \App\Livewire\Inspections\Edit::class)->middleware('permission:job-cards.update')->name('edit');
});
// Timesheets Routes
Route::prefix('timesheets')->name('timesheets.')->middleware('permission:technicians.view')->group(function () {
Route::get('/', \App\Livewire\Timesheets\Index::class)->name('index');
@ -198,17 +206,17 @@ Route::middleware(['auth', 'admin.only'])->group(function () {
Route::get('/{timesheet}', \App\Livewire\Timesheets\Show::class)->name('show');
Route::get('/{timesheet}/edit', \App\Livewire\Timesheets\Edit::class)->name('edit');
});
// Reports Dashboard Route
Route::view('reports', 'reports')->middleware(['auth', 'permission:reports.view'])->name('reports.index');
// Branch Management Routes
Route::prefix('branches')->name('branches.')->middleware('permission:branches.view')->group(function () {
Route::get('/', \App\Livewire\Branches\Index::class)->name('index');
Route::get('/create', \App\Livewire\Branches\Create::class)->middleware('permission:branches.create')->name('create');
Route::get('/{branch}/edit', \App\Livewire\Branches\Edit::class)->middleware('permission:branches.edit')->name('edit');
});
// User Management Routes
Route::prefix('users')->name('users.')->middleware('permission:users.view')->group(function () {
Route::get('/', \App\Livewire\Users\Index::class)->name('index');
@ -217,7 +225,7 @@ Route::middleware(['auth', 'admin.only'])->group(function () {
Route::get('/{user}/edit', \App\Livewire\Users\Edit::class)->middleware('permission:users.update')->name('edit');
Route::get('/{user}/manage-roles', \App\Livewire\Users\ManageRolesPermissions::class)->middleware('permission:users.manage-roles')->name('manage-roles');
});
// Legacy User Management Route (redirect to new structure)
Route::get('/user-management', function () {
return redirect()->route('users.index');

14
test-inspection.blade.php Normal file
View File

@ -0,0 +1,14 @@
<div>
<flux:select wire:model="fuel_level" label="Fuel Level" required>
<option value="">Select fuel level...</option>
<option value="empty">Empty (0-10%)</option>
<option value="low">Low (10-25%)</option>
<option value="quarter">Quarter (25-40%)</option>
<option value="half">Half (40-60%)</option>
<option value="three_quarter">Three Quarter (60-85%)</option>
<option value="full">Full (85-100%)</option>
</flux:select>
@if(true)
<p>Test content</p>
@endif
</div>

View File

@ -0,0 +1,356 @@
<?php
namespace Tests\Feature;
use App\Livewire\Estimates\Create;
use App\Livewire\Estimates\Edit;
use App\Livewire\Estimates\Show;
use App\Models\Branch;
use App\Models\Customer;
use App\Models\Diagnosis;
use App\Models\Estimate;
use App\Models\JobCard;
use App\Models\User;
use App\Models\Vehicle;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
class EstimateModuleTest extends TestCase
{
use RefreshDatabase;
private User $user;
private Branch $branch;
private Customer $customer;
private Vehicle $vehicle;
private JobCard $jobCard;
private Diagnosis $diagnosis;
protected function setUp(): void
{
parent::setUp();
// Create test data using factories
$this->user = User::factory()->create([
'role' => 'service_coordinator',
]);
// Give user required permissions for estimates
$permissions = [
['name' => 'service-orders.view', 'display_name' => 'View Service Orders', 'module' => 'service-orders'],
['name' => 'service-orders.create', 'display_name' => 'Create Service Orders', 'module' => 'service-orders'],
['name' => 'service-orders.update', 'display_name' => 'Update Service Orders', 'module' => 'service-orders'],
];
foreach ($permissions as $permissionData) {
$permission = \App\Models\Permission::firstOrCreate(
['name' => $permissionData['name']],
[
'display_name' => $permissionData['display_name'],
'description' => $permissionData['display_name'],
'module' => $permissionData['module'],
]
);
$this->user->permissions()->attach($permission);
}
$this->branch = Branch::create([
'name' => 'Main Branch',
'code' => 'MAIN',
'address' => '123 Test St',
'phone' => '555-0123',
'email' => 'main@test.com',
]);
$this->customer = Customer::factory()->create();
$this->vehicle = Vehicle::factory()->create([
'customer_id' => $this->customer->id,
]);
$this->jobCard = JobCard::factory()->create([
'customer_id' => $this->customer->id,
'vehicle_id' => $this->vehicle->id,
'branch_code' => $this->branch->code,
'status' => 'in_diagnosis',
'service_advisor_id' => $this->user->id,
]);
$this->diagnosis = Diagnosis::create([
'job_card_id' => $this->jobCard->id,
'service_coordinator_id' => $this->user->id,
'diagnostic_findings' => 'Worn brake pads detected',
'labor_operations' => [
[
'operation' => 'Replace front brake pads',
'estimated_hours' => 2.0,
'labor_rate' => 85.00,
],
],
'parts_required' => [
[
'part_name' => 'Front brake pads',
'part_number' => 'BP-TOY-001',
'quantity' => 1,
'estimated_cost' => 75.00,
],
],
'estimated_repair_time' => 2.5,
'diagnosis_status' => 'completed',
]);
$this->actingAs($this->user);
}
/** @test */
public function it_can_create_an_estimate_from_diagnosis()
{
Livewire::test(Create::class, ['diagnosis' => $this->diagnosis])
->assertSet('diagnosis.id', $this->diagnosis->id)
->assertViewHas('diagnosis')
->call('save')
->assertHasNoErrors()
->assertRedirect();
$this->assertDatabaseHas('estimates', [
'job_card_id' => $this->jobCard->id,
'diagnosis_id' => $this->diagnosis->id,
'prepared_by_id' => $this->user->id,
'status' => 'draft',
]);
// Check that line items were created
$estimate = Estimate::latest()->first();
$this->assertGreaterThan(0, $estimate->lineItems()->count());
// Check that totals were calculated
$this->assertGreaterThan(0, $estimate->total_amount);
}
/** @test */
public function it_calculates_totals_correctly()
{
$component = Livewire::test(Create::class, ['diagnosis' => $this->diagnosis]);
// Verify initial line items from diagnosis
$lineItems = $component->get('lineItems');
$this->assertNotEmpty($lineItems);
// Check that totals are calculated
$subtotal = $component->get('subtotal');
$taxAmount = $component->get('tax_amount');
$totalAmount = $component->get('total_amount');
$this->assertGreaterThan(0, $subtotal);
$this->assertGreaterThan(0, $taxAmount);
$this->assertGreaterThan(0, $totalAmount);
// Verify total calculation: subtotal + tax = total
$expectedTotal = $subtotal + $taxAmount;
$this->assertEquals($expectedTotal, $totalAmount);
}
/** @test */
public function it_can_add_and_remove_line_items()
{
$component = Livewire::test(Create::class, ['diagnosis' => $this->diagnosis]);
$initialCount = count($component->get('lineItems'));
// Add a line item
$component->call('addLineItem');
$this->assertCount($initialCount + 1, $component->get('lineItems'));
// Remove a line item
$component->call('removeLineItem', 0);
$this->assertCount($initialCount, $component->get('lineItems'));
}
/** @test */
public function it_validates_required_fields()
{
Livewire::test(Create::class, ['diagnosis' => $this->diagnosis])
->set('terms_and_conditions', '')
->set('validity_period_days', 0)
->set('tax_rate', -1)
->call('save')
->assertHasErrors([
'terms_and_conditions',
'validity_period_days',
'tax_rate',
]);
}
/** @test */
public function it_can_view_an_estimate()
{
$estimate = Estimate::create([
'estimate_number' => 'MAIN/EST0001',
'job_card_id' => $this->jobCard->id,
'diagnosis_id' => $this->diagnosis->id,
'prepared_by_id' => $this->user->id,
'labor_cost' => 170.00,
'parts_cost' => 90.00,
'subtotal' => 260.00,
'tax_rate' => 8.25,
'tax_amount' => 21.45,
'total_amount' => 281.45,
'validity_period_days' => 30,
'terms_and_conditions' => 'Test terms',
'status' => 'draft',
]);
Livewire::test(Show::class, ['estimate' => $estimate])
->assertSet('estimate.id', $estimate->id)
->assertSee($estimate->estimate_number)
->assertSee('$281.45')
->assertViewHas('estimate');
}
/** @test */
public function it_can_edit_an_estimate()
{
$estimate = Estimate::factory()->create([
'job_card_id' => $this->jobCard->id,
'terms_and_conditions' => 'Original terms',
'validity_period_days' => 30,
]);
// Test the component functionality directly without layout rendering
$component = Livewire::test(\App\Livewire\Estimates\Edit::class, ['estimate' => $estimate])
->set('terms_and_conditions', 'Updated terms and conditions')
->set('validity_period_days', 45);
// Assert the properties are set correctly
$component->assertSet('terms_and_conditions', 'Updated terms and conditions')
->assertSet('validity_period_days', 45);
// Call save method
$component->call('save')
->assertHasNoErrors();
$this->assertDatabaseHas('estimates', [
'id' => $estimate->id,
'terms_and_conditions' => 'Updated terms and conditions',
'validity_period_days' => 45,
]);
}
/** @test */
public function it_generates_unique_estimate_numbers()
{
$initialCount = Estimate::count();
// Create first estimate
Livewire::test(Create::class, ['diagnosis' => $this->diagnosis])
->call('save');
$this->assertEquals($initialCount + 1, Estimate::count());
$firstEstimate = Estimate::latest()->first();
// Create another diagnosis for second estimate
$diagnosis2 = Diagnosis::create([
'job_card_id' => $this->jobCard->id,
'service_coordinator_id' => $this->user->id,
'diagnostic_findings' => 'Different issue',
'diagnosis_status' => 'completed',
]);
// Create second estimate
Livewire::test(Create::class, ['diagnosis' => $diagnosis2])
->call('save');
$this->assertEquals($initialCount + 2, Estimate::count());
$estimates = Estimate::latest()->take(2)->get();
$secondEstimate = $estimates->first();
$firstEstimate = $estimates->last();
$this->assertNotEquals($firstEstimate->estimate_number, $secondEstimate->estimate_number);
$this->assertStringContainsString('MAIN/EST', $firstEstimate->estimate_number);
$this->assertStringContainsString('MAIN/EST', $secondEstimate->estimate_number);
}
/** @test */
public function it_updates_job_card_status_when_estimate_created()
{
$this->assertEquals('in_diagnosis', $this->jobCard->status);
Livewire::test(Create::class, ['diagnosis' => $this->diagnosis])
->call('save');
$this->jobCard->refresh();
$this->assertEquals('estimate_prepared', $this->jobCard->status);
}
/** @test */
public function estimate_routes_are_accessible()
{
// Skip permission checks for this test by using an admin role
$this->user->update(['role' => 'admin']);
$estimate = Estimate::create([
'estimate_number' => 'MAIN/EST0001',
'job_card_id' => $this->jobCard->id,
'diagnosis_id' => $this->diagnosis->id,
'prepared_by_id' => $this->user->id,
'labor_cost' => 170.00,
'parts_cost' => 90.00,
'subtotal' => 260.00,
'tax_rate' => 8.25,
'tax_amount' => 21.45,
'total_amount' => 281.45,
'validity_period_days' => 30,
'terms_and_conditions' => 'Test terms',
'status' => 'draft',
]);
// Test index route
$this->actingAs($this->user)
->get('/estimates')
->assertStatus(200);
// Test show route
$this->actingAs($this->user)
->get("/estimates/{$estimate->id}")
->assertStatus(200);
// Test edit route
$this->get("/estimates/{$estimate->id}/edit")
->assertStatus(200);
// Test create route
$this->get("/estimates/create/{$this->diagnosis->id}")
->assertStatus(200);
}
/** @test */
public function it_can_calculate_estimate_expiration()
{
$estimate = Estimate::create([
'estimate_number' => 'MAIN/EST0001',
'job_card_id' => $this->jobCard->id,
'diagnosis_id' => $this->diagnosis->id,
'prepared_by_id' => $this->user->id,
'labor_cost' => 170.00,
'parts_cost' => 90.00,
'subtotal' => 260.00,
'tax_rate' => 8.25,
'tax_amount' => 21.45,
'total_amount' => 281.45,
'validity_period_days' => 30,
'terms_and_conditions' => 'Test terms',
'status' => 'draft',
]);
$expectedValidUntil = $estimate->created_at->addDays(30);
$this->assertEquals($expectedValidUntil->format('Y-m-d'), $estimate->valid_until->format('Y-m-d'));
// Test if estimate is not expired (fresh estimate)
$this->assertFalse($estimate->is_expired);
}
}

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Models\Customer;
use App\Models\Estimate;
use App\Models\User;
use App\Models\Vehicle;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class EstimatesTest extends TestCase
{
use RefreshDatabase;
public function test_estimate_model_relationships_work(): void
{
// Create test data
$customer = Customer::factory()->create();
$vehicle = Vehicle::factory()->create(['customer_id' => $customer->id]);
$user = User::factory()->create();
// Create standalone estimate
$estimate = Estimate::factory()->create([
'customer_id' => $customer->id,
'vehicle_id' => $vehicle->id,
'prepared_by_id' => $user->id,
'job_card_id' => null,
'diagnosis_id' => null,
]);
// Test relationships
$this->assertEquals($customer->id, $estimate->customer->id);
$this->assertEquals($vehicle->id, $estimate->vehicle->id);
$this->assertEquals($user->id, $estimate->preparedBy->id);
$this->assertNull($estimate->job_card_id);
$this->assertNull($estimate->diagnosis_id);
}
public function test_estimate_customer_and_vehicle_accessors(): void
{
$customer = Customer::factory()->create();
$vehicle = Vehicle::factory()->create(['customer_id' => $customer->id]);
$user = User::factory()->create();
// Create standalone estimate
$estimate = Estimate::factory()->create([
'customer_id' => $customer->id,
'vehicle_id' => $vehicle->id,
'prepared_by_id' => $user->id,
'job_card_id' => null,
'diagnosis_id' => null,
]);
// Test customer accessor (should return direct customer)
$this->assertEquals($customer->name, $estimate->estimate_customer->name);
// Test vehicle accessor (should return direct vehicle)
$this->assertEquals($vehicle->make, $estimate->estimate_vehicle->make);
}
public function test_estimate_stats_calculation_with_different_statuses(): void
{
$customer = Customer::factory()->create();
$vehicle = Vehicle::factory()->create(['customer_id' => $customer->id]);
$user = User::factory()->create();
// Create estimates with different statuses
Estimate::factory()->create([
'customer_id' => $customer->id,
'vehicle_id' => $vehicle->id,
'prepared_by_id' => $user->id,
'job_card_id' => null,
'diagnosis_id' => null,
'status' => 'draft',
'total_amount' => 100.00,
]);
Estimate::factory()->create([
'customer_id' => $customer->id,
'vehicle_id' => $vehicle->id,
'prepared_by_id' => $user->id,
'job_card_id' => null,
'diagnosis_id' => null,
'status' => 'sent',
'total_amount' => 200.00,
]);
Estimate::factory()->create([
'customer_id' => $customer->id,
'vehicle_id' => $vehicle->id,
'prepared_by_id' => $user->id,
'job_card_id' => null,
'diagnosis_id' => null,
'status' => 'approved',
'total_amount' => 300.00,
]);
// Verify the estimates were created with correct status and amounts
$this->assertEquals(1, Estimate::where('status', 'draft')->count());
$this->assertEquals(1, Estimate::where('status', 'sent')->count());
$this->assertEquals(1, Estimate::where('status', 'approved')->count());
$this->assertEquals(600.00, Estimate::sum('total_amount'));
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
class JobCardsIndexTest extends TestCase
{
/**
* A basic feature test example.
*/
public function test_example(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}