feat: Enhance job card workflow with diagnosis actions and technician assignment modal
- Added buttons for assigning diagnosis and starting diagnosis based on job card status in the job card view. - Implemented a modal for assigning technicians for diagnosis, including form validation and technician selection. - Updated routes to include a test route for job cards. - Created a new Blade view for testing inspection inputs. - Developed comprehensive feature tests for the estimate module, including creation, viewing, editing, and validation of estimates. - Added tests for estimate model relationships and statistics calculations. - Introduced a basic feature test for job cards index.
This commit is contained in:
446
.github/copilot-instructions.md
vendored
446
.github/copilot-instructions.md
vendored
@ -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>
|
||||
@ -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',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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!');
|
||||
}
|
||||
|
||||
|
||||
286
app/Livewire/Estimates/CreateStandalone.php
Normal file
286
app/Livewire/Estimates/CreateStandalone.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
25
app/Livewire/Inspections/Print.php
Normal file
25
app/Livewire/Inspections/Print.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
25
app/Livewire/Inspections/PrintView.php
Normal file
25
app/Livewire/Inspections/PrintView.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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}";
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
158
app/Models/Estimate.php.backup
Normal file
158
app/Models/Estimate.php.backup
Normal 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;
|
||||
}
|
||||
}
|
||||
159
app/Models/Estimate.php.broken
Normal file
159
app/Models/Estimate.php.broken
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
77
app/Policies/EstimatePolicy.php
Normal file
77
app/Policies/EstimatePolicy.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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
192
composer.lock
generated
@ -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",
|
||||
|
||||
28
database/factories/DiagnosisFactory.php
Normal file
28
database/factories/DiagnosisFactory.php
Normal 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']),
|
||||
];
|
||||
}
|
||||
}
|
||||
41
database/factories/EstimateFactory.php
Normal file
41
database/factories/EstimateFactory.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
72
public/images/vehicle-diagram.svg
Normal file
72
public/images/vehicle-diagram.svg
Normal 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 |
211
resources/views/components/layouts/print.blade.php
Normal file
211
resources/views/components/layouts/print.blade.php
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
340
resources/views/livewire/estimates/create-standalone.blade.php
Normal file
340
resources/views/livewire/estimates/create-standalone.blade.php
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
384
resources/views/livewire/estimates/show-advanced.blade.php
Normal file
384
resources/views/livewire/estimates/show-advanced.blade.php
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
328
resources/views/livewire/inspections/print.blade.php
Normal file
328
resources/views/livewire/inspections/print.blade.php
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
14
test-inspection.blade.php
Normal 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>
|
||||
356
tests/Feature/EstimateModuleTest.php
Normal file
356
tests/Feature/EstimateModuleTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
107
tests/Feature/EstimatesTest.php
Normal file
107
tests/Feature/EstimatesTest.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
18
tests/Feature/JobCardsIndexTest.php
Normal file
18
tests/Feature/JobCardsIndexTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user