Changes to UI and other stuff

This commit is contained in:
Simon Pocrnjič
2025-11-20 18:11:43 +01:00
parent b7fa2d261b
commit 3b284fa4bd
87 changed files with 7872 additions and 2330 deletions
@@ -1,11 +1,16 @@
<script setup>
import { ref, computed } from "vue";
import { router, useForm } from "@inertiajs/vue3";
import DataTable from "@/Components/DataTable/DataTable.vue";
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
import StatusBadge from "@/Components/DataTable/StatusBadge.vue";
import TableActions from "@/Components/DataTable/TableActions.vue";
import ActionMenuItem from "@/Components/DataTable/ActionMenuItem.vue";
import Dropdown from "@/Components/Dropdown.vue";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
import CaseObjectCreateDialog from "./CaseObjectCreateDialog.vue";
import CaseObjectsDialog from "./CaseObjectsDialog.vue";
import PaymentDialog from "./PaymentDialog.vue";
@@ -27,6 +32,7 @@ import {
faFolderOpen,
} from "@fortawesome/free-solid-svg-icons";
import EmptyState from "@/Components/EmptyState.vue";
import { Button } from "@/Components/ui/button";
const props = defineProps({
client: { type: Object, default: null },
@@ -40,7 +46,7 @@ const props = defineProps({
createDoc: { type: Boolean, default: () => false },
});
const emit = defineEmits(["edit", "delete", "add-activity"]);
const emit = defineEmits(["edit", "delete", "add-activity", "create", "attach-segment"]);
const formatDate = (d) => {
if (!d) return "-";
@@ -106,7 +112,8 @@ const getMetaEntries = (c) => {
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 || "Meta";
const title =
(node.title || keyName || "").toString().trim() || keyName || "Meta";
results.push({ title, value: node.value, type: node.type });
return;
}
@@ -428,36 +435,54 @@ const closePaymentsDialog = () => {
// Columns configuration
const columns = computed(() => [
{ key: "reference", label: "Ref.", sortable: false },
{ key: "reference", label: "Ref.", sortable: false, align: "center" },
{ key: "start_date", label: "Datum začetka", sortable: false },
{ key: "type", label: "Tip", sortable: false },
{ key: "segment", label: "Segment", sortable: false },
{ key: "initial_amount", label: "Predano", sortable: false, align: "right" },
{ key: "balance_amount", label: "Odprto", sortable: false, align: "right" },
{ key: "meta_info", label: "Opis", sortable: false, align: "center" },
{ key: "actions", label: "", sortable: false, hideable: false, align: "center" },
]);
const onEdit = (c) => emit("edit", c);
const onDelete = (c) => emit("delete", c);
const onAddActivity = (c) => emit("add-activity", c);
const onCreate = () => emit("create");
const onAttachSegment = () => emit("attach-segment");
const availableSegmentsCount = computed(() => {
const current = new Set((props.segments || []).map((s) => s.id));
return (props.all_segments || []).filter((s) => !current.has(s.id)).length;
});
</script>
<template>
<div>
<DataTable
:columns="columns"
:rows="contracts"
:data="contracts"
:empty-icon="faFolderOpen"
empty-text="Ni pogodb"
empty-description="Za ta primer še ni ustvarjenih pogodb. Ustvarite novo pogodbo za začetek."
:show-pagination="false"
:show-toolbar="false"
:striped="true"
:show-toolbar="true"
:hoverable="true"
>
<!-- Toolbar Actions -->
<template #toolbar-actions v-if="edit">
<Button variant="outline" @click="onCreate"> Nova </Button>
<Button
variant="outline"
@click="onAttachSegment"
:disabled="availableSegmentsCount === 0"
>
{{ availableSegmentsCount ? "Dodaj segment" : "Ni razpoložljivih segmentov" }}
</Button>
</template>
<!-- Reference -->
<template #cell-reference="{ row }">
<span class="font-medium text-gray-900">{{ row.reference }}</span>
<span class="font-medium text-gray-900 px-2">{{ row.reference }}</span>
</template>
<!-- Start Date -->
@@ -474,11 +499,11 @@ const onAddActivity = (c) => emit("add-activity", c);
<template #cell-segment="{ row }">
<div class="flex items-center gap-2" @click.stop>
<span class="text-gray-700">{{ contractActiveSegment(row)?.name || "-" }}</span>
<Dropdown align="left" v-if="edit">
<template #trigger>
<button
type="button"
class="inline-flex items-center justify-center h-7 w-7 rounded-full hover:bg-gray-100 transition-colors"
<DropdownMenu v-if="edit">
<DropdownMenuTrigger as-child>
<Button
variant="ghost"
size="icon"
:class="{
'opacity-50 cursor-not-allowed':
!segments || segments.length === 0 || !row.active,
@@ -493,64 +518,68 @@ const onAddActivity = (c) => emit("add-activity", c);
:disabled="!row.active || !segments || !segments.length"
>
<FontAwesomeIcon :icon="faPenToSquare" class="h-4 w-4 text-gray-600" />
</button>
</template>
<template #content>
<div class="py-1">
<template v-if="segments && segments.length">
<button
v-for="s in sortedSegments"
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<template v-if="segments && segments.length">
<DropdownMenuItem
v-for="s in sortedSegments"
:key="s.id"
@click="askChangeSegment(row, s.id)"
>
{{ s.name }}
</DropdownMenuItem>
</template>
<template v-else>
<template v-if="all_segments && all_segments.length">
<div class="px-3 py-2 text-xs text-gray-500">
Ni segmentov v tem primeru. Dodaj in nastavi segment:
</div>
<DropdownMenuItem
v-for="s in sortedAllSegments"
:key="s.id"
type="button"
class="w-full px-3 py-1.5 text-left text-sm hover:bg-gray-50"
@click="askChangeSegment(row, s.id)"
@click.stop
@click="askChangeSegment(row, s.id, true)"
>
{{ s.name }}
</button>
</DropdownMenuItem>
</template>
<template v-else>
<template v-if="all_segments && all_segments.length">
<div class="px-3 py-2 text-xs text-gray-500">
Ni segmentov v tem primeru. Dodaj in nastavi segment:
</div>
<button
v-for="s in sortedAllSegments"
:key="s.id"
type="button"
class="w-full px-3 py-1.5 text-left text-sm hover:bg-gray-50"
@click="askChangeSegment(row, s.id, true)"
>
{{ s.name }}
</button>
</template>
<template v-else>
<div class="px-3 py-2 text-sm text-gray-500">Ni konfiguriranih segmentov.</div>
</template>
<div class="px-3 py-2 text-sm text-gray-500">
Ni konfiguriranih segmentov.
</div>
</template>
</div>
</template>
</Dropdown>
<StatusBadge v-if="!row.active" status="Arhivirano" variant="default" size="sm" />
</template>
</DropdownMenuContent>
</DropdownMenu>
<StatusBadge
v-if="!row.active"
status="Arhivirano"
variant="default"
size="sm"
/>
</div>
</template>
<!-- Initial Amount -->
<template #cell-initial_amount="{ row }">
<div class="text-right">{{ formatCurrency(row?.account?.initial_amount ?? 0) }}</div>
<div class="text-right">
{{ formatCurrency(row?.account?.initial_amount ?? 0) }}
</div>
</template>
<!-- Balance Amount -->
<template #cell-balance_amount="{ row }">
<div class="text-right">{{ formatCurrency(row?.account?.balance_amount ?? 0) }}</div>
<div class="text-right">
{{ formatCurrency(row?.account?.balance_amount ?? 0) }}
</div>
</template>
<!-- Meta Info -->
<template #cell-meta_info="{ row }">
<div class="inline-flex items-center justify-center gap-0.5" @click.stop>
<!-- Description -->
<Dropdown align="right">
<template #trigger>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<button
type="button"
class="inline-flex items-center justify-center h-5 w-5 rounded-full transition-colors"
@@ -564,17 +593,17 @@ const onAddActivity = (c) => emit("add-activity", c);
>
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4" />
</button>
</template>
<template #content>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<div class="max-w-sm px-3 py-2 text-sm text-gray-700 whitespace-pre-wrap">
{{ row.description }}
</div>
</template>
</Dropdown>
</DropdownMenuContent>
</DropdownMenu>
<!-- Meta -->
<Dropdown align="right">
<template #trigger>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<button
type="button"
class="inline-flex items-center justify-center h-5 w-5 rounded-full transition-colors"
@@ -588,29 +617,33 @@ const onAddActivity = (c) => emit("add-activity", c);
>
<FontAwesomeIcon :icon="faTags" class="h-4 w-4" />
</button>
</template>
<template #content>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<div class="max-w-sm px-3 py-2 text-sm text-gray-700">
<template v-if="hasMeta(row)">
<div
v-for="(m, idx) in getMetaEntries(row)"
:key="idx"
class="flex items-start gap-2 py-0.5"
class="flex flex-col items-start gap-0.5 py-0.5 mb-0.5"
>
<span class="text-gray-500 whitespace-nowrap">{{ m.title }}:</span>
<span class="text-gray-800">{{ formatMetaValue(m) }}</span>
<span class="text-gray-500 text-xs whitespace-nowrap">{{
m.title
}}</span>
<span class="text-gray-800 font-medium break-all">{{
formatMetaValue(m)
}}</span>
</div>
</template>
<template v-else>
<div class="text-gray-500">Ni meta podatkov.</div>
</template>
</div>
</template>
</Dropdown>
</DropdownMenuContent>
</DropdownMenu>
<!-- Promise Date -->
<Dropdown align="right">
<template #trigger>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<button
type="button"
class="inline-flex items-center justify-center h-5 w-5 rounded-full hover:bg-gray-100 focus:outline-none transition-colors"
@@ -621,13 +654,21 @@ const onAddActivity = (c) => emit("add-activity", c);
"
:disabled="!getPromiseDate(row)"
>
<FontAwesomeIcon :icon="faClock" class="h-4 w-4" :class="promiseColorClass(row)" />
<FontAwesomeIcon
:icon="faClock"
class="h-4 w-4"
:class="promiseColorClass(row)"
/>
</button>
</template>
<template #content>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<div class="px-3 py-2 text-sm text-gray-700">
<div class="flex items-center gap-2">
<FontAwesomeIcon :icon="faClock" class="h-4 w-4" :class="promiseColorClass(row)" />
<FontAwesomeIcon
:icon="faClock"
class="h-4 w-4"
:class="promiseColorClass(row)"
/>
<span class="font-medium">Obljubljeno plačilo</span>
</div>
<div class="mt-1">
@@ -645,159 +686,174 @@ const onAddActivity = (c) => emit("add-activity", c);
</div>
<div class="mt-1 text-gray-500" v-else>Ni nastavljenega datuma.</div>
</div>
</template>
</Dropdown>
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>
<!-- Actions -->
<template #actions="{ row }">
<template #cell-actions="{ row }">
<div @click.stop>
<TableActions align="right">
<template #default="{ handleAction }">
<!-- Editing -->
<template v-if="edit">
<div class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400">
Urejanje
</div>
<template #default="{ handleAction }">
<!-- Editing -->
<template v-if="edit">
<div
class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400"
>
Urejanje
</div>
<ActionMenuItem
v-if="row.active"
:icon="faPenToSquare"
label="Uredi"
@click="onEdit(row)"
/>
</template>
<!-- Add Activity -->
<ActionMenuItem
v-if="row.active"
:icon="faPenToSquare"
label="Uredi"
@click="onEdit(row)"
:icon="faListCheck"
label="Dodaj aktivnost"
@click="onAddActivity(row)"
/>
</template>
<!-- Add Activity -->
<ActionMenuItem
v-if="row.active"
:icon="faListCheck"
label="Dodaj aktivnost"
@click="onAddActivity(row)"
/>
<div class="my-1 border-t border-gray-100" />
<div class="my-1 border-t border-gray-100" />
<!-- Documents -->
<template v-if="createDoc">
<div
class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400"
>
Dokument
</div>
<ActionMenuItem
:icon="generating[row.uuid] ? faSpinner : faFileWord"
:label="
generating[row.uuid]
? 'Generiranje...'
: templates && templates.length
? 'Generiraj dokument'
: 'Ni predlog'
"
:disabled="generating[row.uuid] || !templates || templates.length === 0"
@click="openGenerateDialog(row)"
/>
<a
v-if="generatedDocs[row.uuid]?.path"
:href="'/storage/' + generatedDocs[row.uuid].path"
target="_blank"
class="w-full flex items-center gap-2 px-3 py-2 text-left text-sm text-indigo-600 hover:bg-indigo-50"
>
<FontAwesomeIcon :icon="faFileWord" class="h-4 w-4" />
<span>Prenesi zadnji</span>
</a>
<div
v-if="generationError[row.uuid]"
class="px-3 py-2 text-xs text-rose-600 whitespace-pre-wrap"
>
{{ generationError[row.uuid] }}
</div>
<div class="my-1 border-t border-gray-100" />
</template>
<!-- Documents -->
<template v-if="createDoc">
<div class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400">
Dokument
</div>
<ActionMenuItem
:icon="generating[row.uuid] ? faSpinner : faFileWord"
:label="
generating[row.uuid]
? 'Generiranje...'
: templates && templates.length
? 'Generiraj dokument'
: 'Ni predlog'
"
:disabled="generating[row.uuid] || !templates || templates.length === 0"
@click="openGenerateDialog(row)"
/>
<a
v-if="generatedDocs[row.uuid]?.path"
:href="'/storage/' + generatedDocs[row.uuid].path"
target="_blank"
class="w-full flex items-center gap-2 px-3 py-2 text-left text-sm text-indigo-600 hover:bg-indigo-50"
>
<FontAwesomeIcon :icon="faFileWord" class="h-4 w-4" />
<span>Prenesi zadnji</span>
</a>
<!-- Objects -->
<div
v-if="generationError[row.uuid]"
class="px-3 py-2 text-xs text-rose-600 whitespace-pre-wrap"
class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400"
>
{{ generationError[row.uuid] }}
</div>
<div class="my-1 border-t border-gray-100" />
</template>
<!-- Objects -->
<div class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400">
Predmeti
</div>
<ActionMenuItem
:icon="faCircleInfo"
label="Seznam predmetov"
@click="openObjectsList(row)"
/>
<ActionMenuItem
v-if="row.active"
:icon="faPlus"
label="Dodaj predmet"
@click="openObjectDialog(row)"
/>
<div class="my-1 border-t border-gray-100" />
<!-- Payments -->
<div class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400">
Plačila
</div>
<ActionMenuItem
:icon="faCircleInfo"
label="Pokaži plačila"
@click="openPaymentsDialog(row)"
/>
<ActionMenuItem
v-if="row.active && row?.account"
:icon="faPlus"
label="Dodaj plačilo"
@click="openPaymentDialog(row)"
/>
<!-- Archive -->
<template v-if="edit">
<div class="my-1 border-t border-gray-100" />
<div class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400">
{{ row.active ? "Arhiviranje" : "Ponovna aktivacija" }}
Predmeti
</div>
<ActionMenuItem
:icon="faCircleInfo"
label="Seznam predmetov"
@click="openObjectsList(row)"
/>
<ActionMenuItem
v-if="row.active"
:icon="faBoxArchive"
:label="'Arhiviraj'"
@click="
router.post(
route('clientCase.contract.archive', {
client_case: client_case.uuid,
uuid: row.uuid,
}),
{},
{
preserveScroll: true,
only: ['contracts', 'activities', 'documents'],
}
)
"
:icon="faPlus"
label="Dodaj predmet"
@click="openObjectDialog(row)"
/>
<div class="my-1 border-t border-gray-100" />
<!-- Payments -->
<div
class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400"
>
Plačila
</div>
<ActionMenuItem
:icon="faCircleInfo"
label="Pokaži plačila"
@click="openPaymentsDialog(row)"
/>
<ActionMenuItem
v-else
:icon="faBoxArchive"
label="Ponovno aktiviraj"
@click="
router.post(
route('clientCase.contract.archive', {
client_case: client_case.uuid,
uuid: row.uuid,
}),
{ reactivate: true },
{
preserveScroll: true,
only: ['contracts', 'activities', 'documents'],
}
)
"
v-if="row.active && row?.account"
:icon="faPlus"
label="Dodaj plačilo"
@click="openPaymentDialog(row)"
/>
</template>
<!-- Delete -->
<template v-if="edit">
<div class="my-1 border-t border-gray-100" />
<ActionMenuItem :icon="faTrash" label="Izbriši" danger @click="onDelete(row)" />
<!-- Archive -->
<template v-if="edit">
<div class="my-1 border-t border-gray-100" />
<div
class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400"
>
{{ row.active ? "Arhiviranje" : "Ponovna aktivacija" }}
</div>
<ActionMenuItem
v-if="row.active"
:icon="faBoxArchive"
:label="'Arhiviraj'"
@click="
router.post(
route('clientCase.contract.archive', {
client_case: client_case.uuid,
uuid: row.uuid,
}),
{},
{
preserveScroll: true,
only: ['contracts', 'activities', 'documents'],
}
)
"
/>
<ActionMenuItem
v-else
:icon="faBoxArchive"
label="Ponovno aktiviraj"
@click="
router.post(
route('clientCase.contract.archive', {
client_case: client_case.uuid,
uuid: row.uuid,
}),
{ reactivate: true },
{
preserveScroll: true,
only: ['contracts', 'activities', 'documents'],
}
)
"
/>
</template>
<!-- Delete -->
<template v-if="edit">
<div class="my-1 border-t border-gray-100" />
<ActionMenuItem
:icon="faTrash"
label="Izbriši"
danger
@click="onDelete(row)"
/>
</template>
</template>
</template>
</TableActions>
</TableActions>
</div>
</template>
</DataTable>
@@ -806,7 +862,9 @@ const onAddActivity = (c) => emit("add-activity", c);
<ConfirmationDialog
:show="confirmChange.show"
title="Spremeni segment"
:message="`Ali želite spremeniti segment za pogodbo ${confirmChange.contract?.reference || ''}?`"
:message="`Ali želite spremeniti segment za pogodbo ${
confirmChange.contract?.reference || ''
}?`"
confirm-text="Potrdi"
cancel-text="Prekliči"
@close="closeConfirm"
@@ -855,67 +913,67 @@ const onAddActivity = (c) => emit("add-activity", c);
@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>
<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>
<!-- Custom inputs -->
<template v-if="customTokenList.length > 0">
<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">
{{ token.replace(/^custom\./, "") }}
</label>
<input
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"
/>
</div>
<!-- Custom inputs -->
<template v-if="customTokenList.length > 0">
<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">
{{ token.replace(/^custom\./, "") }}
</label>
<input
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"
/>
</div>
</div>
</template>
<!-- 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>
</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>
</div>
</template>
<div v-if="generationError[generateFor?.uuid]" class="text-sm text-red-600">
{{ generationError[generateFor?.uuid] }}
<!-- 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>
</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>
</div>
<div v-if="generationError[generateFor?.uuid]" class="text-sm text-red-600">
{{ generationError[generateFor?.uuid] }}
</div>
</div>
</CreateDialog>
</div>