diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..30545f3
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,375 @@
+
+=== 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()`.
+ - public function __construct(public GitHub $github) { }
+- 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.
+
+
+protected function isAccessible(User $user, ?string $path = null): bool
+{
+ ...
+}
+
+
+## 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.
+
+
+// routes/web.php example
+Route::get('/users', function () {
+ return Inertia::render('Users/Index', [
+ 'users' => User::all()
+ ]);
+});
+
+
+
+=== 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] ` 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 `.
+- 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:
+
+it('is true', function () {
+ expect(true)->toBeTrue();
+});
+
+
+### 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.:
+
+it('returns all', function () {
+ $response = $this->postJson('/api/docs', []);
+
+ $response->assertSuccessful();
+});
+
+
+### 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.
+
+
+it('has emails', function (string $email) {
+ expect($email)->not->toBeEmpty();
+})->with([
+ 'james' => 'james@laravel.com',
+ 'taylor' => 'taylor@laravel.com',
+]);
+
+
+
+=== inertia-vue/core rules ===
+
+## Inertia + Vue
+
+- Vue components must have a single root element.
+- Use `router.visit()` or `` for navigation instead of traditional links.
+
+
+
+ import { Link } from '@inertiajs/vue3'
+ Home
+
+
+
+
+=== inertia-vue/v2/forms rules ===
+
+## Inertia + Vue Forms
+
+
+
+
+
+
+
+
+
+
+
+
+=== 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.
+
+
+
+
Superior
+
Michigan
+
Erie
+
+
+
+
+### 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.
+
\ No newline at end of file
diff --git a/app/Http/Controllers/ClientCaseContoller.php b/app/Http/Controllers/ClientCaseContoller.php
index b26c4f3..c4c58b2 100644
--- a/app/Http/Controllers/ClientCaseContoller.php
+++ b/app/Http/Controllers/ClientCaseContoller.php
@@ -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'])
]);
}
diff --git a/app/Http/Controllers/ContractConfigController.php b/app/Http/Controllers/ContractConfigController.php
new file mode 100644
index 0000000..8b98176
--- /dev/null
+++ b/app/Http/Controllers/ContractConfigController.php
@@ -0,0 +1,72 @@
+ 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');
+ }
+}
diff --git a/app/Http/Controllers/FieldJobSettingController.php b/app/Http/Controllers/FieldJobSettingController.php
index 41fcdbf..b41bf1b 100644
--- a/app/Http/Controllers/FieldJobSettingController.php
+++ b/app/Http/Controllers/FieldJobSettingController.php
@@ -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!');
+ }
}
diff --git a/app/Http/Controllers/SegmentController.php b/app/Http/Controllers/SegmentController.php
index 96a2f92..dfef658 100644
--- a/app/Http/Controllers/SegmentController.php
+++ b/app/Http/Controllers/SegmentController.php
@@ -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');
+ }
}
diff --git a/app/Http/Controllers/WorkflowController.php b/app/Http/Controllers/WorkflowController.php
index b4178d0..63601a0 100644
--- a/app/Http/Controllers/WorkflowController.php
+++ b/app/Http/Controllers/WorkflowController.php
@@ -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!');
+ }
}
diff --git a/app/Http/Requests/StoreContractRequest.php b/app/Http/Requests/StoreContractRequest.php
new file mode 100644
index 0000000..ea96a26
--- /dev/null
+++ b/app/Http/Requests/StoreContractRequest.php
@@ -0,0 +1,37 @@
+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'],
+ ];
+ }
+}
diff --git a/app/Http/Requests/StoreFieldJobSettingRequest.php b/app/Http/Requests/StoreFieldJobSettingRequest.php
new file mode 100644
index 0000000..798a0ad
--- /dev/null
+++ b/app/Http/Requests/StoreFieldJobSettingRequest.php
@@ -0,0 +1,33 @@
+ ['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.',
+ ];
+ }
+}
diff --git a/app/Http/Requests/StoreSegmentRequest.php b/app/Http/Requests/StoreSegmentRequest.php
new file mode 100644
index 0000000..926bbe8
--- /dev/null
+++ b/app/Http/Requests/StoreSegmentRequest.php
@@ -0,0 +1,29 @@
+ ['required', 'string', 'max:50'],
+ 'description' => ['nullable', 'string', 'max:255'],
+ 'active' => ['boolean'],
+ ];
+ }
+
+ public function messages(): array
+ {
+ return [
+ 'name.required' => 'Name is required.',
+ ];
+ }
+}
diff --git a/app/Http/Requests/UpdateContractRequest.php b/app/Http/Requests/UpdateContractRequest.php
new file mode 100644
index 0000000..f6921a0
--- /dev/null
+++ b/app/Http/Requests/UpdateContractRequest.php
@@ -0,0 +1,39 @@
+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'],
+ ];
+ }
+}
diff --git a/app/Http/Requests/UpdateSegmentRequest.php b/app/Http/Requests/UpdateSegmentRequest.php
new file mode 100644
index 0000000..eb58893
--- /dev/null
+++ b/app/Http/Requests/UpdateSegmentRequest.php
@@ -0,0 +1,29 @@
+ ['required', 'string', 'max:50'],
+ 'description' => ['nullable', 'string', 'max:255'],
+ 'active' => ['boolean'],
+ ];
+ }
+
+ public function messages(): array
+ {
+ return [
+ 'name.required' => 'Name is required.',
+ ];
+ }
+}
diff --git a/app/Models/Action.php b/app/Models/Action.php
index c3ef762..1aec287 100644
--- a/app/Models/Action.php
+++ b/app/Models/Action.php
@@ -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);
+ }
+
}
diff --git a/app/Models/Contract.php b/app/Models/Contract.php
index ec92afb..5c86eab 100644
--- a/app/Models/Contract.php
+++ b/app/Models/Contract.php
@@ -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(),
+ ]);
+ }
+ }
+ });
+ }
}
diff --git a/app/Models/ContractConfig.php b/app/Models/ContractConfig.php
new file mode 100644
index 0000000..7aac0e7
--- /dev/null
+++ b/app/Models/ContractConfig.php
@@ -0,0 +1,37 @@
+ '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');
+ }
+}
diff --git a/app/Models/Decision.php b/app/Models/Decision.php
index 32d3329..b673be5 100644
--- a/app/Models/Decision.php
+++ b/app/Models/Decision.php
@@ -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);
+ }
}
diff --git a/app/Models/FieldJob.php b/app/Models/FieldJob.php
index 2df7446..cc20915 100644
--- a/app/Models/FieldJob.php
+++ b/app/Models/FieldJob.php
@@ -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');
+ }
}
diff --git a/app/Models/FieldJobSetting.php b/app/Models/FieldJobSetting.php
index b06bf47..7a6f260 100644
--- a/app/Models/FieldJobSetting.php
+++ b/app/Models/FieldJobSetting.php
@@ -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');
diff --git a/app/Models/Segment.php b/app/Models/Segment.php
index 4b9aba9..12c6753 100644
--- a/app/Models/Segment.php
+++ b/app/Models/Segment.php
@@ -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);
}
diff --git a/composer.json b/composer.json
index 70473e6..4252aab 100644
--- a/composer.json
+++ b/composer.json
@@ -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",
diff --git a/composer.lock b/composer.lock
index 1fa583e..3ba6e97 100644
--- a/composer.lock
+++ b/composer.lock
@@ -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",
diff --git a/database/migrations/2025_09_28_110600_alter_segments_description_nullable.php b/database/migrations/2025_09_28_110600_alter_segments_description_nullable.php
new file mode 100644
index 0000000..e435283
--- /dev/null
+++ b/database/migrations/2025_09_28_110600_alter_segments_description_nullable.php
@@ -0,0 +1,19 @@
+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');
+ });
+ }
+};
diff --git a/database/migrations/2025_09_28_120500_create_contract_configs_table.php b/database/migrations/2025_09_28_120500_create_contract_configs_table.php
new file mode 100644
index 0000000..4bd9fef
--- /dev/null
+++ b/database/migrations/2025_09_28_120500_create_contract_configs_table.php
@@ -0,0 +1,25 @@
+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');
+ }
+};
diff --git a/database/migrations/2025_09_28_121500_alter_contract_configs_support_multiple.php b/database/migrations/2025_09_28_121500_alter_contract_configs_support_multiple.php
new file mode 100644
index 0000000..ed0c1fe
--- /dev/null
+++ b/database/migrations/2025_09_28_121500_alter_contract_configs_support_multiple.php
@@ -0,0 +1,64 @@
+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');
+ }
+ });
+ }
+};
diff --git a/database/migrations/2025_09_28_151500_add_unique_contract_reference_per_client_case_v2.php b/database/migrations/2025_09_28_151500_add_unique_contract_reference_per_client_case_v2.php
new file mode 100644
index 0000000..0f82b8a
--- /dev/null
+++ b/database/migrations/2025_09_28_151500_add_unique_contract_reference_per_client_case_v2.php
@@ -0,0 +1,44 @@
+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');
+ }
+};
diff --git a/resources/js/Pages/Cases/Partials/ContractDrawer.vue b/resources/js/Pages/Cases/Partials/ContractDrawer.vue
index 563ae7e..8c4be3b 100644
--- a/resources/js/Pages/Cases/Partials/ContractDrawer.vue
+++ b/resources/js/Pages/Cases/Partials/ContractDrawer.vue
@@ -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 = () => {
{{ formContract.uuid ? 'Uredi pogodbo' : 'Dodaj pogodbo' }}