-
Ni telefonov.
-
-
{{ p.nu }} ({{ p.type?.name || phoneTypes[p.type_id] }})
+
+
Ni telefonov.
+
+
+ {{ p.nu }}
+ ({{ p.type?.name || phoneTypes[p.type_id] }})
+
-
-
-
Ni e-poštnih naslovov.
-
-
{{ e.value }}({{ e.label }})
-
-
-
-
-
-
Ni TRR računov.
-
-
{{ maskIban(b.iban) }}
-
{{ b.bank_name }}
+
+
Ni e-poštnih naslovov.
+
+
+ {{ e.value }}({{ e.label }})
+
+
diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue
index 0c2cc64..7cb37a8 100644
--- a/resources/js/Layouts/AppLayout.vue
+++ b/resources/js/Layouts/AppLayout.vue
@@ -9,7 +9,18 @@ import Breadcrumbs from "@/Components/Breadcrumbs.vue";
import GlobalSearch from "./Partials/GlobalSearch.vue";
import NotificationsBell from "./Partials/NotificationsBell.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
-import { faMobileScreenButton } from "@fortawesome/free-solid-svg-icons";
+import {
+ faMobileScreenButton,
+ faGaugeHigh,
+ faLayerGroup,
+ faUserGroup,
+ faFolderOpen,
+ faFileImport,
+ faTableList,
+ faFileCirclePlus,
+ faMap,
+ faGear,
+} from "@fortawesome/free-solid-svg-icons";
const props = defineProps({
title: String,
@@ -173,7 +184,7 @@ const rawMenuGroups = [
],
},
{
- label: "Terensko",
+ label: "Terensko delo",
items: [
{
key: "fieldjobs",
@@ -205,6 +216,19 @@ const menuGroups = computed(() => {
}));
});
+// Icon map for menu keys -> FontAwesome icon definitions
+const menuIconMap = {
+ dashboard: faGaugeHigh,
+ segments: faLayerGroup,
+ clients: faUserGroup,
+ cases: faFolderOpen,
+ imports: faFileImport,
+ "import-templates": faTableList,
+ "import-templates-new": faFileCirclePlus,
+ fieldjobs: faMap,
+ settings: faGear,
+};
+
function isActive(patterns) {
try {
return patterns?.some((p) => route().current(p));
@@ -267,161 +291,12 @@ function isActive(patterns) {
]"
:title="item.title"
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
{{ item.title }}
diff --git a/resources/js/Pages/Cases/Partials/ActivityDrawer.vue b/resources/js/Pages/Cases/Partials/ActivityDrawer.vue
index 1239364..302757f 100644
--- a/resources/js/Pages/Cases/Partials/ActivityDrawer.vue
+++ b/resources/js/Pages/Cases/Partials/ActivityDrawer.vue
@@ -73,11 +73,22 @@ const store = async () => {
amount: form.amount,
note: form.note,
});
+ // Helper to safely format a selected date (Date instance or parsable value) to YYYY-MM-DD
+ const formatDateForSubmit = (value) => {
+ if (!value) return null; // leave empty as null
+ const d = value instanceof Date ? value : new Date(value);
+ if (isNaN(d.getTime())) return null; // invalid date -> null
+ // Avoid timezone shifting by constructing in local time
+ const y = d.getFullYear();
+ const m = String(d.getMonth() + 1).padStart(2, "0");
+ const day = String(d.getDate()).padStart(2, "0");
+ return `${y}-${m}-${day}`; // matches en-CA style YYYY-MM-DD
+ };
form
.transform((data) => ({
...data,
- due_date: new Date(data.due_date).toLocaleDateString("en-CA"),
+ due_date: formatDateForSubmit(data.due_date),
}))
.post(route("clientCase.activity.store", props.client_case), {
onSuccess: () => {
diff --git a/resources/js/Pages/Cases/Partials/ActivityTable.vue b/resources/js/Pages/Cases/Partials/ActivityTable.vue
index 618ee24..15107bd 100644
--- a/resources/js/Pages/Cases/Partials/ActivityTable.vue
+++ b/resources/js/Pages/Cases/Partials/ActivityTable.vue
@@ -179,17 +179,17 @@ const confirmDeleteAction = () => {
>
-
-
+
+
diff --git a/resources/js/Pages/Cases/Partials/ContractTable.vue b/resources/js/Pages/Cases/Partials/ContractTable.vue
index 8b9b221..236ada5 100644
--- a/resources/js/Pages/Cases/Partials/ContractTable.vue
+++ b/resources/js/Pages/Cases/Partials/ContractTable.vue
@@ -21,6 +21,7 @@ import {
faTrash,
faListCheck,
faPlus,
+ faBoxArchive,
} from "@fortawesome/free-solid-svg-icons";
const props = defineProps({
@@ -119,6 +120,10 @@ const confirmChange = ref({
fromAll: false,
});
const askChangeSegment = (c, segmentId, fromAll = false) => {
+ // Prevent segment change for archived contracts
+ if (!c?.active) {
+ return;
+ }
confirmChange.value = { show: true, contract: c, segmentId, fromAll };
};
const closeConfirm = () => {
@@ -262,13 +267,16 @@ const closePaymentsDialog = () => {
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,
+ !segments || segments.length === 0 || !c.active,
}"
:title="
- segments && segments.length
+ !c.active
+ ? 'Segmenta ni mogoče spremeniti za arhivirano pogodbo'
+ : segments && segments.length
? 'Spremeni segment'
: 'Ni segmentov na voljo za ta primer'
"
+ :disabled="!c.active || !segments || !segments.length"
>
{
+ Arhivirano
{{
@@ -433,6 +446,7 @@ const closePaymentsDialog = () => {
{
@@ -468,6 +483,7 @@ const closePaymentsDialog = () => {
@@ -492,12 +508,62 @@ const closePaymentsDialog = () => {
Dodaj plačilo
+
+
+
+ {{ c.active ? "Arhiviranje" : "Ponovna aktivacija" }}
+
+
+
+ Arhiviraj
+
+
+
+ Ponovno aktiviraj
+
{{ activeEntity }}
{{ r.entities[activeEntity].action_label }}
Akcija:
- {{
- r.entities[activeEntity].action_label ||
- r.entities[activeEntity].action
- }}
+ {{
+ r.entities[activeEntity].action_label ||
+ r.entities[activeEntity].action
+ }}
+ react
+
+
+ (iz neaktivnega → aktivno)
diff --git a/resources/js/Pages/Imports/Templates/Create.vue b/resources/js/Pages/Imports/Templates/Create.vue
index 756756f..0177146 100644
--- a/resources/js/Pages/Imports/Templates/Create.vue
+++ b/resources/js/Pages/Imports/Templates/Create.vue
@@ -18,6 +18,7 @@ const form = useForm({
source_type: "csv",
default_record_type: "",
is_active: true,
+ reactivate: false,
client_uuid: null,
entities: [],
meta: {
@@ -285,6 +286,10 @@ watch(
Active
+
+
+ Reactivation import
+
diff --git a/resources/js/Pages/Imports/Templates/Edit.vue b/resources/js/Pages/Imports/Templates/Edit.vue
index 9618f16..688e875 100644
--- a/resources/js/Pages/Imports/Templates/Edit.vue
+++ b/resources/js/Pages/Imports/Templates/Edit.vue
@@ -20,6 +20,7 @@ const form = useForm({
source_type: props.template.source_type,
default_record_type: props.template.default_record_type || "",
is_active: props.template.is_active,
+ reactivate: props.template.reactivate ?? false,
client_uuid: props.template.client_uuid || null,
sample_headers: props.template.sample_headers || [],
// Add meta with default delimiter support
@@ -434,9 +435,11 @@ watch(
type="checkbox"
class="rounded"
/>
-
Aktivna
+
Aktivna
+
+
+ Reaktivacija
+
{
},
});
};
+
+// Contracts objects (Predmeti) modal state
+const objectsModal = reactive({ open: false, items: [], contract: null });
+function getContractObjects(c) {
+ if (!c) return [];
+ // Try a few common property names; fallback empty
+ return c.objects || c.contract_objects || c.items || [];
+}
+function openObjectsModal(c) {
+ objectsModal.contract = c;
+ objectsModal.items = getContractObjects(c) || [];
+ objectsModal.open = true;
+}
+function closeObjectsModal() {
+ objectsModal.open = false;
+ objectsModal.items = [];
+ objectsModal.contract = null;
+}
+
+// Client details (Stranka) summary
+const clientSummary = computed(() => {
+ const p = props.client?.person || {};
+ return {
+ name: p.full_name || p.name || "—",
+ tax: p.tax_number || p.davcna || p.tax || null,
+ emso: p.emso || p.ems || null,
+ trr: p.trr || p.bank_account || null,
+ };
+});
@@ -172,10 +222,13 @@ const submitComplete = () => {
-
- Stranka
-
-
+
+ {{ clientSummary.name }}
+ Naročnik
+
+
{
-
- Primer - oseba
-
-
+
+ {{ client_case.person.full_name }}
+ Primer
+
+
@@ -211,48 +267,82 @@ const submitComplete = () => {
-
-
-
{{ c.reference || c.uuid }}
-
Tip: {{ c.type?.name || "—" }}
-
-
-
-
- Odprto: {{ formatAmount(c.account.balance_amount) }} €
+
+
+
+
+
+ {{ c.reference || c.uuid }}
-
- + Aktivnost
-
-
+
+
+ Odprto
+ {{ formatAmount(c.account.balance_amount) }} €
- + Dokument
-
+
+
+ + Aktivnost
+
+
+ + Dokument
+
+
+
-
-
Predmet:
-
- {{
- c.last_object.name || c.last_object.reference
- }}
-
+
+
+ Zadnji predmet
+
+
+ {{ c.last_object.name || c.last_object.reference }}
+
({{ c.last_object.type }})
-
-
+
+
{{ c.last_object.description }}
-
+
@@ -270,40 +360,66 @@ const submitComplete = () => {
Aktivnosti
Nova
-
-
-
-
- {{ a.action?.name
- }}
→ {{ a.decision?.name }}
+
+
+
+
+
+ {{ activityActionLine(a) || "Aktivnost" }}
-
-
Pogodba: {{ a.contract.reference }}
-
-
{{
- new Date(a.created_at).toLocaleDateString("sl-SI")
- }}
-
· {{ a.user?.name || a.user_name }}
+
+
{{ formatDateShort(a.created_at) }}
+
+ {{ a.user?.name || a.user_name }}
-
{{ a.note }}
-
-
Zapadlost: {{ a.due_date }}
-
+
+ Pogodba: {{ a.contract.reference }}
+ Zapadlost: {{ formatDateShort(a.due_date) || a.due_date }}
+ Znesek: {{ formatAmount(a.amount) }} €
+ {{ a.status }}
+
+
+
+
+ {{ a.note }}
-
@@ -423,6 +539,65 @@ const submitComplete = () => {
+
+
+
+ Predmeti
+
+ {{ objectsModal.contract.reference || objectsModal.contract.uuid }}
+
+
+
+
+
+
+ {{ o.name || o.reference || "#" + (o.id || o.uuid || idx + 1) }}
+
+
+ {{ o.type }}
+ {{ o.status }}
+ {{ formatAmount(o.amount) }} €
+
+
+ {{ o.description }}
+
+
+
+ Ni predmetov.
+
+
+
+ Zapri
+
+
+
+
Dodaj dokument
@@ -493,4 +668,27 @@ const submitComplete = () => {
-
+
diff --git a/resources/js/Pages/Settings/Archive/Index.vue b/resources/js/Pages/Settings/Archive/Index.vue
new file mode 100644
index 0000000..319991c
--- /dev/null
+++ b/resources/js/Pages/Settings/Archive/Index.vue
@@ -0,0 +1,591 @@
+
+
+
+
+
+ Archive Settings
+
+
+
+
+
Archive rule conditions are temporarily inactive.
+
All enabled rules apply to the focus entity and its selected related tables without date/other filters. Stored condition JSON is preserved for future reactivation.
+
The "Run Now" action is currently disabled.
+
+
Chain Path Help
+
Supported chained related tables (dot notation):
+
+
Only these chains are processed; others are ignored.
+
+
+
+
+
+
+
+
+ {{ s.name || "Untitled Rule #" + s.id }}
+ Disabled
+
+
+ {{ s.description }}
+
+
+ Strategy: {{ s.strategy }} • Soft: {{ s.soft ? "Yes" : "No" }}
+
+
+
+
+ Edit
+
+
+
+ {{ s.enabled ? "Disable" : "Enable" }}
+
+
+ Delete
+
+
+
+
+
{{
+ JSON.stringify(s.entities, null, 2)
+ }}
+
+
+
+ No archive rules.
+
+
+
+
+
New Rule
+
+
+ Segment (optional)
+
+ -- none --
+
+ {{ seg.name }}
+
+
+
+
+ Action (optional)
+ {
+ newForm.decision_id = null;
+ }
+ "
+ class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
+ >
+ -- none --
+
+ {{ a.name }}
+
+
+
+
+ Decision (optional)
+
+ -- none --
+
+ {{ d.name }}
+
+
+
+
+
Name
+
+
+ {{ newForm.errors.name }}
+
+
+
+ Focus Entity
+
+ -- choose --
+
+ {{ ae.name || ae.focus }}
+
+
+
+
+
Related Tables
+
+
+
+ {{ r }}
+
+
+
+
+
Description
+
+
+ {{ newForm.errors.description }}
+
+
+
+
+ Enabled
+
+
+
+ Soft Archive
+
+
+
+ Reactivate (undo archive)
+
+
+
Strategy
+
+ Immediate
+ Scheduled
+ Queued
+ Manual (never auto-run)
+
+
+ {{ newForm.errors.strategy }}
+
+
+
+ Create
+
+
+ Please fix validation errors.
+
+
+
+
+
+ Edit Rule #{{ editingSetting.id }}
+
+
+
+ Manual strategy: this rule will only run when triggered manually.
+
+
+ Segment (optional)
+
+ -- none --
+
+ {{ seg.name }}
+
+
+
+
+ Action (optional)
+ {
+ editForm.decision_id = null;
+ }
+ "
+ class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
+ >
+ -- none --
+
+ {{ a.name }}
+
+
+
+
+ Decision (optional)
+
+ -- none --
+
+ {{ d.name }}
+
+
+
+
+
Name
+
+
+ {{ editForm.errors.name }}
+
+
+
+ Focus Entity
+
+ -- choose --
+
+ {{ ae.name || ae.focus }}
+
+
+
+
+
Related Tables
+
+
+
+ {{ r }}
+
+
+
+
+
Description
+
+
+ {{ editForm.errors.description }}
+
+
+
+
+ Enabled
+
+
+
+ Soft Archive
+
+
+
+ Reactivate (undo archive)
+
+
+
Strategy
+
+ Immediate
+ Scheduled
+ Queued
+ Manual (never auto-run)
+
+
+ {{ editForm.errors.strategy }}
+
+
+
+
+ Update
+
+
+ Cancel
+
+
+
+ Please fix validation errors.
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/Pages/Settings/Index.vue b/resources/js/Pages/Settings/Index.vue
index 45d1903..0b81507 100644
--- a/resources/js/Pages/Settings/Index.vue
+++ b/resources/js/Pages/Settings/Index.vue
@@ -1,41 +1,86 @@
-
-
-
-
-
-
-
Segments
-
Manage segments used across the app.
-
Open Segments
-
-
-
Payments
-
Defaults for payments and auto-activity.
-
Open Payment Settings
-
-
-
Workflow
-
Configure actions and decisions relationships.
-
Open Workflow
-
-
-
Field Job Settings
-
Configure segment-based field job rules.
-
Open Field Job
-
-
-
Contract Configs
-
Auto-assign initial segments for contracts by type.
-
Open Contract Configs
-
-
-
+
+
+
+
+
+
+
Segments
+
Manage segments used across the app.
+
+ Open Segments
+
+
+
Payments
+
+ Defaults for payments and auto-activity.
+
+
+ Open Payment Settings
+
+
+
Workflow
+
+ Configure actions and decisions relationships.
+
+
+ Open Workflow
+
+
+
Field Job Settings
+
+ Configure segment-based field job rules.
+
+
+ Open Field Job
+
+
+
Contract Configs
+
+ Auto-assign initial segments for contracts by type.
+
+
+ Open Contract Configs
+
+
+
Archive Settings
+
+ Define rules for archiving or soft-deleting aged data.
+
+
+ Open Archive Settings
+
-
-
\ No newline at end of file
+
+
+
+
diff --git a/routes/web.php b/routes/web.php
index bed1d17..03be493 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -3,6 +3,7 @@
use App\Charts\ExampleChart;
use App\Http\Controllers\AccountBookingController;
use App\Http\Controllers\AccountPaymentController;
+use App\Http\Controllers\ArchiveSettingController;
use App\Http\Controllers\CaseObjectController;
use App\Http\Controllers\ClientCaseContoller;
use App\Http\Controllers\ClientController;
@@ -149,6 +150,7 @@
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/{client_case:uuid}/contracts/{uuid}/archive', [ClientCaseContoller::class, 'archiveContract'])->name('clientCase.contract.archive');
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');
@@ -177,6 +179,12 @@
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');
+ // settings / archive settings
+ Route::get('settings/archive', [ArchiveSettingController::class, 'index'])->name('settings.archive.index');
+ Route::post('settings/archive', [ArchiveSettingController::class, 'store'])->name('settings.archive.store');
+ Route::put('settings/archive/{archiveSetting}', [ArchiveSettingController::class, 'update'])->name('settings.archive.update');
+ Route::post('settings/archive/{archiveSetting}/run', [ArchiveSettingController::class, 'run'])->name('settings.archive.run');
+ Route::delete('settings/archive/{archiveSetting}', [ArchiveSettingController::class, 'destroy'])->name('settings.archive.destroy');
Route::get('settings/workflow', [WorkflowController::class, 'index'])->name('settings.workflow');
Route::get('settings/field-job', [FieldJobSettingController::class, 'index'])->name('settings.fieldjob.index');
diff --git a/tests/Feature/ArchiveContractAccountChainTest.php b/tests/Feature/ArchiveContractAccountChainTest.php
new file mode 100644
index 0000000..e47f187
--- /dev/null
+++ b/tests/Feature/ArchiveContractAccountChainTest.php
@@ -0,0 +1,56 @@
+create();
+ $contract = Contract::factory()->create([
+ 'client_case_id' => $case->id,
+ 'active' => 1,
+ ]);
+ $accountTypeId = \DB::table('account_types')->insertGetId([
+ 'name' => 'Type',
+ 'description' => null,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ 'deleted_at' => null,
+ ]);
+ $account = Account::create([
+ 'contract_id' => $contract->id,
+ 'type_id' => $accountTypeId,
+ 'active' => 1,
+ 'initial_amount' => 0,
+ 'balance_amount' => 0,
+ ]);
+
+ ArchiveSetting::factory()->create([
+ 'enabled' => true,
+ 'strategy' => 'manual',
+ 'soft' => true,
+ 'entities' => [
+ ['table' => 'contracts', 'focus' => true],
+ ['table' => 'contracts.account'],
+ ],
+ ]);
+
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ $this->post(route('clientCase.contract.archive', ['client_case' => $case->uuid, 'uuid' => $contract->uuid]))
+ ->assertRedirect();
+
+ $this->assertDatabaseHas('accounts', ['id' => $account->id, 'active' => 0]);
+ }
+}
diff --git a/tests/Feature/ArchiveContractChainedEntitiesTest.php b/tests/Feature/ArchiveContractChainedEntitiesTest.php
new file mode 100644
index 0000000..8652a48
--- /dev/null
+++ b/tests/Feature/ArchiveContractChainedEntitiesTest.php
@@ -0,0 +1,84 @@
+create();
+ $contract = Contract::factory()->create([
+ 'client_case_id' => $case->id,
+ 'active' => 1,
+ ]);
+ // Create account tied to contract
+ // Minimal account type requirement
+ $accountTypeId = \DB::table('account_types')->insertGetId([
+ 'name' => 'Test Type',
+ 'description' => 'Temp',
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ 'deleted_at' => null,
+ ]);
+ $account = Account::create([
+ 'contract_id' => $contract->id,
+ 'type_id' => $accountTypeId,
+ 'active' => 1,
+ 'initial_amount' => 0,
+ 'balance_amount' => 0,
+ ]);
+
+ // Seed payments & bookings for that account
+ $payment = Payment::create([
+ 'account_id' => $account->id,
+ 'amount_cents' => 10000,
+ 'currency' => 'EUR',
+ 'reference' => 'P-TEST',
+ 'paid_at' => now(),
+ 'meta' => json_encode([]),
+ ]);
+ $booking = Booking::create([
+ 'account_id' => $account->id,
+ 'payment_id' => $payment->id,
+ 'amount_cents' => 10000,
+ 'type' => 'debit',
+ 'description' => 'Test Booking',
+ 'booked_at' => now(),
+ ]);
+
+ ArchiveSetting::factory()->create([
+ 'enabled' => true,
+ 'strategy' => 'manual',
+ 'soft' => true,
+ 'entities' => [
+ ['table' => 'contracts', 'focus' => true],
+ ['table' => 'account.payments'],
+ ['table' => 'account.bookings'],
+ ],
+ ]);
+
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ $this->post(route('clientCase.contract.archive', ['client_case' => $case->uuid, 'uuid' => $contract->uuid]))
+ ->assertRedirect();
+
+ // Refresh models
+ $payment->refresh();
+ $booking->refresh();
+
+ $this->assertDatabaseHas('payments', ['id' => $payment->id, 'active' => 0]);
+ $this->assertDatabaseHas('bookings', ['id' => $booking->id, 'active' => 0]);
+ }
+}
diff --git a/tests/Feature/ArchiveContractSegmentTest.php b/tests/Feature/ArchiveContractSegmentTest.php
new file mode 100644
index 0000000..4278fd4
--- /dev/null
+++ b/tests/Feature/ArchiveContractSegmentTest.php
@@ -0,0 +1,66 @@
+create();
+ $contract = Contract::factory()->create([
+ 'client_case_id' => $case->id,
+ 'active' => 1,
+ ]);
+
+ $originalSegment = Segment::factory()->create(['active' => true]);
+ $archiveSegment = Segment::factory()->create(['active' => true]);
+
+ DB::table('client_case_segment')->insert([
+ 'client_case_id' => $case->id,
+ 'segment_id' => $originalSegment->id,
+ 'active' => true,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+ DB::table('contract_segment')->insert([
+ 'contract_id' => $contract->id,
+ 'segment_id' => $originalSegment->id,
+ 'active' => true,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+
+ ArchiveSetting::factory()->create([
+ 'enabled' => true,
+ 'strategy' => 'manual',
+ 'segment_id' => $archiveSegment->id,
+ 'entities' => [
+ ['table' => 'contracts', 'focus' => true],
+ ],
+ ]);
+
+ $user = User::factory()->create();
+ $this->actingAs($user);
+
+ $response = $this->post(route('clientCase.contract.archive', ['client_case' => $case->uuid, 'uuid' => $contract->uuid]));
+ $response->assertRedirect();
+
+ $activePivots = DB::table('contract_segment')
+ ->where('contract_id', $contract->id)
+ ->where('active', true)
+ ->pluck('segment_id');
+
+ $this->assertTrue($activePivots->contains($archiveSegment->id));
+ $this->assertFalse($activePivots->contains($originalSegment->id));
+ }
+}
diff --git a/tests/Feature/ArchiveContractTest.php b/tests/Feature/ArchiveContractTest.php
new file mode 100644
index 0000000..cd1e9eb
--- /dev/null
+++ b/tests/Feature/ArchiveContractTest.php
@@ -0,0 +1,48 @@
+create();
+ test()->actingAs($user);
+
+ $case = ClientCase::factory()->create(['active' => 1]);
+ $typeId = DB::table('contract_types')->insertGetId([
+ 'name' => 'Standard',
+ 'description' => 'Test',
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+
+ $contract = $case->contracts()->create([
+ 'uuid' => (string) Str::uuid(),
+ 'reference' => 'T-TEST',
+ 'start_date' => now()->toDateString(),
+ 'end_date' => null,
+ 'type_id' => $typeId,
+ 'active' => 1,
+ ]);
+
+ ArchiveSetting::factory()->create([
+ 'entities' => [
+ ['table' => 'contracts', 'conditions' => ['where' => ['client_case_id' => $case->id]]],
+ ],
+ 'strategy' => 'immediate',
+ 'soft' => true,
+ 'enabled' => true,
+ ]);
+
+ test()->post(route('clientCase.contract.archive', [$case->uuid, $contract->uuid]))
+ ->assertRedirect();
+
+ $contract->refresh();
+ expect($contract->active)->toBe(0);
+});
diff --git a/tests/Feature/ArchiveRunNowTest.php b/tests/Feature/ArchiveRunNowTest.php
new file mode 100644
index 0000000..4bd3de3
--- /dev/null
+++ b/tests/Feature/ArchiveRunNowTest.php
@@ -0,0 +1,48 @@
+create();
+ test()->actingAs($user);
+
+ // Insert sample document older than 200 days (minimal required columns)
+ $docId = DB::table('documents')->insertGetId([
+ 'uuid' => (string) Str::uuid(),
+ 'documentable_type' => 'App\\Models\\ClientCase', // generic
+ 'documentable_id' => 1,
+ 'name' => 'Old Doc',
+ 'file_name' => 'old.txt',
+ 'original_name' => 'old.txt',
+ 'disk' => 'public',
+ 'path' => 'documents/old.txt',
+ 'mime_type' => 'text/plain',
+ 'active' => 1,
+ 'created_at' => now()->subDays(210),
+ 'updated_at' => now()->subDays(210),
+ ]);
+
+ $setting = ArchiveSetting::factory()->create([
+ 'entities' => [[
+ 'table' => 'documents',
+ 'conditions' => ['older_than_days' => 180],
+ ]],
+ 'enabled' => true,
+ 'strategy' => 'immediate',
+ ]);
+
+ test()->post(route('settings.archive.run', $setting->id))->assertRedirect();
+
+ $setting->refresh();
+ $docRow = DB::table('documents')->where('id', $docId)->first();
+ expect(ArchiveRun::count())->toBe(1)
+ ->and($docRow->active)->toBe(0);
+});
diff --git a/tests/Feature/ArchiveSettingCrudTest.php b/tests/Feature/ArchiveSettingCrudTest.php
new file mode 100644
index 0000000..5149ac2
--- /dev/null
+++ b/tests/Feature/ArchiveSettingCrudTest.php
@@ -0,0 +1,61 @@
+create();
+ test()->actingAs($user);
+
+ $response = test()->post(route('settings.archive.store'), [
+ 'entities' => [
+ ['table' => 'documents', 'conditions' => ['older_than_days' => 30]],
+ ],
+ 'strategy' => 'immediate',
+ 'soft' => true,
+ 'enabled' => true,
+ ]);
+
+ $response->assertRedirect();
+ expect(ArchiveSetting::count())->toBe(1);
+});
+
+it('updates an archive setting', function () {
+ $user = User::factory()->create();
+ test()->actingAs($user);
+ $setting = ArchiveSetting::factory()->create();
+
+ $response = test()->put(route('settings.archive.update', $setting->id), [
+ 'entities' => $setting->entities,
+ 'strategy' => 'queued',
+ 'soft' => false,
+ 'enabled' => false,
+ ]);
+
+ $response->assertRedirect();
+ $setting->refresh();
+ expect($setting->strategy)->toBe('queued')
+ ->and($setting->soft)->toBeFalse()
+ ->and($setting->enabled)->toBeFalse();
+});
+
+it('deletes an archive setting', function () {
+ $user = User::factory()->create();
+ test()->actingAs($user);
+ $setting = ArchiveSetting::factory()->create();
+
+ $response = test()->delete(route('settings.archive.destroy', $setting->id));
+ $response->assertRedirect();
+ expect(ArchiveSetting::withTrashed()->count())->toBe(1) // soft deleted
+ ->and(ArchiveSetting::count())->toBe(0);
+});
diff --git a/tests/Feature/ReactivateContractTest.php b/tests/Feature/ReactivateContractTest.php
new file mode 100644
index 0000000..2e8bffb
--- /dev/null
+++ b/tests/Feature/ReactivateContractTest.php
@@ -0,0 +1,50 @@
+create();
+ test()->actingAs($user);
+
+ $case = ClientCase::factory()->create(['active' => 1]);
+ $typeId = DB::table('contract_types')->insertGetId([
+ 'name' => 'Standard',
+ 'description' => 'Test',
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+
+ $contract = $case->contracts()->create([
+ 'uuid' => (string) Str::uuid(),
+ 'reference' => 'T-REACT',
+ 'start_date' => now()->toDateString(),
+ 'end_date' => null,
+ 'type_id' => $typeId,
+ 'active' => 0, // initially archived
+ 'deleted_at' => now(), // also soft deleted to test clearing
+ ]);
+
+ ArchiveSetting::factory()->create([
+ 'entities' => [
+ ['table' => 'contracts', 'conditions' => ['where' => ['client_case_id' => $case->id]]],
+ ],
+ 'strategy' => 'immediate',
+ 'soft' => true,
+ 'enabled' => true,
+ 'reactivate' => true,
+ ]);
+
+ test()->post(route('clientCase.contract.archive', ['client_case' => $case->uuid, 'uuid' => $contract->uuid]))
+ ->assertRedirect();
+
+ $contract->refresh();
+ expect($contract->active)->toBe(1)
+ ->and($contract->deleted_at)->toBeNull();
+});