Mass archiving added to segment view show

This commit is contained in:
Simon Pocrnjič 2026-01-15 21:16:26 +01:00
parent ca8754cd94
commit 6a2dd860fa
3 changed files with 282 additions and 8 deletions

View File

@ -1585,6 +1585,156 @@ public function archiveContract(ClientCase $clientCase, string $uuid, Request $r
); );
} }
/**
* Archive multiple contracts in a batch operation
*/
public function archiveBatch(Request $request)
{
$validated = $request->validate([
'contracts' => 'required|array',
'contracts.*' => 'required|uuid|exists:contracts,uuid',
'reactivate' => 'boolean',
]);
$reactivate = $validated['reactivate'] ?? false;
// Get archive setting
$setting = \App\Models\ArchiveSetting::query()
->where('enabled', true)
->whereIn('strategy', ['immediate', 'manual'])
->where('reactivate', $reactivate)
->orderByDesc('id')
->first();
if (! $setting) {
\Log::warning('No archive settings found for batch archive');
return back()->with('flash', [
'error' => 'No archive settings found',
]);
}
$executor = app(\App\Services\Archiving\ArchiveExecutor::class);
$successCount = 0;
$skippedCount = 0;
$errors = [];
foreach ($validated['contracts'] as $contractUuid) {
try {
$contract = Contract::where('uuid', $contractUuid)->firstOrFail();
// Skip if contract is already archived (active = 0)
if (!$contract->active) {
$skippedCount++;
continue;
}
$clientCase = $contract->clientCase;
$context = [
'contract_id' => $contract->id,
'client_case_id' => $clientCase->id,
'account_id' => $contract->account->id ?? null,
];
// Execute archive setting
$executor->executeSetting($setting, $context, \Auth::id());
// Transaction for segment updates and activity logging
\DB::transaction(function () use ($contract, $clientCase, $setting, $reactivate) {
// Create activity log
if ($setting->action_id && $setting->decision_id) {
$activityData = [
'client_case_id' => $clientCase->id,
'action_id' => $setting->action_id,
'decision_id' => $setting->decision_id,
'note' => ($reactivate)
? "Ponovno aktivirana pogodba $contract->reference"
: "Arhivirana pogodba $contract->reference",
];
try {
\App\Models\Activity::create($activityData);
} catch (Exception $e) {
\Log::warning('Activity could not be created during batch archive');
}
}
// Move to archive segment if specified
if ($setting->segment_id) {
$segmentId = $setting->segment_id;
// Deactivate all current segments
$contract->segments()
->allRelatedIds()
->map(fn (int $val) => $contract->segments()->updateExistingPivot($val, [
'active' => false,
'updated_at' => now(),
]));
// Activate archive segment
if ($contract->attachedSegments()->find($segmentId)->pluck('id')->isNotEmpty()) {
$contract->attachedSegments()->updateExistingPivot($segmentId, [
'active' => true,
'updated_at' => now(),
]);
} else {
$contract->segments()->attach($segmentId, [
'active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
// Cancel pending field jobs
$contract->fieldJobs()
->whereNull('completed_at')
->whereNull('cancelled_at')
->update([
'cancelled_at' => date('Y-m-d'),
'updated_at' => now(),
]);
});
$successCount++;
} catch (Exception $e) {
\Log::error('Error archiving contract in batch', [
'uuid' => $contractUuid,
'error' => $e->getMessage(),
]);
$errors[] = [
'uuid' => $contractUuid,
'error' => $e->getMessage(),
];
}
}
if (count($errors) > 0) {
$message = "Archived $successCount contracts";
if ($skippedCount > 0) {
$message .= ", skipped $skippedCount already archived";
}
$message .= ", " . count($errors) . " failed";
return back()->with('flash', [
'error' => $message,
'details' => $errors,
]);
}
$message = $reactivate
? "Successfully reactivated $successCount contracts"
: "Successfully archived $successCount contracts";
if ($skippedCount > 0) {
$message .= " ($skippedCount already archived)";
}
return back()->with('flash', [
'success' => $message,
]);
}
/** /**
* Emergency: recreate a missing / soft-deleted person for a client case and re-link related data. * Emergency: recreate a missing / soft-deleted person for a client case and re-link related data.
*/ */

View File

@ -1,10 +1,11 @@
<script setup> <script setup>
import AppLayout from "@/Layouts/AppLayout.vue"; import AppLayout from "@/Layouts/AppLayout.vue";
import { Link, router } from "@inertiajs/vue3"; import { Link, router, useForm, usePage } from "@inertiajs/vue3";
import { ref, computed, watch } from "vue"; import { ref, computed, watch } from "vue";
import axios from "axios"; import axios from "axios";
import DataTableServer from "@/Components/DataTable/DataTableServer.vue"; import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
import DialogModal from "@/Components/DialogModal.vue"; import DialogModal from "@/Components/DialogModal.vue";
import ConfirmDialog from "@/Components/ConfirmDialog.vue";
const props = defineProps({ const props = defineProps({
segment: Object, segment: Object,
@ -20,6 +21,7 @@ const selectedClient = ref(initialClient);
// Column definitions for the server-driven table // Column definitions for the server-driven table
const columns = [ const columns = [
{ key: "select", label: "", sortable: false, width: "50px" },
{ key: "reference", label: "Pogodba", sortable: true }, { key: "reference", label: "Pogodba", sortable: true },
{ key: "client_case", label: "Primer" }, { key: "client_case", label: "Primer" },
{ key: "address", label: "Naslov" }, { key: "address", label: "Naslov" },
@ -36,6 +38,13 @@ const exportColumns = ref(columns.map((col) => col.key));
const exportError = ref(""); const exportError = ref("");
const isExporting = ref(false); const isExporting = ref(false);
const selectedRows = ref([]);
const showConfirmDialog = ref(false);
const archiveForm = useForm({
contracts: [],
reactivate: false,
});
const contractsCurrentPage = computed(() => props.contracts?.current_page ?? 1); const contractsCurrentPage = computed(() => props.contracts?.current_page ?? 1);
const contractsPerPage = computed(() => props.contracts?.per_page ?? 15); const contractsPerPage = computed(() => props.contracts?.per_page ?? 15);
const totalContracts = computed( const totalContracts = computed(
@ -48,6 +57,11 @@ const exportDisabled = computed(
() => exportColumns.value.length === 0 || isExporting.value () => exportColumns.value.length === 0 || isExporting.value
); );
const canManageSettings = computed(() => {
const permissions = usePage().props?.auth?.user?.permissions || [];
return permissions.includes('manage-settings');
});
function toggleAllColumns(checked) { function toggleAllColumns(checked) {
exportColumns.value = checked ? columns.map((col) => col.key) : []; exportColumns.value = checked ? columns.map((col) => col.key) : [];
} }
@ -205,6 +219,67 @@ function extractFilenameFromHeaders(headers) {
const asciiMatch = disposition.match(/filename="?([^";]+)"?/i); const asciiMatch = disposition.match(/filename="?([^";]+)"?/i);
return asciiMatch?.[1] || null; return asciiMatch?.[1] || null;
} }
function toggleSelectAll() {
if (selectedRows.value.length === props.contracts.data.length) {
selectedRows.value = [];
} else {
selectedRows.value = props.contracts.data.map((row) => row.uuid);
}
}
function toggleRowSelection(uuid) {
const index = selectedRows.value.indexOf(uuid);
if (index > -1) {
selectedRows.value.splice(index, 1);
} else {
selectedRows.value.push(uuid);
}
}
function isRowSelected(uuid) {
return selectedRows.value.includes(uuid);
}
function isAllSelected() {
return (
props.contracts.data.length > 0 &&
selectedRows.value.length === props.contracts.data.length
);
}
function isIndeterminate() {
return (
selectedRows.value.length > 0 &&
selectedRows.value.length < props.contracts.data.length
);
}
function openArchiveModal() {
if (!selectedRows.value.length) return;
showConfirmDialog.value = true;
}
function closeConfirmDialog() {
showConfirmDialog.value = false;
}
function submitArchive() {
if (!selectedRows.value.length) return;
showConfirmDialog.value = false;
archiveForm.contracts = [...selectedRows.value];
archiveForm.reactivate = false;
archiveForm.post(route("contracts.archive-batch"), {
preserveScroll: true,
onSuccess: () => {
selectedRows.value = [];
router.reload({ only: ["contracts"] });
},
});
}
</script> </script>
<template> <template>
@ -259,6 +334,7 @@ function extractFilenameFromHeaders(headers) {
row-key="uuid" row-key="uuid"
> >
<template #toolbar-extra> <template #toolbar-extra>
<div class="flex items-center gap-2">
<button <button
type="button" type="button"
class="inline-flex items-center rounded-md border border-indigo-200 bg-white px-3 py-2 text-sm font-medium text-indigo-700 shadow-sm hover:bg-indigo-50" class="inline-flex items-center rounded-md border border-indigo-200 bg-white px-3 py-2 text-sm font-medium text-indigo-700 shadow-sm hover:bg-indigo-50"
@ -266,6 +342,40 @@ function extractFilenameFromHeaders(headers) {
> >
Izvozi v Excel Izvozi v Excel
</button> </button>
<div v-if="canManageSettings && selectedRows.length" class="flex items-center gap-2">
<span class="text-sm text-gray-600"
>{{ selectedRows.length }} izbran{{
selectedRows.length === 1 ? "a" : "ih"
}}</span
>
<button
type="button"
class="inline-flex items-center rounded-md border border-red-200 bg-white px-3 py-2 text-sm font-medium text-red-700 shadow-sm hover:bg-red-50"
@click="openArchiveModal"
>
Arhiviraj izbrane
</button>
</div>
</div>
</template>
<template #header-select>
<input
v-if="canManageSettings"
type="checkbox"
:checked="isAllSelected()"
:indeterminate="isIndeterminate()"
@change="toggleSelectAll"
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
</template>
<template #cell-select="{ row }">
<input
v-if="canManageSettings"
type="checkbox"
:checked="isRowSelected(row.uuid)"
@change="toggleRowSelection(row.uuid)"
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
</template> </template>
<!-- Primer (client_case) cell with link when available --> <!-- Primer (client_case) cell with link when available -->
<template #cell-client_case="{ row }"> <template #cell-client_case="{ row }">
@ -316,6 +426,19 @@ function extractFilenameFromHeaders(headers) {
</div> </div>
</div> </div>
<ConfirmDialog
:show="showConfirmDialog"
title="Arhiviraj pogodbe"
:message="`Ali ste prepričani, da želite arhivirati ${selectedRows.length} pogodb${
selectedRows.length === 1 ? 'o' : ''
}? Arhivirane pogodbe bodo odstranjene iz aktivnih segmentov.`"
confirm-text="Arhiviraj"
cancel-text="Prekliči"
:danger="true"
@close="closeConfirmDialog"
@confirm="submitArchive"
/>
<DialogModal :show="exportDialogOpen" max-width="3xl" @close="closeExportDialog"> <DialogModal :show="exportDialogOpen" max-width="3xl" @close="closeExportDialog">
<template #title> <template #title>
<div> <div>

View File

@ -322,6 +322,7 @@
Route::get('client-cases/{client_case:uuid}', [ClientCaseContoller::class, 'show'])->name('clientCase.show'); 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}/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/{client_case:uuid}/contracts/{uuid}/archive', [ClientCaseContoller::class, 'archiveContract'])->name('clientCase.contract.archive');
Route::post('contracts/archive-batch', [ClientCaseContoller::class, 'archiveBatch'])->name('contracts.archive-batch')->middleware('permission:manage-settings');
Route::post('client-cases', [ClientCaseContoller::class, 'store'])->name('clientCase.store'); Route::post('client-cases', [ClientCaseContoller::class, 'store'])->name('clientCase.store');
Route::post('client-cases/{client_case:uuid}/emergency-person', [ClientCaseContoller::class, 'emergencyCreatePerson'])->name('clientCase.emergencyPerson'); Route::post('client-cases/{client_case:uuid}/emergency-person', [ClientCaseContoller::class, 'emergencyCreatePerson'])->name('clientCase.emergencyPerson');
// client-case / contract // client-case / contract