Changes
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { router } from "@inertiajs/vue3";
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { ScrollArea } from "@/Components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import { Plus, Trash2 } from "lucide-vue-next";
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
client_case: { type: Object, required: true },
|
||||
contract: { type: Object, default: null },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close"]);
|
||||
|
||||
const processing = ref(false);
|
||||
const metaEntries = ref([]);
|
||||
|
||||
// Extract meta entries from contract
|
||||
function extractMetaEntries(contract) {
|
||||
if (!contract?.meta) return [];
|
||||
|
||||
const results = [];
|
||||
const visit = (node, keyName) => {
|
||||
if (node === null || node === undefined) return;
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach((el) => visit(el));
|
||||
return;
|
||||
}
|
||||
if (typeof node === "object") {
|
||||
const hasValue = Object.prototype.hasOwnProperty.call(node, "value");
|
||||
const hasTitle = Object.prototype.hasOwnProperty.call(node, "title");
|
||||
if (hasValue || hasTitle) {
|
||||
const title = (node.title || keyName || "").toString().trim() || keyName || "";
|
||||
results.push({
|
||||
title,
|
||||
value: node.value ?? "",
|
||||
type: node.type || "string",
|
||||
});
|
||||
return;
|
||||
}
|
||||
for (const [k, v] of Object.entries(node)) {
|
||||
visit(v, k);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (keyName) {
|
||||
results.push({ title: keyName, value: node ?? "", type: "string" });
|
||||
}
|
||||
};
|
||||
visit(contract.meta, undefined);
|
||||
return results;
|
||||
}
|
||||
|
||||
// Initialize meta entries when dialog opens
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
if (newVal && props.contract) {
|
||||
const entries = extractMetaEntries(props.contract);
|
||||
metaEntries.value =
|
||||
entries.length > 0 ? entries : [{ title: "", value: "", type: "string" }];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function addEntry() {
|
||||
metaEntries.value.push({ title: "", value: "", type: "string" });
|
||||
}
|
||||
|
||||
function removeEntry(index) {
|
||||
metaEntries.value.splice(index, 1);
|
||||
if (metaEntries.value.length === 0) {
|
||||
metaEntries.value.push({ title: "", value: "", type: "string" });
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit("close");
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (!props.contract?.uuid || processing.value) return;
|
||||
|
||||
// Filter out empty entries and build meta object
|
||||
const validEntries = metaEntries.value.filter((e) => e.title && e.title.trim() !== "");
|
||||
|
||||
const meta = {};
|
||||
validEntries.forEach((entry) => {
|
||||
meta[entry.title] = {
|
||||
title: entry.title,
|
||||
value: entry.value,
|
||||
type: entry.type,
|
||||
};
|
||||
});
|
||||
|
||||
processing.value = true;
|
||||
|
||||
router.patch(
|
||||
route("clientCase.contract.patchMeta", {
|
||||
client_case: props.client_case.uuid,
|
||||
uuid: props.contract.uuid,
|
||||
}),
|
||||
{ meta },
|
||||
{
|
||||
preserveScroll: true,
|
||||
only: ["contracts"],
|
||||
onSuccess: () => {
|
||||
close();
|
||||
processing.value = false;
|
||||
},
|
||||
onError: () => {
|
||||
processing.value = false;
|
||||
},
|
||||
onFinish: () => {
|
||||
processing.value = false;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogModal :show="show" max-width="3xl" @close="close">
|
||||
<template #title>
|
||||
<h3 class="text-lg font-semibold leading-6 text-foreground">Uredi Meta podatke</h3>
|
||||
</template>
|
||||
<template #description>
|
||||
Posodobi meta podatke za pogodbo {{ contract?.reference }}
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<form id="meta-edit-form" @submit.prevent="submit" class="space-y-4">
|
||||
<ScrollArea class="h-[60vh]">
|
||||
<div class="space-y-3 pr-4">
|
||||
<div
|
||||
v-for="(entry, index) in metaEntries"
|
||||
:key="index"
|
||||
class="flex items-start gap-2 p-3 border rounded-lg bg-muted/20"
|
||||
>
|
||||
<div class="flex-1 space-y-3">
|
||||
<div>
|
||||
<Label :for="`meta-title-${index}`">Naziv</Label>
|
||||
<Input
|
||||
:id="`meta-title-${index}`"
|
||||
v-model="entry.title"
|
||||
placeholder="Vnesi naziv..."
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label :for="`meta-type-${index}`">Tip</Label>
|
||||
<Select v-model="entry.type">
|
||||
<SelectTrigger :id="`meta-type-${index}`" class="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="string">Tekst</SelectItem>
|
||||
<SelectItem value="number">Številka</SelectItem>
|
||||
<SelectItem value="date">Datum</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label :for="`meta-value-${index}`">Vrednost</Label>
|
||||
<Input
|
||||
:id="`meta-value-${index}`"
|
||||
v-model="entry.value"
|
||||
:type="
|
||||
entry.type === 'date'
|
||||
? 'date'
|
||||
: entry.type === 'number'
|
||||
? 'number'
|
||||
: 'text'
|
||||
"
|
||||
:step="entry.type === 'number' ? '0.01' : undefined"
|
||||
placeholder="Vnesi vrednost..."
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@click="removeEntry(index)"
|
||||
:disabled="metaEntries.length === 1"
|
||||
class="mt-6"
|
||||
>
|
||||
<Trash2 class="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<Button type="button" variant="outline" @click="addEntry" class="w-full">
|
||||
<Plus class="h-4 w-4 mr-2" />
|
||||
Dodaj vnos
|
||||
</Button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button type="button" variant="ghost" @click="close" :disabled="processing">
|
||||
Prekliči
|
||||
</Button>
|
||||
<Button type="submit" form="meta-edit-form" :disabled="processing">
|
||||
{{ processing ? "Shranjujem..." : "Shrani" }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</template>
|
||||
@@ -15,6 +15,7 @@ import CaseObjectCreateDialog from "./CaseObjectCreateDialog.vue";
|
||||
import CaseObjectsDialog from "./CaseObjectsDialog.vue";
|
||||
import PaymentDialog from "./PaymentDialog.vue";
|
||||
import ViewPaymentsDialog from "./ViewPaymentsDialog.vue";
|
||||
import ContractMetaEditDialog from "./ContractMetaEditDialog.vue";
|
||||
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
|
||||
import ConfirmationDialog from "@/Components/Dialogs/ConfirmationDialog.vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
@@ -33,6 +34,16 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import EmptyState from "@/Components/EmptyState.vue";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Textarea } from "@/Components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
|
||||
const props = defineProps({
|
||||
client: { type: Object, default: null },
|
||||
@@ -433,6 +444,19 @@ const closePaymentsDialog = () => {
|
||||
selectedContract.value = null;
|
||||
};
|
||||
|
||||
// Meta edit dialog
|
||||
const showMetaEditDialog = ref(false);
|
||||
|
||||
const openMetaEditDialog = (c) => {
|
||||
selectedContract.value = c;
|
||||
showMetaEditDialog.value = true;
|
||||
};
|
||||
|
||||
const closeMetaEditDialog = () => {
|
||||
showMetaEditDialog.value = false;
|
||||
selectedContract.value = null;
|
||||
};
|
||||
|
||||
// Columns configuration
|
||||
const columns = computed(() => [
|
||||
{ key: "reference", label: "Ref.", sortable: false, align: "center" },
|
||||
@@ -638,6 +662,19 @@ const availableSegmentsCount = computed(() => {
|
||||
<div class="text-gray-500">Ni meta podatkov.</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="edit && row.active" class="border-t border-gray-200 mt-2 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="openMetaEditDialog(row)"
|
||||
class="w-full flex items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-100 rounded transition-colors"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="faPenToSquare"
|
||||
class="h-3.5 w-3.5 text-gray-600"
|
||||
/>
|
||||
<span>Uredi meta podatke</span>
|
||||
</button>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -901,6 +938,13 @@ const availableSegmentsCount = computed(() => {
|
||||
:edit="edit"
|
||||
/>
|
||||
|
||||
<ContractMetaEditDialog
|
||||
:show="showMetaEditDialog"
|
||||
:client_case="client_case"
|
||||
:contract="selectedContract"
|
||||
@close="closeMetaEditDialog"
|
||||
/>
|
||||
|
||||
<!-- Generate Document Dialog -->
|
||||
<CreateDialog
|
||||
:show="showGenerateDialog"
|
||||
@@ -913,18 +957,18 @@ const availableSegmentsCount = computed(() => {
|
||||
@confirm="submitGenerate"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Predloga</label>
|
||||
<select
|
||||
v-model="selectedTemplateSlug"
|
||||
@change="onTemplateChange"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
|
||||
>
|
||||
<option :value="null">Izberi predlogo...</option>
|
||||
<option v-for="t in templates" :key="t.slug" :value="t.slug">
|
||||
{{ t.name }} (v{{ t.version }})
|
||||
</option>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<Label>Predloga</Label>
|
||||
<Select v-model="selectedTemplateSlug" @update:model-value="onTemplateChange">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Izberi predlogo..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="t in templates" :key="t.slug" :value="t.slug">
|
||||
{{ t.name }} (v{{ t.version }})
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- Custom inputs -->
|
||||
@@ -932,14 +976,30 @@ const availableSegmentsCount = computed(() => {
|
||||
<div class="border-t border-gray-200 pt-4">
|
||||
<h3 class="text-sm font-medium text-gray-700 mb-3">Prilagojene vrednosti</h3>
|
||||
<div class="space-y-3">
|
||||
<div v-for="token in customTokenList" :key="token">
|
||||
<label class="block text-sm font-medium text-gray-700">
|
||||
<div v-for="token in customTokenList" :key="token" class="space-y-2">
|
||||
<Label>
|
||||
{{ token.replace(/^custom\./, "") }}
|
||||
</label>
|
||||
<input
|
||||
</Label>
|
||||
<Textarea
|
||||
v-if="templateCustomTypes[token.replace(/^custom\./, '')] === 'text'"
|
||||
v-model="customInputs[token.replace(/^custom\./, '')]"
|
||||
type="text"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
|
||||
rows="3"
|
||||
/>
|
||||
<Input
|
||||
v-else
|
||||
v-model="customInputs[token.replace(/^custom\./, '')]"
|
||||
:type="
|
||||
templateCustomTypes[token.replace(/^custom\./, '')] === 'date'
|
||||
? 'date'
|
||||
: templateCustomTypes[token.replace(/^custom\./, '')] === 'number'
|
||||
? 'number'
|
||||
: 'text'
|
||||
"
|
||||
:step="
|
||||
templateCustomTypes[token.replace(/^custom\./, '')] === 'number'
|
||||
? '0.01'
|
||||
: undefined
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -948,26 +1008,30 @@ const availableSegmentsCount = computed(() => {
|
||||
|
||||
<!-- Address overrides -->
|
||||
<div class="border-t border-gray-200 pt-4 space-y-3">
|
||||
<h3 class="text-sm font-medium text-gray-700">Naslovi</h3>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Naslov stranke</label>
|
||||
<select
|
||||
v-model="clientAddressSource"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
|
||||
>
|
||||
<option value="client">Stranka</option>
|
||||
<option value="case_person">Oseba primera</option>
|
||||
</select>
|
||||
<h3 class="text-sm font-medium text-gray-700 mb-2">Naslovi</h3>
|
||||
<div class="space-y-2">
|
||||
<Label>Naslov stranke</Label>
|
||||
<Select v-model="clientAddressSource">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="client">Stranka</SelectItem>
|
||||
<SelectItem value="case_person">Oseba primera</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Naslov osebe</label>
|
||||
<select
|
||||
v-model="personAddressSource"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
|
||||
>
|
||||
<option value="case_person">Oseba primera</option>
|
||||
<option value="client">Stranka</option>
|
||||
</select>
|
||||
<div class="space-y-2">
|
||||
<Label>Naslov osebe</Label>
|
||||
<Select v-model="personAddressSource">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="case_person">Oseba primera</SelectItem>
|
||||
<SelectItem value="client">Stranka</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -210,14 +210,6 @@ const closeDrawer = () => {
|
||||
drawerAddActivity.value = false;
|
||||
};
|
||||
|
||||
const showClientDetails = () => {
|
||||
clientDetails.value = false;
|
||||
};
|
||||
|
||||
const hideClietnDetails = () => {
|
||||
clientDetails.value = true;
|
||||
};
|
||||
|
||||
// Attach segment to case
|
||||
const showAttachSegment = ref(false);
|
||||
const openAttachSegment = () => {
|
||||
|
||||
@@ -24,18 +24,31 @@ import DateRangePicker from "@/Components/DateRangePicker.vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { ButtonGroup } from "@/Components/ui/button-group";
|
||||
import AppPopover from "@/Components/app/ui/AppPopover.vue";
|
||||
import { Filter, LinkIcon, FileDown } from "lucide-vue-next";
|
||||
import { Filter, LinkIcon, FileDown, LayoutIcon } from "lucide-vue-next";
|
||||
import { Card } from "@/Components/ui/card";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { hasPermission } from "@/Services/permissions";
|
||||
import InputLabel from "@/Components/InputLabel.vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/Components/ui/dropdown-menu";
|
||||
import { toNumber } from "lodash";
|
||||
import { FormControl, FormField, FormFieldArray, FormLabel } from "@/Components/ui/form";
|
||||
import { Field, FieldLabel } from "@/Components/ui/field";
|
||||
import { toTypedSchema } from "@vee-validate/zod";
|
||||
import { z } from "zod";
|
||||
import FormChangeSegment from "./Partials/FormChangeSegment.vue";
|
||||
|
||||
const props = defineProps({
|
||||
client: Object,
|
||||
contracts: Object,
|
||||
filters: Object,
|
||||
segments: Object,
|
||||
segments: Array,
|
||||
types: Object,
|
||||
});
|
||||
|
||||
@@ -59,10 +72,20 @@ const selectedSegments = ref(
|
||||
: []
|
||||
);
|
||||
const filterPopoverOpen = ref(false);
|
||||
const selectedContracts = ref([]);
|
||||
const changeSegmentDialogOpen = ref(false);
|
||||
const contractTable = ref(null);
|
||||
|
||||
const exportDialogOpen = ref(false);
|
||||
const exportScope = ref("current");
|
||||
const exportColumns = ref(["reference", "customer", "address", "start", "segment", "balance"]);
|
||||
const exportColumns = ref([
|
||||
"reference",
|
||||
"customer",
|
||||
"address",
|
||||
"start",
|
||||
"segment",
|
||||
"balance",
|
||||
]);
|
||||
const exportError = ref("");
|
||||
const isExporting = ref(false);
|
||||
|
||||
@@ -85,6 +108,12 @@ const allColumnsSelected = computed(
|
||||
const exportDisabled = computed(
|
||||
() => exportColumns.value.length === 0 || isExporting.value
|
||||
);
|
||||
const segmentSelectItems = computed(() =>
|
||||
props.segments.map((val, i) => ({
|
||||
label: val.name,
|
||||
value: val.id,
|
||||
}))
|
||||
);
|
||||
|
||||
function applyDateFilter() {
|
||||
filterPopoverOpen.value = false;
|
||||
@@ -288,6 +317,24 @@ function extractFilenameFromHeaders(headers) {
|
||||
const asciiMatch = disposition.match(/filename="?([^";]+)"?/i);
|
||||
return asciiMatch?.[1] || null;
|
||||
}
|
||||
|
||||
function handleSelectionChange(selectedKeys) {
|
||||
selectedContracts.value = selectedKeys.map((val, i) => {
|
||||
const num = toNumber(val);
|
||||
|
||||
return props.contracts.data[num].uuid;
|
||||
});
|
||||
}
|
||||
|
||||
function openDialogChangeSegment() {
|
||||
changeSegmentDialogOpen.value = true;
|
||||
}
|
||||
|
||||
function clearContractTableSelected() {
|
||||
if (contractTable.value) {
|
||||
contractTable.value.clearSelection();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -357,6 +404,7 @@ function extractFilenameFromHeaders(headers) {
|
||||
</Link>
|
||||
</div>
|
||||
<DataTable
|
||||
ref="contractTable"
|
||||
:columns="[
|
||||
{ key: 'reference', label: 'Referenca', sortable: false },
|
||||
{ key: 'customer', label: 'Stranka', sortable: false },
|
||||
@@ -380,11 +428,13 @@ function extractFilenameFromHeaders(headers) {
|
||||
row-key="uuid"
|
||||
:only-props="['contracts']"
|
||||
:page-size-options="[10, 15, 25, 50, 100]"
|
||||
:enable-row-selection="true"
|
||||
@selection:change="handleSelectionChange"
|
||||
page-param-name="contracts_page"
|
||||
per-page-param-name="contracts_per_page"
|
||||
:show-toolbar="true"
|
||||
>
|
||||
<template #toolbar-filters>
|
||||
<template #toolbar-filters="{ table }">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<AppPopover
|
||||
v-model:open="filterPopoverOpen"
|
||||
@@ -481,6 +531,32 @@ function extractFilenameFromHeaders(headers) {
|
||||
<FileDown class="h-4 w-4" />
|
||||
Izvozi v Excel
|
||||
</Button>
|
||||
<DropdownMenu v-if="table.getSelectedRowModel().rows.length > 0">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button class="gap-2 px-3" variant="outline">
|
||||
<Badge
|
||||
class="h-5 min-w-5 rounded-full font-mono tabular-nums text-accent"
|
||||
variant="destructive"
|
||||
>
|
||||
{{ table.getSelectedRowModel().rows.length }}
|
||||
</Badge>
|
||||
Akcija
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem @click="openDialogChangeSegment">
|
||||
<LayoutIcon />
|
||||
Spremeni segment
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant="outline"
|
||||
@click="clearContractTableSelected"
|
||||
v-if="table.getSelectedRowModel().rows.length > 0"
|
||||
>
|
||||
Odznači izbrane
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-reference="{ row }">
|
||||
@@ -519,7 +595,7 @@ function extractFilenameFromHeaders(headers) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Excel export dialog -->
|
||||
<DialogModal :show="exportDialogOpen" max-width="3xl" @close="closeExportDialog">
|
||||
<template #title>
|
||||
<div class="space-y-1">
|
||||
@@ -626,5 +702,15 @@ function extractFilenameFromHeaders(headers) {
|
||||
</div>
|
||||
</template>
|
||||
</DialogModal>
|
||||
|
||||
<!-- Change segment selected contracts dialog -->
|
||||
|
||||
<FormChangeSegment
|
||||
:show="changeSegmentDialogOpen"
|
||||
@close="changeSegmentDialogOpen = false"
|
||||
:segments="segmentSelectItems"
|
||||
:contracts="selectedContracts"
|
||||
:clear-selected-rows="clearContractTableSelected"
|
||||
/>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup>
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
} from "@/Components/ui/field";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import { toTypedSchema } from "@vee-validate/zod";
|
||||
import { useForm, Field as VeeField } from "vee-validate";
|
||||
import { router } from "@inertiajs/vue3";
|
||||
import { onMounted, ref } from "vue";
|
||||
import z from "zod";
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
segments: { type: Array, default: [] },
|
||||
contracts: { type: Array, default: [] },
|
||||
clearSelectedRows: { type: Function, default: () => console.log("test") },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close"]);
|
||||
|
||||
const close = () => {
|
||||
emit("close");
|
||||
};
|
||||
|
||||
const processing = ref(false);
|
||||
|
||||
// vee-validate Form setup
|
||||
const formSchema = toTypedSchema(
|
||||
z.object({
|
||||
segment_id: z
|
||||
.number()
|
||||
.refine((val) => props.segments.find((item) => item.value == val) !== undefined, {
|
||||
message: "Izbran segment ne obstaja v zbirki segmentov",
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const { handleSubmit, resetForm, errors } = useForm({
|
||||
validationSchema: formSchema,
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
processing.value = true;
|
||||
router.patch(
|
||||
route("contracts.segment"),
|
||||
{
|
||||
...data,
|
||||
contracts: props.contracts,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
router.reload({ only: ["contracts"] });
|
||||
close();
|
||||
resetForm();
|
||||
props.clearSelectedRows();
|
||||
processing.value = false;
|
||||
},
|
||||
onError: (e) => {
|
||||
errors = e;
|
||||
|
||||
processing.value = false;
|
||||
},
|
||||
onFinish: () => {
|
||||
processing.value = false;
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
console.log(props.segments);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogModal :show="show" @close="close">
|
||||
<template #title>
|
||||
<h3 class="text-lg font-semibold leading-6 text-foreground">
|
||||
Spremeni segment pogodbam
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<form id="segment-change-form" @submit.prevent="onSubmit">
|
||||
<VeeField v-slot="{ field, errors }" name="segment_id">
|
||||
<Field orientation="responsive" :data-invalid="!!errors.length">
|
||||
<FieldContent>
|
||||
<FieldLabel for="segment">Segment</FieldLabel>
|
||||
<FieldDescription>Izberi segment za preusmeritev</FieldDescription>
|
||||
<FieldError v-if="errors.length" :errors="errors" />
|
||||
</FieldContent>
|
||||
|
||||
<Select
|
||||
:model-value="field.value"
|
||||
@update:model-value="field.onChange"
|
||||
@blur="field.onBlur"
|
||||
>
|
||||
<SelectTrigger id="segment_id" :aria-invalid="!!errors.length">
|
||||
<SelectValue placeholder="Izberi segment..."></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
<SelectItem value="auto"> Auto </SelectItem>
|
||||
<SelectItem
|
||||
v-for="segment in segments"
|
||||
:key="segment.label"
|
||||
:value="segment.value"
|
||||
>
|
||||
{{ segment.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</VeeField>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
:disabled="processing"
|
||||
variant="ghost"
|
||||
@click="
|
||||
() => {
|
||||
close();
|
||||
resetForm();
|
||||
}
|
||||
"
|
||||
>
|
||||
Prekliči
|
||||
</Button>
|
||||
<Button type="submit" form="segment-change-form" :disabled="processing">
|
||||
Potrdi
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</template>
|
||||
|
||||
<style></style>
|
||||
@@ -30,14 +30,15 @@ import AppPopover from "@/Components/app/ui/AppPopover.vue";
|
||||
import InputLabel from "@/Components/InputLabel.vue";
|
||||
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
|
||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||
import { toNumber } from "lodash";
|
||||
|
||||
const props = defineProps({
|
||||
setting: Object,
|
||||
unassignedContracts: Object,
|
||||
assignedContracts: Object,
|
||||
users: Array,
|
||||
unassignedClients: Array,
|
||||
assignedClients: Array,
|
||||
unassignedClients: [Array, Object],
|
||||
assignedClients: [Array, Object],
|
||||
filters: Object,
|
||||
});
|
||||
|
||||
@@ -54,6 +55,8 @@ const filterAssignedSelectedClient = ref(
|
||||
: []
|
||||
);
|
||||
|
||||
const unassignedContractTable = ref(null);
|
||||
|
||||
const form = useForm({
|
||||
contract_uuid: null,
|
||||
assigned_user_id: null,
|
||||
@@ -107,6 +110,14 @@ function toggleContractSelection(uuid, checked) {
|
||||
console.log(selectedContractUuids.value);
|
||||
}
|
||||
|
||||
function handleContractSelection(selected) {
|
||||
selectedContractUuids.value = selected.map((val, i) => {
|
||||
const num = toNumber(val);
|
||||
|
||||
return props.unassignedContracts.data[num].uuid;
|
||||
});
|
||||
}
|
||||
|
||||
// Format helpers (Slovenian formatting)
|
||||
|
||||
// Initialize search and filter from URL params
|
||||
@@ -296,6 +307,7 @@ function assignSelected() {
|
||||
bulkForm.contract_uuids = selectedContractUuids.value;
|
||||
bulkForm.post(route("fieldjobs.assign-bulk"), {
|
||||
onSuccess: () => {
|
||||
unassignedContractTable.value.clearSelection();
|
||||
selectedContractUuids.value = [];
|
||||
bulkForm.contract_uuids = [];
|
||||
},
|
||||
@@ -304,7 +316,11 @@ function assignSelected() {
|
||||
|
||||
function cancelAssignment(contract) {
|
||||
const payload = { contract_uuid: contract.uuid };
|
||||
form.transform(() => payload).post(route("fieldjobs.cancel"));
|
||||
form
|
||||
.transform(() => payload)
|
||||
.post(route("fieldjobs.cancel"), {
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Column definitions for DataTableNew2
|
||||
@@ -437,6 +453,7 @@ const assignedRows = computed(() =>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
ref="unassignedContractTable"
|
||||
:columns="unassignedColumns"
|
||||
:data="unassignedRows"
|
||||
:meta="{
|
||||
@@ -449,6 +466,8 @@ const assignedRows = computed(() =>
|
||||
links: unassignedContracts.links,
|
||||
}"
|
||||
row-key="uuid"
|
||||
:enable-row-selection="true"
|
||||
@selection:change="handleContractSelection"
|
||||
:page-size="props.unassignedContracts?.per_page || 10"
|
||||
:page-size-options="[10, 15, 25, 50, 100]"
|
||||
:show-toolbar="true"
|
||||
@@ -482,7 +501,10 @@ const assignedRows = computed(() =>
|
||||
<AppMultiSelect
|
||||
v-model="filterUnassignedSelectedClient"
|
||||
:items="
|
||||
(props.unassignedClients || []).map((client) => ({
|
||||
(Array.isArray(props.unassignedClients)
|
||||
? props.unassignedClients
|
||||
: props.unassignedClients?.data || []
|
||||
).map((client) => ({
|
||||
value: client.uuid,
|
||||
label: client.person.full_name,
|
||||
}))
|
||||
@@ -497,14 +519,6 @@ const assignedRows = computed(() =>
|
||||
</AppPopover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-_select="{ row }">
|
||||
<Checkbox
|
||||
@update:model-value="
|
||||
(checked) => toggleContractSelection(row.uuid, checked)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #cell-case_person="{ row }">
|
||||
<Link
|
||||
v-if="row.client_case?.uuid"
|
||||
@@ -605,7 +619,10 @@ const assignedRows = computed(() =>
|
||||
<AppMultiSelect
|
||||
v-model="filterAssignedSelectedClient"
|
||||
:items="
|
||||
(props.assignedClients || []).map((client) => ({
|
||||
(Array.isArray(props.assignedClients)
|
||||
? props.assignedClients
|
||||
: props.assignedClients?.data || []
|
||||
).map((client) => ({
|
||||
value: client.uuid,
|
||||
label: client.person.full_name,
|
||||
}))
|
||||
|
||||
@@ -75,13 +75,13 @@ const closeModal = () => {
|
||||
</p>
|
||||
|
||||
<!-- Other Browser Sessions -->
|
||||
<div v-if="sessions.length > 0" class="space-y-4">
|
||||
<div v-if="sessions && sessions.length > 0" class="space-y-4">
|
||||
<div
|
||||
v-for="(session, i) in sessions"
|
||||
:key="i"
|
||||
class="flex items-center gap-3 rounded-lg border p-3"
|
||||
>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<Monitor
|
||||
v-if="session.agent.is_desktop"
|
||||
class="h-8 w-8 text-muted-foreground"
|
||||
@@ -108,6 +108,14 @@ const closeModal = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="rounded-lg border border-dashed p-8 text-center">
|
||||
<Monitor class="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
||||
<p class="text-sm text-muted-foreground">
|
||||
No active sessions found. This feature requires session data to be configured in your Laravel application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<Button @click="confirmLogout">
|
||||
<LogOut class="h-4 w-4 mr-2" />
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup>
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import { Link, router } from "@inertiajs/vue3";
|
||||
import { Link, router, useForm, usePage } from "@inertiajs/vue3";
|
||||
import { ref, computed } from "vue";
|
||||
import axios from "axios";
|
||||
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
import ConfirmDialog from "@/Components/ConfirmDialog.vue";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Label } from "@/Components/ui/label";
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||
import { CardTitle } from "@/Components/ui/card";
|
||||
import { toNumber } from "lodash";
|
||||
|
||||
const props = defineProps({
|
||||
segment: Object,
|
||||
@@ -63,6 +65,14 @@ const exportColumns = ref(columns.map((col) => col.key));
|
||||
const exportError = ref("");
|
||||
const isExporting = ref(false);
|
||||
|
||||
const contractTable = ref(null);
|
||||
const selectedRows = ref([]);
|
||||
const showConfirmDialog = ref(false);
|
||||
const archiveForm = useForm({
|
||||
contracts: [],
|
||||
reactivate: false,
|
||||
});
|
||||
|
||||
const hasActiveFilters = computed(() => {
|
||||
return Boolean(search.value?.trim()) || Boolean(selectedClient.value);
|
||||
});
|
||||
@@ -78,6 +88,13 @@ const appliedFilterCount = computed(() => {
|
||||
return count;
|
||||
});
|
||||
|
||||
function handleSelectionChange(selectedKeys) {
|
||||
selectedRows.value = selectedKeys.map((val, i) => {
|
||||
const nu = toNumber(val);
|
||||
return props.contracts.data[nu].uuid;
|
||||
});
|
||||
}
|
||||
|
||||
const contractsCurrentPage = computed(() => props.contracts?.current_page ?? 1);
|
||||
const contractsPerPage = computed(() => props.contracts?.per_page ?? 15);
|
||||
const totalContracts = computed(
|
||||
@@ -317,43 +334,9 @@ function extractFilenameFromHeaders(headers) {
|
||||
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;
|
||||
console.log(selectedRows.value);
|
||||
if (!selectedRows.value?.length) return;
|
||||
showConfirmDialog.value = true;
|
||||
}
|
||||
|
||||
@@ -362,7 +345,7 @@ function closeConfirmDialog() {
|
||||
}
|
||||
|
||||
function submitArchive() {
|
||||
if (!selectedRows.value.length) return;
|
||||
if (!selectedRows.value?.length) return;
|
||||
|
||||
showConfirmDialog.value = false;
|
||||
|
||||
@@ -373,6 +356,9 @@ function submitArchive() {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
selectedRows.value = [];
|
||||
if (contractTable.value) {
|
||||
contractTable.value.clearSelection();
|
||||
}
|
||||
router.reload({ only: ["contracts"] });
|
||||
},
|
||||
});
|
||||
@@ -430,10 +416,13 @@ function submitArchive() {
|
||||
</div>
|
||||
</template>
|
||||
<DataTable
|
||||
ref="contractTable"
|
||||
:columns="columns"
|
||||
:data="contracts?.data || []"
|
||||
:meta="contracts || {}"
|
||||
route-name="segments.show"
|
||||
:enable-row-selection="canManageSettings"
|
||||
@selection:change="handleSelectionChange"
|
||||
:route-params="{ segment: segment?.id ?? segment }"
|
||||
:only-props="['contracts']"
|
||||
:page-size="contracts?.per_page ?? 15"
|
||||
@@ -566,6 +555,17 @@ function submitArchive() {
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template #toolbar-actions="{ table }">
|
||||
<Button
|
||||
v-if="canManageSettings && table?.getSelectedRowModel()?.rows?.length > 0"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
class="gap-2"
|
||||
@click="openArchiveModal"
|
||||
>
|
||||
Arhiviraj ({{ table.getSelectedRowModel().rows.length }})
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<template #cell-client_case="{ row }">
|
||||
<Link
|
||||
@@ -610,8 +610,10 @@ function submitArchive() {
|
||||
<ConfirmDialog
|
||||
:show="showConfirmDialog"
|
||||
title="Arhiviraj pogodbe"
|
||||
:message="`Ali ste prepričani, da želite arhivirati ${selectedRows.length} pogodb${
|
||||
selectedRows.length === 1 ? 'o' : ''
|
||||
:message="`Ali ste prepričani, da želite arhivirati ${
|
||||
selectedRows?.length || 0
|
||||
} pogodb${
|
||||
selectedRows?.length === 1 ? 'o' : ''
|
||||
}? Arhivirane pogodbe bodo odstranjene iz aktivnih segmentov.`"
|
||||
confirm-text="Arhiviraj"
|
||||
cancel-text="Prekliči"
|
||||
|
||||
Reference in New Issue
Block a user