Changes 0228092025 Laptop
This commit is contained in:
parent
765beb78b7
commit
b40ee9dcde
375
.github/copilot-instructions.md
vendored
Normal file
375
.github/copilot-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
<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.6
|
||||
- inertiajs/inertia-laravel (INERTIA) - v2
|
||||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/sanctum (SANCTUM) - v4
|
||||
- laravel/scout (SCOUT) - v10
|
||||
- tightenco/ziggy (ZIGGY) - v2
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pint (PINT) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- pestphp/pest (PEST) - v3
|
||||
- phpunit/phpunit (PHPUNIT) - v11
|
||||
- @inertiajs/vue3 (INERTIA) - v2
|
||||
- tailwindcss (TAILWINDCSS) - v3
|
||||
- vue (VUE) - v3
|
||||
|
||||
|
||||
## 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. For example, 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
|
||||
|
||||
|
||||
=== php rules ===
|
||||
|
||||
## PHP
|
||||
|
||||
- Always use curly braces for control structures, even if it has one line.
|
||||
|
||||
### Constructors
|
||||
- Use PHP 8 constructor property promotion in `__construct()`.
|
||||
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
|
||||
- Do not allow empty `__construct()` methods with zero parameters.
|
||||
|
||||
### Type Declarations
|
||||
- Always use explicit return type declarations for methods and functions.
|
||||
- Use appropriate PHP type hints for method parameters.
|
||||
|
||||
<code-snippet name="Explicit Return Types and Method Params" lang="php">
|
||||
protected function isAccessible(User $user, ?string $path = null): bool
|
||||
{
|
||||
...
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
## Comments
|
||||
- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on.
|
||||
|
||||
## PHPDoc Blocks
|
||||
- Add useful array shape type definitions for arrays when appropriate.
|
||||
|
||||
## Enums
|
||||
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
|
||||
|
||||
=== inertia-laravel/core rules ===
|
||||
|
||||
## Inertia Core
|
||||
|
||||
- Inertia.js components should be placed in the `resources/js/Pages` directory unless specified differently in the JS bundler (vite.config.js).
|
||||
- Use `Inertia::render()` for server-side routing instead of traditional Blade views.
|
||||
- Use `search-docs` for accurate guidance on all things Inertia.
|
||||
|
||||
<code-snippet lang="php" name="Inertia::render Example">
|
||||
// routes/web.php example
|
||||
Route::get('/users', function () {
|
||||
return Inertia::render('Users/Index', [
|
||||
'users' => User::all()
|
||||
]);
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
|
||||
=== inertia-laravel/v2 rules ===
|
||||
|
||||
## Inertia v2
|
||||
|
||||
- Make use of all Inertia features from v1 & v2. Check the documentation before making any changes to ensure we are taking the correct approach.
|
||||
|
||||
### Inertia v2 New Features
|
||||
- Polling
|
||||
- Prefetching
|
||||
- Deferred props
|
||||
- Infinite scrolling using merging props and `WhenVisible`
|
||||
- Lazy loading data on scroll
|
||||
|
||||
### Deferred Props & Empty States
|
||||
- When using deferred props on the frontend, you should add a nice empty state with pulsing / animated skeleton.
|
||||
|
||||
### Inertia Form General Guidance
|
||||
- Build forms using the `useForm` helper. Use the code examples and `search-docs` tool with a query of `useForm helper` for guidance.
|
||||
|
||||
|
||||
=== 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.
|
||||
|
||||
|
||||
=== 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.
|
||||
|
||||
|
||||
=== pest/core rules ===
|
||||
|
||||
## Pest
|
||||
|
||||
### Testing
|
||||
- If you need to verify a feature is working, write or update a Unit / Feature test.
|
||||
|
||||
### Pest Tests
|
||||
- All tests must be written using Pest. Use `php artisan make:test --pest <name>`.
|
||||
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application.
|
||||
- Tests should test all of the happy paths, failure paths, and weird paths.
|
||||
- Tests live in the `tests/Feature` and `tests/Unit` directories.
|
||||
- Pest tests look and behave like this:
|
||||
<code-snippet name="Basic Pest Test Example" lang="php">
|
||||
it('is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Running Tests
|
||||
- Run the minimal number of tests using an appropriate filter before finalizing code edits.
|
||||
- To run all tests: `php artisan test`.
|
||||
- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`.
|
||||
- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file).
|
||||
- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing.
|
||||
|
||||
### Pest Assertions
|
||||
- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.:
|
||||
<code-snippet name="Pest Example Asserting postJson Response" lang="php">
|
||||
it('returns all', function () {
|
||||
$response = $this->postJson('/api/docs', []);
|
||||
|
||||
$response->assertSuccessful();
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Mocking
|
||||
- Mocking can be very helpful when appropriate.
|
||||
- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do.
|
||||
- You can also create partial mocks using the same import or self method.
|
||||
|
||||
### Datasets
|
||||
- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules.
|
||||
|
||||
<code-snippet name="Pest Dataset Example" lang="php">
|
||||
it('has emails', function (string $email) {
|
||||
expect($email)->not->toBeEmpty();
|
||||
})->with([
|
||||
'james' => 'james@laravel.com',
|
||||
'taylor' => 'taylor@laravel.com',
|
||||
]);
|
||||
</code-snippet>
|
||||
|
||||
|
||||
=== inertia-vue/core rules ===
|
||||
|
||||
## Inertia + Vue
|
||||
|
||||
- Vue components must have a single root element.
|
||||
- Use `router.visit()` or `<Link>` for navigation instead of traditional links.
|
||||
|
||||
<code-snippet name="Inertia Client Navigation" lang="vue">
|
||||
|
||||
import { Link } from '@inertiajs/vue3'
|
||||
<Link href="/">Home</Link>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
|
||||
=== inertia-vue/v2/forms rules ===
|
||||
|
||||
## Inertia + Vue Forms
|
||||
|
||||
<code-snippet name="Inertia Vue useForm example" lang="vue">
|
||||
|
||||
<script setup>
|
||||
import { useForm } from '@inertiajs/vue3'
|
||||
|
||||
const form = useForm({
|
||||
email: null,
|
||||
password: null,
|
||||
remember: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="form.post('/login')">
|
||||
<!-- email -->
|
||||
<input type="text" v-model="form.email">
|
||||
<div v-if="form.errors.email">{{ form.errors.email }}</div>
|
||||
<!-- password -->
|
||||
<input type="password" v-model="form.password">
|
||||
<div v-if="form.errors.password">{{ form.errors.password }}</div>
|
||||
<!-- remember me -->
|
||||
<input type="checkbox" v-model="form.remember"> Remember Me
|
||||
<!-- submit -->
|
||||
<button type="submit" :disabled="form.processing">Login</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
</code-snippet>
|
||||
|
||||
|
||||
=== 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/v3 rules ===
|
||||
|
||||
## Tailwind 3
|
||||
|
||||
- Always use Tailwind CSS v3 - verify you're using only classes supported by this version.
|
||||
|
||||
|
||||
=== 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>
|
||||
|
|
@ -10,6 +10,8 @@
|
|||
use Exception;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Requests\StoreContractRequest;
|
||||
use App\Http\Requests\UpdateContractRequest;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ClientCaseContoller extends Controller
|
||||
|
|
@ -93,7 +95,7 @@ public function store(Request $request)
|
|||
return to_route('client.show', $client);
|
||||
}
|
||||
|
||||
public function storeContract(ClientCase $clientCase, Request $request)
|
||||
public function storeContract(ClientCase $clientCase, StoreContractRequest $request)
|
||||
{
|
||||
|
||||
\DB::transaction(function() use ($request, $clientCase){
|
||||
|
|
@ -106,6 +108,8 @@ public function storeContract(ClientCase $clientCase, Request $request)
|
|||
'description' => $request->input('description'),
|
||||
]);
|
||||
|
||||
// Note: Contract config auto-application is handled in Contract model created hook.
|
||||
|
||||
// Optionally create/update related account amounts
|
||||
$initial = $request->input('initial_amount');
|
||||
$balance = $request->input('balance_amount');
|
||||
|
|
@ -121,7 +125,7 @@ public function storeContract(ClientCase $clientCase, Request $request)
|
|||
return to_route('clientCase.show', $clientCase);
|
||||
}
|
||||
|
||||
public function updateContract(ClientCase $clientCase, String $uuid, Request $request)
|
||||
public function updateContract(ClientCase $clientCase, String $uuid, UpdateContractRequest $request)
|
||||
{
|
||||
$contract = Contract::where('uuid', $uuid)->firstOrFail();
|
||||
|
||||
|
|
@ -209,6 +213,86 @@ public function deleteContract(ClientCase $clientCase, String $uuid, Request $re
|
|||
return to_route('clientCase.show', $clientCase);
|
||||
}
|
||||
|
||||
public function updateContractSegment(ClientCase $clientCase, string $uuid, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'segment_id' => ['required', 'integer', 'exists:segments,id'],
|
||||
]);
|
||||
|
||||
$contract = $clientCase->contracts()->where('uuid', $uuid)->firstOrFail();
|
||||
|
||||
\DB::transaction(function () use ($contract, $validated) {
|
||||
// Deactivate current active relation(s)
|
||||
\DB::table('contract_segment')
|
||||
->where('contract_id', $contract->id)
|
||||
->where('active', true)
|
||||
->update(['active' => false]);
|
||||
|
||||
// Attach or update the selected segment as active
|
||||
$existing = \DB::table('contract_segment')
|
||||
->where('contract_id', $contract->id)
|
||||
->where('segment_id', $validated['segment_id'])
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
\DB::table('contract_segment')
|
||||
->where('id', $existing->id)
|
||||
->update(['active' => true, 'updated_at' => now()]);
|
||||
} else {
|
||||
$contract->segments()->attach($validated['segment_id'], ['active' => true, 'created_at' => now(), 'updated_at' => now()]);
|
||||
}
|
||||
});
|
||||
|
||||
return back()->with('success', 'Contract segment updated.');
|
||||
}
|
||||
|
||||
public function attachSegment(ClientCase $clientCase, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'segment_id' => ['required', 'integer', 'exists:segments,id'],
|
||||
'contract_uuid' => ['nullable', 'uuid'],
|
||||
'make_active_for_contract' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
\DB::transaction(function () use ($clientCase, $validated) {
|
||||
// Attach segment to client case if not already attached
|
||||
$attached = \DB::table('client_case_segment')
|
||||
->where('client_case_id', $clientCase->id)
|
||||
->where('segment_id', $validated['segment_id'])
|
||||
->first();
|
||||
if (!$attached) {
|
||||
$clientCase->segments()->attach($validated['segment_id'], ['active' => true]);
|
||||
} else if (!$attached->active) {
|
||||
\DB::table('client_case_segment')
|
||||
->where('id', $attached->id)
|
||||
->update(['active' => true, 'updated_at' => now()]);
|
||||
}
|
||||
|
||||
// Optionally make it active for a specific contract
|
||||
if (!empty($validated['contract_uuid']) && ($validated['make_active_for_contract'] ?? false)) {
|
||||
$contract = $clientCase->contracts()->where('uuid', $validated['contract_uuid'])->firstOrFail();
|
||||
\DB::table('contract_segment')
|
||||
->where('contract_id', $contract->id)
|
||||
->where('active', true)
|
||||
->update(['active' => false]);
|
||||
|
||||
$existing = \DB::table('contract_segment')
|
||||
->where('contract_id', $contract->id)
|
||||
->where('segment_id', $validated['segment_id'])
|
||||
->first();
|
||||
if ($existing) {
|
||||
\DB::table('contract_segment')
|
||||
->where('id', $existing->id)
|
||||
->update(['active' => true, 'updated_at' => now()]);
|
||||
} else {
|
||||
$contract->segments()->attach($validated['segment_id'], ['active' => true, 'created_at' => now(), 'updated_at' => now()]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return back()->with('success', 'Segment attached to case.');
|
||||
}
|
||||
|
||||
public function storeDocument(ClientCase $clientCase, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
|
|
@ -336,11 +420,11 @@ public function show(ClientCase $clientCase)
|
|||
'phone_types' => \App\Models\Person\PhoneType::all()
|
||||
];
|
||||
|
||||
return Inertia::render('Cases/Show', [
|
||||
return Inertia::render('Cases/Show', [
|
||||
'client' => $case->client()->with('person', fn($q) => $q->with(['addresses', 'phones']))->firstOrFail(),
|
||||
'client_case' => $case,
|
||||
'contracts' => $case->contracts()
|
||||
->with(['type', 'account', 'objects'])
|
||||
->with(['type', 'account', 'objects', 'segments:id,name'])
|
||||
->orderByDesc('created_at')->get(),
|
||||
'activities' => $case->activities()->with(['action', 'decision', 'contract:id,uuid,reference'])
|
||||
->orderByDesc('created_at')
|
||||
|
|
@ -348,7 +432,9 @@ public function show(ClientCase $clientCase)
|
|||
'documents' => $case->documents()->orderByDesc('created_at')->get(),
|
||||
'contract_types' => \App\Models\ContractType::whereNull('deleted_at')->get(),
|
||||
'actions' => \App\Models\Action::with('decisions')->get(),
|
||||
'types' => $types
|
||||
'types' => $types,
|
||||
'segments' => $case->segments()->wherePivot('active', true)->get(['segments.id','segments.name']),
|
||||
'all_segments' => \App\Models\Segment::query()->where('active', true)->get(['id','name'])
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
72
app/Http/Controllers/ContractConfigController.php
Normal file
72
app/Http/Controllers/ContractConfigController.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ContractConfig;
|
||||
use App\Models\ContractType;
|
||||
use App\Models\Segment;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ContractConfigController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return Inertia::render('Settings/ContractConfigs/Index', [
|
||||
'configs' => ContractConfig::with(['type:id,name', 'segment:id,name'])->get(),
|
||||
'types' => ContractType::query()->get(['id','name']),
|
||||
'segments' => Segment::query()->where('active', true)->get(['id','name']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'contract_type_id' => ['required', 'integer', 'exists:contract_types,id'],
|
||||
'segment_id' => ['required', 'integer', 'exists:segments,id'],
|
||||
'is_initial' => ['sometimes', 'boolean'],
|
||||
'active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
// Prevent duplicates for same type/segment
|
||||
$exists = ContractConfig::query()
|
||||
->where('contract_type_id', $data['contract_type_id'])
|
||||
->where('segment_id', $data['segment_id'])
|
||||
->exists();
|
||||
if ($exists) {
|
||||
return back()->withErrors(['segment_id' => 'This segment is already configured for the selected type.']);
|
||||
}
|
||||
|
||||
ContractConfig::create([
|
||||
'contract_type_id' => $data['contract_type_id'],
|
||||
'segment_id' => $data['segment_id'],
|
||||
'is_initial' => (bool)($data['is_initial'] ?? false),
|
||||
'active' => (bool)($data['active'] ?? true),
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Configuration created');
|
||||
}
|
||||
|
||||
public function update(ContractConfig $config, Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'segment_id' => ['required', 'integer', 'exists:segments,id'],
|
||||
'is_initial' => ['sometimes', 'boolean'],
|
||||
'active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$config->update([
|
||||
'segment_id' => $data['segment_id'],
|
||||
'is_initial' => (bool)($data['is_initial'] ?? $config->is_initial),
|
||||
'active' => (bool)($data['active'] ?? $config->active),
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Configuration updated');
|
||||
}
|
||||
|
||||
public function destroy(ContractConfig $config)
|
||||
{
|
||||
$config->delete();
|
||||
return back()->with('success', 'Configuration deleted');
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,9 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\FieldJobSetting;
|
||||
use App\Models\Segment;
|
||||
use App\Models\Decision;
|
||||
use App\Http\Requests\StoreFieldJobSettingRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
|
|
@ -11,11 +14,27 @@ class FieldJobSettingController extends Controller
|
|||
public function index(Request $request)
|
||||
{
|
||||
$settings = FieldJobSetting::query()
|
||||
->with(['segment', 'asignDecision', 'completeDecision'])
|
||||
->with(['segment', 'initialDecision', 'asignDecision', 'completeDecision'])
|
||||
->get();
|
||||
|
||||
return Inertia::render('Settings/FieldJob/Index', [
|
||||
'settings' => $settings,
|
||||
'segments' => Segment::query()->get(),
|
||||
'decisions' => Decision::query()->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreFieldJobSettingRequest $request)
|
||||
{
|
||||
$attributes = $request->validated();
|
||||
|
||||
FieldJobSetting::create([
|
||||
'segment_id' => $attributes['segment_id'],
|
||||
'initial_decision_id' => $attributes['initial_decision_id'],
|
||||
'asign_decision_id' => $attributes['asign_decision_id'],
|
||||
'complete_decision_id' => $attributes['complete_decision_id'],
|
||||
]);
|
||||
|
||||
return to_route('settings.fieldjob.index')->with('success', 'Field job setting created successfully!');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Segment;
|
||||
use App\Http\Requests\StoreSegmentRequest;
|
||||
use App\Http\Requests\UpdateSegmentRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
|
|
@ -14,5 +16,29 @@ public function settings(Request $request)
|
|||
'segments' => Segment::query()->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreSegmentRequest $request)
|
||||
{
|
||||
$data = $request->validated();
|
||||
Segment::create([
|
||||
'name' => $data['name'],
|
||||
'description' => $data['description'] ?? null,
|
||||
'active' => $data['active'] ?? true,
|
||||
]);
|
||||
|
||||
return to_route('settings.segments')->with('success', 'Segment created');
|
||||
}
|
||||
|
||||
public function update(UpdateSegmentRequest $request, Segment $segment)
|
||||
{
|
||||
$data = $request->validated();
|
||||
$segment->update([
|
||||
'name' => $data['name'],
|
||||
'description' => $data['description'] ?? null,
|
||||
'active' => $data['active'] ?? $segment->active,
|
||||
]);
|
||||
|
||||
return to_route('settings.segments')->with('success', 'Segment updated');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ class WorkflowController extends Controller
|
|||
public function index(Request $request)
|
||||
{
|
||||
return Inertia::render('Settings/Workflow/Index', [
|
||||
'actions' => Action::query()->with(['decisions', 'segment'])->get(),
|
||||
'decisions' => Decision::query()->with('actions')->get(),
|
||||
'actions' => Action::query()->with(['decisions', 'segment'])->withCount('activities')->get(),
|
||||
'decisions' => Decision::query()->with('actions')->withCount('activities')->get(),
|
||||
'segments' => Segment::query()->get(),
|
||||
]);
|
||||
}
|
||||
|
|
@ -127,4 +127,32 @@ public function updateDecision(int $id, Request $request)
|
|||
|
||||
return to_route('settings.workflow')->with('success', 'Decision updated successfully!');
|
||||
}
|
||||
|
||||
public function destroyAction(int $id)
|
||||
{
|
||||
$row = Action::findOrFail($id);
|
||||
if ($row->activities()->exists()) {
|
||||
return back()->with('error', 'Cannot delete action because dependent activities exist.');
|
||||
}
|
||||
|
||||
\DB::transaction(function () use ($row) {
|
||||
$row->decisions()->detach();
|
||||
$row->delete();
|
||||
});
|
||||
return back()->with('success', 'Action deleted successfully!');
|
||||
}
|
||||
|
||||
public function destroyDecision(int $id)
|
||||
{
|
||||
$row = Decision::findOrFail($id);
|
||||
if ($row->activities()->exists()) {
|
||||
return back()->with('error', 'Cannot delete decision because dependent activities exist.');
|
||||
}
|
||||
|
||||
\DB::transaction(function () use ($row) {
|
||||
$row->actions()->detach();
|
||||
$row->delete();
|
||||
});
|
||||
return back()->with('success', 'Decision deleted successfully!');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
app/Http/Requests/StoreContractRequest.php
Normal file
37
app/Http/Requests/StoreContractRequest.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreContractRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$clientCase = $this->route('client_case');
|
||||
|
||||
return [
|
||||
'reference' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:125',
|
||||
Rule::unique('contracts', 'reference')
|
||||
->where(fn ($q) => $q
|
||||
->where('client_case_id', optional($clientCase)->id)
|
||||
->whereNull('deleted_at')
|
||||
),
|
||||
],
|
||||
'start_date' => ['required', 'date'],
|
||||
'type_id' => ['required', 'integer', 'exists:contract_types,id'],
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
'initial_amount' => ['nullable', 'numeric'],
|
||||
'balance_amount' => ['nullable', 'numeric'],
|
||||
];
|
||||
}
|
||||
}
|
||||
33
app/Http/Requests/StoreFieldJobSettingRequest.php
Normal file
33
app/Http/Requests/StoreFieldJobSettingRequest.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreFieldJobSettingRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'segment_id' => ['required', 'integer', 'exists:segments,id'],
|
||||
'initial_decision_id' => ['required', 'integer', 'exists:decisions,id'],
|
||||
'asign_decision_id' => ['required', 'integer', 'exists:decisions,id'],
|
||||
'complete_decision_id' => ['required', 'integer', 'exists:decisions,id'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'segment_id.required' => 'Segment is required.',
|
||||
'initial_decision_id.required' => 'Initial decision is required.',
|
||||
'asign_decision_id.required' => 'Assign decision is required.',
|
||||
'complete_decision_id.required' => 'Complete decision is required.',
|
||||
];
|
||||
}
|
||||
}
|
||||
29
app/Http/Requests/StoreSegmentRequest.php
Normal file
29
app/Http/Requests/StoreSegmentRequest.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreSegmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:50'],
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
'active' => ['boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'Name is required.',
|
||||
];
|
||||
}
|
||||
}
|
||||
39
app/Http/Requests/UpdateContractRequest.php
Normal file
39
app/Http/Requests/UpdateContractRequest.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateContractRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$clientCase = $this->route('client_case');
|
||||
$uuid = $this->route('uuid');
|
||||
|
||||
return [
|
||||
'reference' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:125',
|
||||
Rule::unique('contracts', 'reference')
|
||||
->where(fn ($q) => $q
|
||||
->where('client_case_id', optional($clientCase)->id)
|
||||
->whereNull('deleted_at')
|
||||
->where('uuid', '!=', $uuid)
|
||||
),
|
||||
],
|
||||
'start_date' => ['sometimes', 'date'],
|
||||
'type_id' => ['required', 'integer', 'exists:contract_types,id'],
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
'initial_amount' => ['nullable', 'numeric'],
|
||||
'balance_amount' => ['nullable', 'numeric'],
|
||||
];
|
||||
}
|
||||
}
|
||||
29
app/Http/Requests/UpdateSegmentRequest.php
Normal file
29
app/Http/Requests/UpdateSegmentRequest.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateSegmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:50'],
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
'active' => ['boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'Name is required.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Action extends Model
|
||||
|
|
@ -26,4 +27,9 @@ public function segment(): BelongsTo
|
|||
return $this->belongsTo(\App\Models\Segment::class);
|
||||
}
|
||||
|
||||
public function activities(): HasMany
|
||||
{
|
||||
return $this->hasMany(\App\Models\Activity::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,4 +65,72 @@ public function objects(): HasMany
|
|||
return $this->hasMany(\App\Models\CaseObject::class, 'contract_id');
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::created(function (Contract $contract): void {
|
||||
// Only apply configs when type and client case are set and no segments are already attached
|
||||
if (empty($contract->type_id) || empty($contract->client_case_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = \DB::table('contract_segment')
|
||||
->where('contract_id', $contract->id)
|
||||
->count();
|
||||
if ($existing > 0) {
|
||||
// Respect pre-attached segments (e.g. custom import logic)
|
||||
return;
|
||||
}
|
||||
|
||||
$configs = ContractConfig::query()
|
||||
->where('contract_type_id', $contract->type_id)
|
||||
->where('active', true)
|
||||
->get(['segment_id', 'is_initial']);
|
||||
|
||||
if ($configs->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($configs as $cfg) {
|
||||
// Ensure the segment is attached to the client case and active
|
||||
$attached = \DB::table('client_case_segment')
|
||||
->where('client_case_id', $contract->client_case_id)
|
||||
->where('segment_id', $cfg->segment_id)
|
||||
->first();
|
||||
if (!$attached) {
|
||||
\DB::table('client_case_segment')->insert([
|
||||
'client_case_id' => $contract->client_case_id,
|
||||
'segment_id' => $cfg->segment_id,
|
||||
'active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
} elseif (!$attached->active) {
|
||||
\DB::table('client_case_segment')
|
||||
->where('id', $attached->id)
|
||||
->update(['active' => true, 'updated_at' => now()]);
|
||||
}
|
||||
|
||||
// Attach to contract; mark active if initial, otherwise inactive
|
||||
$pivot = \DB::table('contract_segment')
|
||||
->where('contract_id', $contract->id)
|
||||
->where('segment_id', $cfg->segment_id)
|
||||
->first();
|
||||
|
||||
$activeFlag = $cfg->is_initial ? true : false;
|
||||
if ($pivot) {
|
||||
\DB::table('contract_segment')
|
||||
->where('id', $pivot->id)
|
||||
->update(['active' => $activeFlag, 'updated_at' => now()]);
|
||||
} else {
|
||||
\DB::table('contract_segment')->insert([
|
||||
'contract_id' => $contract->id,
|
||||
'segment_id' => $cfg->segment_id,
|
||||
'active' => $activeFlag,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
app/Models/ContractConfig.php
Normal file
37
app/Models/ContractConfig.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ContractConfig extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'contract_type_id',
|
||||
'segment_id',
|
||||
'is_initial',
|
||||
'active',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'active' => 'boolean',
|
||||
'is_initial' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function type(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ContractType::class, 'contract_type_id');
|
||||
}
|
||||
|
||||
public function segment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Segment::class, 'segment_id');
|
||||
}
|
||||
}
|
||||
|
|
@ -24,4 +24,9 @@ public function events(): BelongsToMany
|
|||
{
|
||||
return $this->belongsToMany(\App\Models\Event::class);
|
||||
}
|
||||
|
||||
public function activities(): HasMany
|
||||
{
|
||||
return $this->hasMany(\App\Models\Activity::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,21 +5,32 @@
|
|||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class FieldJob extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'field_job_setting_id',
|
||||
'asigned_user_id',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'user_id',
|
||||
'contract_id',
|
||||
'assigned_at',
|
||||
'completed_at',
|
||||
'cancelled_at',
|
||||
'priority',
|
||||
'notes',
|
||||
'address_snapshot ',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
'assigned_at' => 'date',
|
||||
'completed_at' => 'date',
|
||||
'cancelled_at' => 'date',
|
||||
'priority' => 'boolean',
|
||||
'address_snapshot ' => 'array',
|
||||
];
|
||||
|
||||
protected static function booted(){
|
||||
|
|
@ -39,4 +50,14 @@ public function assignedUser(): BelongsTo
|
|||
{
|
||||
return $this->belongsTo(User::class, 'asigned_user_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function contract(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Contract::class, 'contract_id');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class FieldJobSetting extends Model
|
|||
|
||||
protected $fillable = [
|
||||
'segment_id',
|
||||
'initial_decision_id',
|
||||
'asign_decision_id',
|
||||
'complete_decision_id',
|
||||
];
|
||||
|
|
@ -27,6 +28,11 @@ public function asignDecision(): BelongsTo
|
|||
return $this->belongsTo(Decision::class, 'asign_decision_id');
|
||||
}
|
||||
|
||||
public function initialDecision(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Decision::class, 'initial_decision_id');
|
||||
}
|
||||
|
||||
public function completeDecision(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Decision::class, 'complete_decision_id');
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ class Segment extends Model
|
|||
/** @use HasFactory<\Database\Factories\SegmentFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'active',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function contracts(): BelongsToMany {
|
||||
return $this->belongsToMany(\App\Models\Contract::class);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/boost": "^1.1",
|
||||
"laravel/pint": "^1.13",
|
||||
"laravel/sail": "^1.26",
|
||||
"mockery/mockery": "^1.6",
|
||||
|
|
|
|||
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": "845b21f4ecbbf1baa0aed4846a6355b8",
|
||||
"content-hash": "af8a7f4584f3bab04f410483a25e092f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "arielmejiadev/larapex-charts",
|
||||
|
|
@ -7517,6 +7517,135 @@
|
|||
},
|
||||
"time": "2025-03-19T14:43:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/boost",
|
||||
"version": "v1.1.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/boost.git",
|
||||
"reference": "4bd1692c064b2135eb2f8f7bd8bcb710e5e75f86"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/boost/zipball/4bd1692c064b2135eb2f8f7bd8bcb710e5e75f86",
|
||||
"reference": "4bd1692c064b2135eb2f8f7bd8bcb710e5e75f86",
|
||||
"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.1",
|
||||
"laravel/prompts": "^0.1.9|^0.3",
|
||||
"laravel/roster": "^0.2.5",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"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\\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-09-18T07:33:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/mcp",
|
||||
"version": "v0.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/mcp.git",
|
||||
"reference": "6d6284a491f07c74d34f48dfd999ed52c567c713"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/mcp/zipball/6d6284a491f07c74d34f48dfd999ed52c567c713",
|
||||
"reference": "6d6284a491f07c74d34f48dfd999ed52c567c713",
|
||||
"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-16T09:50:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/pint",
|
||||
"version": "v1.21.2",
|
||||
|
|
@ -7583,6 +7712,67 @@
|
|||
},
|
||||
"time": "2025-03-14T22:31:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/roster",
|
||||
"version": "v0.2.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/roster.git",
|
||||
"reference": "832a6db43743bf08a58691da207f977ec8dc43aa"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/roster/zipball/832a6db43743bf08a58691da207f977ec8dc43aa",
|
||||
"reference": "832a6db43743bf08a58691da207f977ec8dc43aa",
|
||||
"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-09-22T13:28:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/sail",
|
||||
"version": "v1.41.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
// PostgreSQL: drop NOT NULL constraint on description
|
||||
DB::statement('ALTER TABLE segments ALTER COLUMN description DROP NOT NULL');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Ensure no NULLs before setting NOT NULL
|
||||
DB::statement("UPDATE segments SET description = '' WHERE description IS NULL");
|
||||
DB::statement('ALTER TABLE segments ALTER COLUMN description SET NOT NULL');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('field_jobs', function (Blueprint $table) {
|
||||
$table->foreignId('contract_id')
|
||||
->nullable()
|
||||
->after('user_id')
|
||||
->constrained('contracts')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('field_jobs', function (Blueprint $table) {
|
||||
$table->dropForeign(['contract_id']);
|
||||
$table->dropColumn('contract_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('contract_configs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(\App\Models\ContractType::class, 'contract_type_id')->constrained('contract_types')->cascadeOnDelete();
|
||||
$table->foreignIdFor(\App\Models\Segment::class, 'initial_segment_id')->constrained('segments')->cascadeOnDelete();
|
||||
$table->boolean('active')->default(true);
|
||||
$table->timestamps();
|
||||
$table->unique('contract_type_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('contract_configs');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('contract_configs', function (Blueprint $table) {
|
||||
// Rename initial_segment_id to segment_id
|
||||
if (Schema::hasColumn('contract_configs', 'initial_segment_id')) {
|
||||
$table->renameColumn('initial_segment_id', 'segment_id');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('contract_configs', function (Blueprint $table) {
|
||||
// Add is_initial and drop unique(contract_type_id)
|
||||
$table->boolean('is_initial')->default(false)->after('segment_id');
|
||||
});
|
||||
|
||||
// Drop existing unique index on contract_type_id if present and add composite unique
|
||||
try {
|
||||
Schema::table('contract_configs', function (Blueprint $table) {
|
||||
$table->dropUnique(['contract_type_id']);
|
||||
});
|
||||
} catch (Throwable $e) {
|
||||
// ignore if index does not exist
|
||||
}
|
||||
|
||||
Schema::table('contract_configs', function (Blueprint $table) {
|
||||
$table->unique(['contract_type_id', 'segment_id']);
|
||||
});
|
||||
|
||||
// Mark existing rows as initial
|
||||
\DB::table('contract_configs')->update(['is_initial' => true]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Remove composite unique
|
||||
Schema::table('contract_configs', function (Blueprint $table) {
|
||||
$table->dropUnique(['contract_type_id', 'segment_id']);
|
||||
});
|
||||
|
||||
// Try to restore unique on contract_type_id
|
||||
Schema::table('contract_configs', function (Blueprint $table) {
|
||||
$table->unique('contract_type_id');
|
||||
});
|
||||
|
||||
// Drop is_initial
|
||||
Schema::table('contract_configs', function (Blueprint $table) {
|
||||
$table->dropColumn('is_initial');
|
||||
});
|
||||
|
||||
// Rename segment_id back to initial_segment_id
|
||||
Schema::table('contract_configs', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('contract_configs', 'segment_id')) {
|
||||
$table->renameColumn('segment_id', 'initial_segment_id');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Resolve duplicates among non-deleted rows by appending the row id to later duplicates
|
||||
$dupes = DB::table('contracts')
|
||||
->select('client_case_id', 'reference', DB::raw('COUNT(*) as cnt'))
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('reference')
|
||||
->groupBy('client_case_id', 'reference')
|
||||
->havingRaw('COUNT(*) > 1')
|
||||
->get();
|
||||
|
||||
foreach ($dupes as $d) {
|
||||
$rows = DB::table('contracts')
|
||||
->where('client_case_id', $d->client_case_id)
|
||||
->where('reference', $d->reference)
|
||||
->whereNull('deleted_at')
|
||||
->orderBy('id')
|
||||
->get(['id', 'reference']);
|
||||
|
||||
$keepFirst = true;
|
||||
foreach ($rows as $row) {
|
||||
if ($keepFirst) { $keepFirst = false; continue; }
|
||||
$base = mb_substr($row->reference, 0, 120);
|
||||
$newRef = $base . '-' . $row->id;
|
||||
DB::table('contracts')->where('id', $row->id)->update(['reference' => $newRef]);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a partial unique index (Postgres) for non-deleted rows
|
||||
DB::statement('CREATE UNIQUE INDEX IF NOT EXISTS contracts_client_case_reference_unique ON contracts (client_case_id, reference) WHERE deleted_at IS NULL');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement('DROP INDEX IF EXISTS contracts_client_case_reference_unique');
|
||||
}
|
||||
};
|
||||
|
|
@ -2,12 +2,13 @@
|
|||
import ActionMessage from '@/Components/ActionMessage.vue';
|
||||
import DialogModal from '@/Components/DialogModal.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import SectionTitle from '@/Components/SectionTitle.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import DatePickerField from '@/Components/DatePickerField.vue';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { watch } from 'vue';
|
||||
import { watch, nextTick, ref as vRef } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
client_case: Object,
|
||||
|
|
@ -22,7 +23,10 @@ console.log(props.types);
|
|||
const emit = defineEmits(['close']);
|
||||
|
||||
const close = () => {
|
||||
emit('close');
|
||||
// Clear any previous validation warnings when closing
|
||||
formContract.clearErrors()
|
||||
formContract.recentlySuccessful = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// form state for create or edit
|
||||
|
|
@ -58,6 +62,20 @@ watch(() => props.show, (open) => {
|
|||
// reset for create
|
||||
applyContract(null)
|
||||
}
|
||||
if (!open) {
|
||||
// Ensure warnings are cleared when dialog hides
|
||||
formContract.clearErrors()
|
||||
formContract.recentlySuccessful = false
|
||||
}
|
||||
})
|
||||
|
||||
// optional: focus the reference input when a reference validation error appears
|
||||
const contractRefInput = vRef(null)
|
||||
watch(() => formContract.errors.reference, async (err) => {
|
||||
if (err && props.show) {
|
||||
await nextTick()
|
||||
try { contractRefInput.value?.focus?.() } catch (e) {}
|
||||
}
|
||||
})
|
||||
|
||||
const storeOrUpdate = () => {
|
||||
|
|
@ -90,6 +108,9 @@ const storeOrUpdate = () => {
|
|||
<template #title>{{ formContract.uuid ? 'Uredi pogodbo' : 'Dodaj pogodbo' }}</template>
|
||||
<template #content>
|
||||
<form @submit.prevent="storeOrUpdate">
|
||||
<div v-if="formContract.errors.reference" class="mb-4 rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
||||
{{ formContract.errors.reference }}
|
||||
</div>
|
||||
<SectionTitle class="mt-4 border-b mb-4">
|
||||
<template #title>
|
||||
Pogodba
|
||||
|
|
@ -102,9 +123,10 @@ const storeOrUpdate = () => {
|
|||
ref="contractRefInput"
|
||||
v-model="formContract.reference"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
:class="['mt-1 block w-full', formContract.errors.reference ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : '']"
|
||||
autocomplete="contract-reference"
|
||||
/>
|
||||
<InputError :message="formContract.errors.reference" />
|
||||
</div>
|
||||
<DatePickerField
|
||||
id="contractStartDate"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ const props = defineProps({
|
|||
client_case: Object,
|
||||
contract_types: Array,
|
||||
contracts: { type: Array, default: () => [] },
|
||||
segments: { type: Array, default: () => [] },
|
||||
all_segments: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['edit', 'delete', 'add-activity'])
|
||||
|
|
@ -30,7 +32,8 @@ const onDelete = (c) => emit('delete', c)
|
|||
const onAddActivity = (c) => emit('add-activity', c)
|
||||
|
||||
// CaseObject dialog state
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { router } from '@inertiajs/vue3'
|
||||
const showObjectDialog = ref(false)
|
||||
const showObjectsList = ref(false)
|
||||
const selectedContract = ref(null)
|
||||
|
|
@ -38,6 +41,39 @@ const openObjectDialog = (c) => { selectedContract.value = c; showObjectDialog.v
|
|||
const closeObjectDialog = () => { showObjectDialog.value = false; selectedContract.value = null }
|
||||
const openObjectsList = (c) => { selectedContract.value = c; showObjectsList.value = true }
|
||||
const closeObjectsList = () => { showObjectsList.value = false; selectedContract.value = null }
|
||||
|
||||
// Segment helpers
|
||||
const contractActiveSegment = (c) => {
|
||||
const arr = c?.segments || []
|
||||
return arr.find(s => s.pivot?.active) || arr[0] || null
|
||||
}
|
||||
const segmentName = (id) => props.segments.find(s => s.id === id)?.name || ''
|
||||
const confirmChange = ref({ show: false, contract: null, segmentId: null, fromAll: false })
|
||||
const askChangeSegment = (c, segmentId, fromAll = false) => {
|
||||
confirmChange.value = { show: true, contract: c, segmentId, fromAll }
|
||||
}
|
||||
const closeConfirm = () => { confirmChange.value = { show: false, contract: null, segmentId: null } }
|
||||
const doChangeSegment = () => {
|
||||
const { contract, segmentId, fromAll } = confirmChange.value
|
||||
if (!contract || !segmentId) return closeConfirm()
|
||||
if (fromAll) {
|
||||
router.post(route('clientCase.segments.attach', props.client_case), {
|
||||
segment_id: segmentId,
|
||||
contract_uuid: contract.uuid,
|
||||
make_active_for_contract: true,
|
||||
}, {
|
||||
preserveScroll: true,
|
||||
only: ['contracts', 'segments'],
|
||||
onFinish: () => closeConfirm(),
|
||||
})
|
||||
} else {
|
||||
router.post(route('clientCase.contract.updateSegment', { client_case: props.client_case.uuid, uuid: contract.uuid }), { segment_id: segmentId }, {
|
||||
preserveScroll: true,
|
||||
only: ['contracts'],
|
||||
onFinish: () => closeConfirm(),
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -47,6 +83,7 @@ const closeObjectsList = () => { showObjectsList.value = false; selectedContract
|
|||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Ref.</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Datum začetka</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Tip</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Segment</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3 text-right">Predano</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3 text-right">Odprto</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3 text-center">Opis</FwbTableHeadCell>
|
||||
|
|
@ -58,6 +95,43 @@ const closeObjectsList = () => { showObjectsList.value = false; selectedContract
|
|||
<FwbTableCell>{{ c.reference }}</FwbTableCell>
|
||||
<FwbTableCell>{{ formatDate(c.start_date) }}</FwbTableCell>
|
||||
<FwbTableCell>{{ c?.type?.name }}</FwbTableCell>
|
||||
<FwbTableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-700">{{ contractActiveSegment(c)?.name || '-' }}</span>
|
||||
<Dropdown width="64" align="left">
|
||||
<template #trigger>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center h-7 w-7 rounded-full hover:bg-gray-100"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': !segments || segments.length === 0 }"
|
||||
:title="segments && segments.length ? 'Change segment' : 'No segments available for this case'"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPenToSquare" class="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="py-1">
|
||||
<template v-if="segments && segments.length">
|
||||
<button v-for="s in segments" :key="s.id" type="button" class="w-full px-3 py-1.5 text-left text-sm hover:bg-gray-50" @click="askChangeSegment(c, s.id)">
|
||||
<span>{{ s.name }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="all_segments && all_segments.length">
|
||||
<div class="px-3 py-2 text-xs text-gray-500">Ni segmentov v tem primeru. Dodaj in nastavi segment:</div>
|
||||
<button v-for="s in all_segments" :key="s.id" type="button" class="w-full px-3 py-1.5 text-left text-sm hover:bg-gray-50" @click="askChangeSegment(c, s.id, true)">
|
||||
<span>{{ s.name }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="px-3 py-2 text-sm text-gray-500">No segments configured.</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</FwbTableCell>
|
||||
<FwbTableCell class="text-right">{{ Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(c?.account?.initial_amount ?? 0) }}</FwbTableCell>
|
||||
<FwbTableCell class="text-right">{{ Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(c?.account?.balance_amount ?? 0) }}</FwbTableCell>
|
||||
<FwbTableCell class="text-center">
|
||||
|
|
@ -149,6 +223,18 @@ const closeObjectsList = () => { showObjectsList.value = false; selectedContract
|
|||
</FwbTable>
|
||||
<div v-if="!contracts || contracts.length === 0" class="p-6 text-center text-sm text-gray-500">No contracts.</div>
|
||||
</div>
|
||||
<!-- Confirm change segment -->
|
||||
<div v-if="confirmChange.show" class="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
<div class="bg-white rounded-lg shadow-lg p-4 w-full max-w-sm">
|
||||
<div class="text-sm text-gray-800">
|
||||
Ali želite spremeniti segment za pogodbo <span class="font-medium">{{ confirmChange.contract?.reference }}</span>?
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300" @click="closeConfirm">Prekliči</button>
|
||||
<button class="px-4 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700" @click="doChangeSegment">Potrdi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CaseObjectCreateDialog
|
||||
:show="showObjectDialog"
|
||||
@close="closeObjectDialog"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import PersonInfoGrid from "@/Components/PersonInfoGrid.vue";
|
|||
import SectionTitle from "@/Components/SectionTitle.vue";
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import { FwbButton } from "flowbite-vue";
|
||||
import { onBeforeMount, ref } from "vue";
|
||||
import { onBeforeMount, ref, computed } from "vue";
|
||||
import ContractDrawer from "./Partials/ContractDrawer.vue";
|
||||
import ContractTable from "./Partials/ContractTable.vue";
|
||||
import ActivityDrawer from "./Partials/ActivityDrawer.vue";
|
||||
|
|
@ -12,10 +12,11 @@ import DocumentsTable from "@/Components/DocumentsTable.vue";
|
|||
import DocumentUploadDialog from "@/Components/DocumentUploadDialog.vue";
|
||||
import DocumentViewerDialog from "@/Components/DocumentViewerDialog.vue";
|
||||
import { classifyDocument } from "@/Services/documents";
|
||||
import { router } from '@inertiajs/vue3';
|
||||
import { router, useForm } from '@inertiajs/vue3';
|
||||
import { AngleDownIcon, AngleUpIcon } from "@/Utilities/Icons";
|
||||
import Pagination from "@/Components/Pagination.vue";
|
||||
import ConfirmDialog from "@/Components/ConfirmDialog.vue";
|
||||
import DialogModal from '@/Components/DialogModal.vue';
|
||||
|
||||
const props = defineProps({
|
||||
client: Object,
|
||||
|
|
@ -25,7 +26,9 @@ const props = defineProps({
|
|||
contract_types: Array,
|
||||
actions: Array,
|
||||
types: Object,
|
||||
documents: Array
|
||||
documents: Array,
|
||||
segments: { type: Array, default: () => [] },
|
||||
all_segments: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const showUpload = ref(false);
|
||||
|
|
@ -99,6 +102,27 @@ const showClientDetails = () => {
|
|||
const hideClietnDetails = () => {
|
||||
clientDetails.value = true;
|
||||
};
|
||||
|
||||
// Attach segment to case
|
||||
const showAttachSegment = ref(false)
|
||||
const openAttachSegment = () => { showAttachSegment.value = true }
|
||||
const closeAttachSegment = () => { showAttachSegment.value = false }
|
||||
const attachForm = useForm({ segment_id: null })
|
||||
const availableSegments = computed(() => {
|
||||
const current = new Set((props.segments || []).map(s => s.id))
|
||||
return (props.all_segments || []).filter(s => !current.has(s.id))
|
||||
})
|
||||
const submitAttachSegment = () => {
|
||||
if (!attachForm.segment_id) { return }
|
||||
attachForm.post(route('clientCase.segments.attach', props.client_case), {
|
||||
preserveScroll: true,
|
||||
only: ['segments'],
|
||||
onSuccess: () => {
|
||||
closeAttachSegment()
|
||||
attachForm.reset('segment_id')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<AppLayout title="Client case">
|
||||
|
|
@ -169,12 +193,18 @@ const hideClietnDetails = () => {
|
|||
<SectionTitle>
|
||||
<template #title> Pogodbe </template>
|
||||
</SectionTitle>
|
||||
<FwbButton @click="openDrawerCreateContract">Nova</FwbButton>
|
||||
<div class="flex items-center gap-2">
|
||||
<FwbButton @click="openDrawerCreateContract">Nova</FwbButton>
|
||||
<FwbButton color="light" :disabled="availableSegments.length === 0" @click="openAttachSegment">
|
||||
{{ availableSegments.length ? 'Dodaj segment' : 'Ni razpoložljivih segmentov' }}
|
||||
</FwbButton>
|
||||
</div>
|
||||
</div>
|
||||
<ContractTable
|
||||
:client_case="client_case"
|
||||
:contracts="contracts"
|
||||
:contract_types="contract_types"
|
||||
:segments="segments"
|
||||
@edit="openDrawerEditContract"
|
||||
@delete="requestDeleteContract"
|
||||
@add-activity="openDrawerAddActivity"
|
||||
|
|
@ -252,4 +282,21 @@ const hideClietnDetails = () => {
|
|||
@close="closeConfirmDelete"
|
||||
@confirm="doDeleteContract"
|
||||
/>
|
||||
<DialogModal :show="showAttachSegment" @close="closeAttachSegment">
|
||||
<template #title>Dodaj segment k primeru</template>
|
||||
<template #content>
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700">Segment</label>
|
||||
<select v-model="attachForm.segment_id" class="w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option :value="null" disabled>-- izberi segment --</option>
|
||||
<option v-for="s in availableSegments" :key="s.id" :value="s.id">{{ s.name }}</option>
|
||||
</select>
|
||||
<div v-if="attachForm.errors.segment_id" class="text-sm text-red-600">{{ attachForm.errors.segment_id }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<FwbButton color="light" @click="closeAttachSegment">Prekliči</FwbButton>
|
||||
<FwbButton class="ml-2" :disabled="attachForm.processing || !attachForm.segment_id" @click="submitAttachSegment">Dodaj</FwbButton>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</template>
|
||||
|
|
|
|||
172
resources/js/Pages/Settings/ContractConfigs/Index.vue
Normal file
172
resources/js/Pages/Settings/ContractConfigs/Index.vue
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
<script setup>
|
||||
import AppLayout from '@/Layouts/AppLayout.vue'
|
||||
import SectionTitle from '@/Components/SectionTitle.vue'
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue'
|
||||
import DialogModal from '@/Components/DialogModal.vue'
|
||||
import ConfirmationModal from '@/Components/ConfirmationModal.vue'
|
||||
import InputLabel from '@/Components/InputLabel.vue'
|
||||
import InputError from '@/Components/InputError.vue'
|
||||
import { useForm, router } from '@inertiajs/vue3'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
configs: Array,
|
||||
types: Array,
|
||||
segments: Array,
|
||||
})
|
||||
|
||||
// create modal
|
||||
const showCreate = ref(false)
|
||||
const openCreate = () => { showCreate.value = true }
|
||||
const closeCreate = () => { showCreate.value = false; createForm.reset() }
|
||||
const createForm = useForm({ contract_type_id: null, segment_id: null, is_initial: false })
|
||||
const submitCreate = () => {
|
||||
createForm.post(route('settings.contractConfigs.store'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => closeCreate(),
|
||||
})
|
||||
}
|
||||
|
||||
// inline edit
|
||||
const editing = ref(null)
|
||||
const editForm = useForm({ segment_id: null, is_initial: false, active: true })
|
||||
const openEdit = (row) => { editing.value = row; editForm.segment_id = row?.segment_id ?? row?.segment?.id; editForm.is_initial = !!row.is_initial; editForm.active = !!row.active }
|
||||
const closeEdit = () => { editing.value = null }
|
||||
const submitEdit = () => {
|
||||
if (!editing.value) return
|
||||
editForm.put(route('settings.contractConfigs.update', editing.value.id), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => closeEdit(),
|
||||
})
|
||||
}
|
||||
|
||||
// delete confirmation
|
||||
const showDelete = ref(false)
|
||||
const toDelete = ref(null)
|
||||
const confirmDelete = (row) => { toDelete.value = row; showDelete.value = true }
|
||||
const cancelDelete = () => { toDelete.value = null; showDelete.value = false }
|
||||
const destroyConfig = () => {
|
||||
if (!toDelete.value) return
|
||||
router.delete(route('settings.contractConfigs.destroy', toDelete.value.id), {
|
||||
preserveScroll: true,
|
||||
onFinish: () => cancelDelete(),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout title="Settings: Contract Configurations">
|
||||
<template #header />
|
||||
<div class="pt-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4 border-indigo-400">
|
||||
<div class="p-4 flex items-center justify-between">
|
||||
<SectionTitle>
|
||||
<template #title>Contract configurations</template>
|
||||
</SectionTitle>
|
||||
<PrimaryButton @click="openCreate">+ New</PrimaryButton>
|
||||
</div>
|
||||
<div class="px-4 pb-4">
|
||||
<table class="min-w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr class="border-b">
|
||||
<th class="py-2 pr-4">Type</th>
|
||||
<th class="py-2 pr-4">Segment</th>
|
||||
<th class="py-2 pr-4">Active</th>
|
||||
<th class="py-2 pr-4 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="cfg in configs" :key="cfg.id" class="border-b last:border-0">
|
||||
<td class="py-2 pr-4">{{ cfg.type?.name }}</td>
|
||||
<td class="py-2 pr-4">{{ cfg.segment?.name }} <span v-if="cfg.is_initial" class="ml-2 text-xs text-indigo-600">(initial)</span></td>
|
||||
<td class="py-2 pr-4">{{ cfg.active ? 'Yes' : 'No' }}</td>
|
||||
<td class="py-2 pr-4 text-right">
|
||||
<button class="px-2 py-1 text-indigo-600 hover:underline" @click="openEdit(cfg)">Edit</button>
|
||||
<button class="ml-2 px-2 py-1 text-red-600 hover:underline" @click="confirmDelete(cfg)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!configs || configs.length === 0">
|
||||
<td colspan="4" class="py-6 text-center text-gray-500">No configurations.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- create modal -->
|
||||
<DialogModal :show="showCreate" @close="closeCreate">
|
||||
<template #title>New Contract Configuration</template>
|
||||
<template #content>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<InputLabel for="type">Contract Type</InputLabel>
|
||||
<select id="type" v-model="createForm.contract_type_id" class="mt-1 w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option :value="null" disabled>-- select type --</option>
|
||||
<option v-for="t in types" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
<InputError :message="createForm.errors.contract_type_id" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="segment">Segment</InputLabel>
|
||||
<select id="segment" v-model="createForm.segment_id" class="mt-1 w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option :value="null" disabled>-- select segment --</option>
|
||||
<option v-for="s in segments" :key="s.id" :value="s.id">{{ s.name }}</option>
|
||||
</select>
|
||||
<InputError :message="createForm.errors.segment_id" />
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<input id="is_initial" type="checkbox" v-model="createForm.is_initial" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
<label for="is_initial" class="text-sm text-gray-700">Mark as initial</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="px-4 py-2 rounded bg-gray-200" @click="closeCreate">Cancel</button>
|
||||
<PrimaryButton class="ml-2" :disabled="createForm.processing || !createForm.contract_type_id || !createForm.segment_id" @click="submitCreate">Create</PrimaryButton>
|
||||
</template>
|
||||
</DialogModal>
|
||||
|
||||
<!-- simple inline edit dialog -->
|
||||
<DialogModal :show="!!editing" @close="closeEdit">
|
||||
<template #title>Edit Configuration</template>
|
||||
<template #content>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<InputLabel>Segment</InputLabel>
|
||||
<select v-model="editForm.segment_id" class="mt-1 w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option v-for="s in segments" :key="s.id" :value="s.id">{{ s.name }}</option>
|
||||
</select>
|
||||
<InputError :message="editForm.errors.segment_id" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input id="is_initial_edit" type="checkbox" v-model="editForm.is_initial" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
<label for="is_initial_edit" class="text-sm text-gray-700">Initial</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input id="active" type="checkbox" v-model="editForm.active" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
<label for="active" class="text-sm text-gray-700">Active</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="px-4 py-2 rounded bg-gray-200" @click="closeEdit">Cancel</button>
|
||||
<PrimaryButton class="ml-2" :disabled="editForm.processing || !editForm.segment_id" @click="submitEdit">Save</PrimaryButton>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</AppLayout>
|
||||
<ConfirmationModal :show="showDelete" @close="cancelDelete">
|
||||
<template #title>
|
||||
Delete configuration
|
||||
</template>
|
||||
<template #content>
|
||||
Are you sure you want to delete configuration for type "{{ toDelete?.type?.name }}"?
|
||||
</template>
|
||||
<template #footer>
|
||||
<button @click="cancelDelete" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300 me-2">Cancel</button>
|
||||
<PrimaryButton @click="destroyConfig">Delete</PrimaryButton>
|
||||
</template>
|
||||
</ConfirmationModal>
|
||||
</template>
|
||||
|
|
@ -1,10 +1,51 @@
|
|||
<script setup>
|
||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
import { Link, useForm } from '@inertiajs/vue3';
|
||||
import DialogModal from '@/Components/DialogModal.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
|
||||
const props = defineProps({
|
||||
settings: Array,
|
||||
segments: Array,
|
||||
decisions: Array,
|
||||
});
|
||||
|
||||
const showCreate = ref(false);
|
||||
const segmentOptions = ref([]);
|
||||
const decisionOptions = ref([]);
|
||||
|
||||
onMounted(() => {
|
||||
segmentOptions.value = (props.segments || []).map(s => ({ id: s.id, name: s.name }));
|
||||
decisionOptions.value = (props.decisions || []).map(d => ({ id: d.id, name: d.name }));
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
segment_id: null,
|
||||
initial_decision_id: null,
|
||||
asign_decision_id: null,
|
||||
complete_decision_id: null,
|
||||
});
|
||||
|
||||
const openCreate = () => {
|
||||
form.reset();
|
||||
showCreate.value = true;
|
||||
};
|
||||
|
||||
const closeCreate = () => {
|
||||
showCreate.value = false;
|
||||
form.reset();
|
||||
};
|
||||
|
||||
const store = () => {
|
||||
form.post(route('settings.fieldjob.store'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => closeCreate(),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -12,20 +53,93 @@ const props = defineProps({
|
|||
<template #header></template>
|
||||
<div class="pt-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold mb-2">Segments</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Manage segments used across the app.</p>
|
||||
<Link :href="route('settings.segments')" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700">Open Segments</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Field Job Settings</h2>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold">Field Job Settings</h2>
|
||||
<PrimaryButton @click="openCreate">+ New</PrimaryButton>
|
||||
</div>
|
||||
|
||||
<DialogModal :show="showCreate" @close="closeCreate">
|
||||
<template #title>
|
||||
Create Field Job Setting
|
||||
</template>
|
||||
<template #content>
|
||||
<form @submit.prevent="store">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<InputLabel for="segment" value="Segment" />
|
||||
<multiselect
|
||||
id="segment"
|
||||
v-model="form.segment_id"
|
||||
:options="segmentOptions.map(o=>o.id)"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
placeholder="Select segment"
|
||||
:append-to-body="true"
|
||||
:custom-label="(opt) => (segmentOptions.find(o=>o.id===opt)?.name || '')"
|
||||
/>
|
||||
<InputError :message="form.errors.segment_id" class="mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="initialDecision" value="Initial Decision" />
|
||||
<multiselect
|
||||
id="initialDecision"
|
||||
v-model="form.initial_decision_id"
|
||||
:options="decisionOptions.map(o=>o.id)"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
placeholder="Select initial decision"
|
||||
:append-to-body="true"
|
||||
:custom-label="(opt) => (decisionOptions.find(o=>o.id===opt)?.name || '')"
|
||||
/>
|
||||
<InputError :message="form.errors.initial_decision_id" class="mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="assignDecision" value="Assign Decision" />
|
||||
<multiselect
|
||||
id="assignDecision"
|
||||
v-model="form.asign_decision_id"
|
||||
:options="decisionOptions.map(o=>o.id)"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
placeholder="Select assign decision"
|
||||
:append-to-body="true"
|
||||
:custom-label="(opt) => (decisionOptions.find(o=>o.id===opt)?.name || '')"
|
||||
/>
|
||||
<InputError :message="form.errors.asign_decision_id" class="mt-1" />
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<InputLabel for="completeDecision" value="Complete Decision" />
|
||||
<multiselect
|
||||
id="completeDecision"
|
||||
v-model="form.complete_decision_id"
|
||||
:options="decisionOptions.map(o=>o.id)"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
placeholder="Select complete decision"
|
||||
:append-to-body="true"
|
||||
:custom-label="(opt) => (decisionOptions.find(o=>o.id===opt)?.name || '')"
|
||||
/>
|
||||
<InputError :message="form.errors.complete_decision_id" class="mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-6">
|
||||
<button type="button" @click="closeCreate" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300">Cancel</button>
|
||||
<PrimaryButton :disabled="form.processing">Create</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</DialogModal>
|
||||
<table class="min-w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr class="border-b">
|
||||
<th class="py-2 pr-4">ID</th>
|
||||
<th class="py-2 pr-4">Segment</th>
|
||||
<th class="py-2 pr-4">Initial Decision</th>
|
||||
<th class="py-2 pr-4">Assign Decision</th>
|
||||
<th class="py-2 pr-4">Complete Decision</th>
|
||||
</tr>
|
||||
|
|
@ -34,6 +148,7 @@ const props = defineProps({
|
|||
<tr v-for="row in settings" :key="row.id" class="border-b last:border-0">
|
||||
<td class="py-2 pr-4">{{ row.id }}</td>
|
||||
<td class="py-2 pr-4">{{ row.segment?.name }}</td>
|
||||
<td class="py-2 pr-4">{{ row.initial_decision?.name || row.initialDecision?.name }}</td>
|
||||
<td class="py-2 pr-4">{{ row.asign_decision?.name || row.asignDecision?.name }}</td>
|
||||
<td class="py-2 pr-4">{{ row.complete_decision?.name || row.completeDecision?.name }}</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@ import { Link } from '@inertiajs/vue3';
|
|||
<p class="text-sm text-gray-600 mb-4">Configure segment-based field job rules.</p>
|
||||
<Link :href="route('settings.fieldjob.index')" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700">Open Field Job</Link>
|
||||
</div>
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold mb-2">Contract Configs</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Auto-assign initial segments for contracts by type.</p>
|
||||
<Link :href="route('settings.contractConfigs.index')" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700">Open Contract Configs</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<script setup>
|
||||
import { FwbTable, FwbTableBody, FwbTableHead, FwbTableHeadCell, FwbTableCell, FwbTableRow, FwbDropdown } from 'flowbite-vue';
|
||||
import { DottedMenu, EditIcon, TrashBinIcon } from '@/Utilities/Icons';
|
||||
import { FwbTable, FwbTableBody, FwbTableHead, FwbTableHeadCell, FwbTableCell, FwbTableRow } from 'flowbite-vue';
|
||||
import { EditIcon, TrashBinIcon } from '@/Utilities/Icons';
|
||||
import DialogModal from '@/Components/DialogModal.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import ConfirmationModal from '@/Components/ConfirmationModal.vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { router, useForm } from '@inertiajs/vue3';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
|
|
@ -16,10 +17,13 @@ const props = defineProps({
|
|||
segments: Array
|
||||
});
|
||||
|
||||
const menuDropdown = ref();
|
||||
|
||||
const drawerEdit = ref(false);
|
||||
const drawerCreate = ref(false);
|
||||
const showDelete = ref(false);
|
||||
const toDelete = ref(null);
|
||||
|
||||
const search = ref('');
|
||||
const selectedSegment = ref(null);
|
||||
|
||||
const selectOptions = ref([]);
|
||||
const segmentOptions = ref([]);
|
||||
|
|
@ -91,6 +95,15 @@ onMounted(() => {
|
|||
|
||||
});
|
||||
|
||||
const filtered = computed(() => {
|
||||
const term = search.value?.toLowerCase() ?? '';
|
||||
return (props.actions || []).filter(a => {
|
||||
const matchesSearch = !term || a.name?.toLowerCase().includes(term) || a.color_tag?.toLowerCase().includes(term);
|
||||
const matchesSegment = !selectedSegment.value || a.segment?.id === selectedSegment.value;
|
||||
return matchesSearch && matchesSegment;
|
||||
});
|
||||
});
|
||||
|
||||
const update = () => {
|
||||
form.put(route('settings.actions.update', { id: form.id }), {
|
||||
onSuccess: () => {
|
||||
|
|
@ -107,11 +120,44 @@ const store = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const confirmDelete = (action) => {
|
||||
toDelete.value = action;
|
||||
showDelete.value = true;
|
||||
};
|
||||
|
||||
const cancelDelete = () => {
|
||||
toDelete.value = null;
|
||||
showDelete.value = false;
|
||||
};
|
||||
|
||||
const destroyAction = () => {
|
||||
if (!toDelete.value) return;
|
||||
router.delete(route('settings.actions.destroy', { id: toDelete.value.id }), {
|
||||
preserveScroll: true,
|
||||
onFinish: () => cancelDelete(),
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="py-4 px-3">
|
||||
<div class="p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex gap-3 items-center w-full sm:w-auto">
|
||||
<TextInput v-model="search" placeholder="Search actions..." class="w-full sm:w-64" />
|
||||
<div class="w-64">
|
||||
<Multiselect
|
||||
v-model="selectedSegment"
|
||||
:options="segmentOptions.map(o=>o.id)"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
placeholder="Filter by segment"
|
||||
:append-to-body="true"
|
||||
:custom-label="(opt) => (segmentOptions.find(o=>o.id===opt)?.name || '')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PrimaryButton @click="openCreateDrawer">+ Dodaj akcijo</PrimaryButton>
|
||||
</div>
|
||||
|
||||
<fwb-table>
|
||||
<fwb-table-head>
|
||||
<fwb-table-head-cell>#</fwb-table-head-cell>
|
||||
|
|
@ -123,22 +169,25 @@ const store = () => {
|
|||
</fwb-table-head-cell>
|
||||
</fwb-table-head>
|
||||
<fwb-table-body>
|
||||
<fwb-table-row v-for="act in actions" :key="act.id">
|
||||
<fwb-table-row v-for="act in filtered" :key="act.id">
|
||||
<fwb-table-cell>{{ act.id }}</fwb-table-cell>
|
||||
<fwb-table-cell>{{ act.name }}</fwb-table-cell>
|
||||
<fwb-table-cell>{{ act.color_tag }}</fwb-table-cell>
|
||||
<fwb-table-cell>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-block h-4 w-4 rounded" :style="{ backgroundColor: act.color_tag }"></span>
|
||||
<span>{{ act.color_tag }}</span>
|
||||
</div>
|
||||
</fwb-table-cell>
|
||||
<fwb-table-cell>{{ act.decisions.length }}</fwb-table-cell>
|
||||
<fwb-table-cell>
|
||||
<button class="px-2" @click="openEditDrawer(act)"><EditIcon size="md" css="text-gray-500" /></button>
|
||||
<button class="px-2 disabled:opacity-40" :disabled="(act.activities_count ?? 0) > 0" @click="confirmDelete(act)"><TrashBinIcon size="md" css="text-red-500" /></button>
|
||||
</fwb-table-cell>
|
||||
</fwb-table-row>
|
||||
</fwb-table-body>
|
||||
</fwb-table>
|
||||
<DialogModal
|
||||
:show="drawerEdit"
|
||||
@close="closeEditDrawer"
|
||||
>
|
||||
|
||||
<DialogModal :show="drawerEdit" @close="closeEditDrawer">
|
||||
<template #title>
|
||||
<span>Spremeni akcijo</span>
|
||||
</template>
|
||||
|
|
@ -215,10 +264,7 @@ const store = () => {
|
|||
</template>
|
||||
</DialogModal>
|
||||
|
||||
<DialogModal
|
||||
:show="drawerCreate"
|
||||
@close="closeCreateDrawer"
|
||||
>
|
||||
<DialogModal :show="drawerCreate" @close="closeCreateDrawer">
|
||||
<template #title>
|
||||
<span>Dodaj akcijo</span>
|
||||
</template>
|
||||
|
|
@ -290,4 +336,17 @@ const store = () => {
|
|||
</form>
|
||||
</template>
|
||||
</DialogModal>
|
||||
|
||||
<ConfirmationModal :show="showDelete" @close="cancelDelete">
|
||||
<template #title>
|
||||
Delete action
|
||||
</template>
|
||||
<template #content>
|
||||
Are you sure you want to delete action "{{ toDelete?.name }}"? This cannot be undone.
|
||||
</template>
|
||||
<template #footer>
|
||||
<button @click="cancelDelete" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300 me-2">Cancel</button>
|
||||
<PrimaryButton @click="destroyAction">Delete</PrimaryButton>
|
||||
</template>
|
||||
</ConfirmationModal>
|
||||
</template>
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
<script setup>
|
||||
import { FwbTable, FwbTableBody, FwbTableHead, FwbTableHeadCell, FwbTableCell, FwbTableRow } from 'flowbite-vue';
|
||||
import { EditIcon } from '@/Utilities/Icons';
|
||||
import { EditIcon, TrashBinIcon } from '@/Utilities/Icons';
|
||||
import DialogModal from '@/Components/DialogModal.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import ConfirmationModal from '@/Components/ConfirmationModal.vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { router, useForm } from '@inertiajs/vue3';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
|
|
@ -17,6 +18,10 @@ const props = defineProps({
|
|||
|
||||
const drawerEdit = ref(false);
|
||||
const drawerCreate = ref(false);
|
||||
const showDelete = ref(false);
|
||||
const toDelete = ref(null);
|
||||
|
||||
const search = ref('');
|
||||
|
||||
const actionOptions = ref([]);
|
||||
|
||||
|
|
@ -72,6 +77,13 @@ onMounted(() => {
|
|||
});
|
||||
});
|
||||
|
||||
const filtered = computed(() => {
|
||||
const term = search.value?.toLowerCase() ?? '';
|
||||
return (props.decisions || []).filter(d => {
|
||||
return !term || d.name?.toLowerCase().includes(term) || d.color_tag?.toLowerCase().includes(term);
|
||||
});
|
||||
});
|
||||
|
||||
const update = () => {
|
||||
form.put(route('settings.decisions.update', { id: form.id }), {
|
||||
onSuccess: () => {
|
||||
|
|
@ -88,11 +100,31 @@ const store = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const confirmDelete = (decision) => {
|
||||
toDelete.value = decision;
|
||||
showDelete.value = true;
|
||||
};
|
||||
|
||||
const cancelDelete = () => {
|
||||
toDelete.value = null;
|
||||
showDelete.value = false;
|
||||
};
|
||||
|
||||
const destroyDecision = () => {
|
||||
if (!toDelete.value) return;
|
||||
router.delete(route('settings.decisions.destroy', { id: toDelete.value.id }), {
|
||||
preserveScroll: true,
|
||||
onFinish: () => cancelDelete(),
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="py-4 px-3">
|
||||
<div class="p-4 flex items-center justify-between gap-3">
|
||||
<TextInput v-model="search" placeholder="Search decisions..." class="w-full sm:w-72" />
|
||||
<PrimaryButton @click="openCreateDrawer">+ Dodaj odločitev</PrimaryButton>
|
||||
</div>
|
||||
|
||||
<fwb-table>
|
||||
<fwb-table-head>
|
||||
<fwb-table-head-cell>#</fwb-table-head-cell>
|
||||
|
|
@ -104,22 +136,25 @@ const store = () => {
|
|||
</fwb-table-head-cell>
|
||||
</fwb-table-head>
|
||||
<fwb-table-body>
|
||||
<fwb-table-row v-for="d in decisions" :key="d.id">
|
||||
<fwb-table-row v-for="d in filtered" :key="d.id">
|
||||
<fwb-table-cell>{{ d.id }}</fwb-table-cell>
|
||||
<fwb-table-cell>{{ d.name }}</fwb-table-cell>
|
||||
<fwb-table-cell>{{ d.color_tag }}</fwb-table-cell>
|
||||
<fwb-table-cell>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-block h-4 w-4 rounded" :style="{ backgroundColor: d.color_tag }"></span>
|
||||
<span>{{ d.color_tag }}</span>
|
||||
</div>
|
||||
</fwb-table-cell>
|
||||
<fwb-table-cell>{{ d.actions.length }}</fwb-table-cell>
|
||||
<fwb-table-cell>
|
||||
<button class="px-2" @click="openEditDrawer(d)"><EditIcon size="md" css="text-gray-500" /></button>
|
||||
<button class="px-2 disabled:opacity-40" :disabled="(d.activities_count ?? 0) > 0" @click="confirmDelete(d)"><TrashBinIcon size="md" css="text-red-500" /></button>
|
||||
</fwb-table-cell>
|
||||
</fwb-table-row>
|
||||
</fwb-table-body>
|
||||
</fwb-table>
|
||||
|
||||
<DialogModal
|
||||
:show="drawerEdit"
|
||||
@close="closeEditDrawer"
|
||||
>
|
||||
<DialogModal :show="drawerEdit" @close="closeEditDrawer">
|
||||
<template #title>
|
||||
<span>Spremeni odločitev</span>
|
||||
</template>
|
||||
|
|
@ -177,10 +212,7 @@ const store = () => {
|
|||
</template>
|
||||
</DialogModal>
|
||||
|
||||
<DialogModal
|
||||
:show="drawerCreate"
|
||||
@close="closeCreateDrawer"
|
||||
>
|
||||
<DialogModal :show="drawerCreate" @close="closeCreateDrawer">
|
||||
<template #title>
|
||||
<span>Dodaj odločitev</span>
|
||||
</template>
|
||||
|
|
@ -237,4 +269,17 @@ const store = () => {
|
|||
</form>
|
||||
</template>
|
||||
</DialogModal>
|
||||
|
||||
<ConfirmationModal :show="showDelete" @close="cancelDelete">
|
||||
<template #title>
|
||||
Delete decision
|
||||
</template>
|
||||
<template #content>
|
||||
Are you sure you want to delete decision "{{ toDelete?.name }}"? This cannot be undone.
|
||||
</template>
|
||||
<template #footer>
|
||||
<button @click="cancelDelete" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300 me-2">Cancel</button>
|
||||
<PrimaryButton @click="destroyDecision">Delete</PrimaryButton>
|
||||
</template>
|
||||
</ConfirmationModal>
|
||||
</template>
|
||||
|
|
@ -1,9 +1,73 @@
|
|||
<script setup>
|
||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||
import { useForm, router } from '@inertiajs/vue3';
|
||||
import { ref } from 'vue';
|
||||
import DialogModal from '@/Components/DialogModal.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
segments: Array,
|
||||
});
|
||||
|
||||
const showCreate = ref(false);
|
||||
const showEdit = ref(false);
|
||||
const editing = ref(null);
|
||||
|
||||
const createForm = useForm({
|
||||
name: '',
|
||||
description: '',
|
||||
active: true,
|
||||
});
|
||||
|
||||
const editForm = useForm({
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
active: true,
|
||||
});
|
||||
|
||||
const openCreate = () => {
|
||||
createForm.reset();
|
||||
createForm.active = true;
|
||||
showCreate.value = true;
|
||||
};
|
||||
|
||||
const closeCreate = () => {
|
||||
showCreate.value = false;
|
||||
createForm.reset();
|
||||
};
|
||||
|
||||
const store = () => {
|
||||
createForm.post(route('settings.segments.store'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => closeCreate(),
|
||||
});
|
||||
};
|
||||
|
||||
const openEdit = (segment) => {
|
||||
editing.value = segment;
|
||||
editForm.id = segment.id;
|
||||
editForm.name = segment.name;
|
||||
editForm.description = segment.description ?? '';
|
||||
editForm.active = !!segment.active;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
const closeEdit = () => {
|
||||
showEdit.value = false;
|
||||
editing.value = null;
|
||||
editForm.reset();
|
||||
};
|
||||
|
||||
const update = () => {
|
||||
editForm.put(route('settings.segments.update', { segment: editForm.id }), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => closeEdit(),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -12,10 +76,93 @@ const props = defineProps({
|
|||
<div class="pt-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Segments</h2>
|
||||
<ul class="list-disc list-inside">
|
||||
<li v-for="s in segments" :key="s.id">{{ s.name }}</li>
|
||||
</ul>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold">Segments</h2>
|
||||
<PrimaryButton @click="openCreate">+ New</PrimaryButton>
|
||||
</div>
|
||||
|
||||
<DialogModal :show="showCreate" @close="closeCreate">
|
||||
<template #title>New Segment</template>
|
||||
<template #content>
|
||||
<form @submit.prevent="store" class="space-y-4">
|
||||
<div>
|
||||
<InputLabel for="nameCreate" value="Name" />
|
||||
<TextInput id="nameCreate" v-model="createForm.name" type="text" class="mt-1 block w-full" />
|
||||
<InputError :message="createForm.errors.name" class="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="descCreate" value="Description" />
|
||||
<TextInput id="descCreate" v-model="createForm.description" type="text" class="mt-1 block w-full" />
|
||||
<InputError :message="createForm.errors.description" class="mt-1" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input id="activeCreate" type="checkbox" v-model="createForm.active" />
|
||||
<label for="activeCreate">Active</label>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<button type="button" @click="closeCreate" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300">Cancel</button>
|
||||
<PrimaryButton :disabled="createForm.processing">Create</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</DialogModal>
|
||||
|
||||
<DialogModal :show="showEdit" @close="closeEdit">
|
||||
<template #title>Edit Segment</template>
|
||||
<template #content>
|
||||
<form @submit.prevent="update" class="space-y-4">
|
||||
<div>
|
||||
<InputLabel for="nameEdit" value="Name" />
|
||||
<TextInput id="nameEdit" v-model="editForm.name" type="text" class="mt-1 block w-full" />
|
||||
<InputError :message="editForm.errors.name" class="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="descEdit" value="Description" />
|
||||
<TextInput id="descEdit" v-model="editForm.description" type="text" class="mt-1 block w-full" />
|
||||
<InputError :message="editForm.errors.description" class="mt-1" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input id="activeEdit" type="checkbox" v-model="editForm.active" />
|
||||
<label for="activeEdit">Active</label>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<button type="button" @click="closeEdit" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300">Cancel</button>
|
||||
<PrimaryButton :disabled="editForm.processing">Save</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</DialogModal>
|
||||
|
||||
<table class="min-w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr class="border-b">
|
||||
<th class="py-2 pr-4">ID</th>
|
||||
<th class="py-2 pr-4">Name</th>
|
||||
<th class="py-2 pr-4">Description</th>
|
||||
<th class="py-2 pr-4">Active</th>
|
||||
<th class="py-2 pr-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="s in segments" :key="s.id" class="border-b last:border-0">
|
||||
<td class="py-2 pr-4">{{ s.id }}</td>
|
||||
<td class="py-2 pr-4">{{ s.name }}</td>
|
||||
<td class="py-2 pr-4">{{ s.description }}</td>
|
||||
<td class="py-2 pr-4">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span :class="s.active ? 'bg-green-500' : 'bg-gray-400'" class="inline-block w-2 h-2 rounded-full"></span>
|
||||
{{ s.active ? 'Yes' : 'No' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2 pr-4">
|
||||
<button class="text-indigo-600 hover:text-indigo-800" @click="openEdit(s)">Edit</button>
|
||||
<!-- Delete intentionally skipped as requested -->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@
|
|||
// with `$trail`. This is nice for IDE type checking and completion.
|
||||
use Diglactic\Breadcrumbs\Generator as BreadcrumbTrail;
|
||||
|
||||
Breadcrumbs::for('settings.contractConfigs.index', function (BreadcrumbTrail $trail): void {
|
||||
$trail->parent('settings');
|
||||
$trail->push('Contract Configs', route('settings.contractConfigs.index'));
|
||||
});
|
||||
|
||||
// Dashboard
|
||||
Breadcrumbs::for('dashboard', function (BreadcrumbTrail $trail) {
|
||||
$trail->push('Nadzorna plošča', route('dashboard'));
|
||||
|
|
@ -45,4 +50,22 @@
|
|||
Breadcrumbs::for('settings', function (BreadcrumbTrail $trail) {
|
||||
$trail->parent('dashboard');
|
||||
$trail->push('Nastavitve', route('settings'));
|
||||
});
|
||||
|
||||
// Dashboard > Settings > Segments
|
||||
Breadcrumbs::for('settings.segments', function (BreadcrumbTrail $trail) {
|
||||
$trail->parent('settings');
|
||||
$trail->push('Segmenti', route('settings.segments'));
|
||||
});
|
||||
|
||||
// Dashboard > Settings > Workflow
|
||||
Breadcrumbs::for('settings.workflow', function (BreadcrumbTrail $trail) {
|
||||
$trail->parent('settings');
|
||||
$trail->push('Potek dela', route('settings.workflow'));
|
||||
});
|
||||
|
||||
// Dashboard > Settings > Field Job
|
||||
Breadcrumbs::for('settings.fieldjob.index', function (BreadcrumbTrail $trail) {
|
||||
$trail->parent('settings');
|
||||
$trail->push('Terensko delo', route('settings.fieldjob.index'));
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
use App\Http\Controllers\WorkflowController;
|
||||
use App\Http\Controllers\SegmentController;
|
||||
use App\Http\Controllers\FieldJobSettingController;
|
||||
use App\Http\Controllers\ContractConfigController;
|
||||
use App\Http\Controllers\ImportController;
|
||||
use App\Http\Controllers\ImportTemplateController;
|
||||
use App\Http\Controllers\CaseObjectController;
|
||||
|
|
@ -118,6 +119,7 @@
|
|||
//client-case
|
||||
Route::get('client-cases', [ClientCaseContoller::class, 'index'])->name('clientCase');
|
||||
Route::get('client-cases/{client_case:uuid}', [ClientCaseContoller::class, 'show'])->name('clientCase.show');
|
||||
Route::post('client-cases/{client_case:uuid}/contracts/{uuid}/segment', [ClientCaseContoller::class, 'updateContractSegment'])->name('clientCase.contract.updateSegment');
|
||||
Route::post('client-cases', [ClientCaseContoller::class, 'store'])->name('clientCase.store');
|
||||
//client-case / contract
|
||||
Route::post('client-cases/{client_case:uuid}/contract', [ClientCaseContoller::class, 'storeContract'])->name('clientCase.contract.store');
|
||||
|
|
@ -129,6 +131,8 @@
|
|||
Route::delete('client-cases/{client_case:uuid}/objects/{id}', [CaseObjectController::class, 'destroy'])->name('clientCase.object.delete');
|
||||
//client-case / activity
|
||||
Route::post('client-cases/{client_case:uuid}/activity', [ClientCaseContoller::class, 'storeActivity'])->name('clientCase.activity.store');
|
||||
// client-case / segments
|
||||
Route::post('client-cases/{client_case:uuid}/segments', [ClientCaseContoller::class, 'attachSegment'])->name('clientCase.segments.attach');
|
||||
//client-case / documents
|
||||
Route::post('client-cases/{client_case:uuid}/documents', [ClientCaseContoller::class, 'storeDocument'])->name('clientCase.document.store');
|
||||
Route::get('client-cases/{client_case:uuid}/documents/{document:uuid}/view', [ClientCaseContoller::class, 'viewDocument'])->name('clientCase.document.view');
|
||||
|
|
@ -136,12 +140,22 @@
|
|||
//settings
|
||||
Route::get('settings', [SettingController::class, 'index'])->name('settings');
|
||||
Route::get('settings/segments', [SegmentController::class, 'settings'])->name('settings.segments');
|
||||
Route::post('settings/segments', [SegmentController::class, 'store'])->name('settings.segments.store');
|
||||
Route::put('settings/segments/{segment}', [SegmentController::class, 'update'])->name('settings.segments.update');
|
||||
Route::get('settings/workflow', [WorkflowController::class, 'index'])->name('settings.workflow');
|
||||
Route::get('settings/field-job', [FieldJobSettingController::class, 'index'])->name('settings.fieldjob.index');
|
||||
Route::post('settings/field-job', [FieldJobSettingController::class, 'store'])->name('settings.fieldjob.store');
|
||||
// settings / contract-configs
|
||||
Route::get('settings/contract-configs', [ContractConfigController::class, 'index'])->name('settings.contractConfigs.index');
|
||||
Route::post('settings/contract-configs', [ContractConfigController::class, 'store'])->name('settings.contractConfigs.store');
|
||||
Route::put('settings/contract-configs/{config}', [ContractConfigController::class, 'update'])->name('settings.contractConfigs.update');
|
||||
Route::delete('settings/contract-configs/{config}', [ContractConfigController::class, 'destroy'])->name('settings.contractConfigs.destroy');
|
||||
Route::post('settings/actions', [WorkflowController::class, 'storeAction'])->name('settings.actions.store');
|
||||
Route::put('settings/actions/{id}', [WorkflowController::class, 'updateAction'])->name('settings.actions.update');
|
||||
Route::post('settings/decisions', [WorkflowController::class, 'storeDecision'])->name('settings.decisions.store');
|
||||
Route::put('settings/decisions/{id}', [WorkflowController::class, 'updateDecision'])->name('settings.decisions.update');
|
||||
Route::delete('settings/actions/{id}', [WorkflowController::class, 'destroyAction'])->name('settings.actions.destroy');
|
||||
Route::delete('settings/decisions/{id}', [WorkflowController::class, 'destroyDecision'])->name('settings.decisions.destroy');
|
||||
|
||||
// imports
|
||||
Route::get('imports/create', [ImportController::class, 'create'])->name('imports.create');
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user