update case index page segment index and show page

This commit is contained in:
Simon Pocrnjič
2025-12-14 20:57:39 +01:00
parent a6ec92ec6b
commit 80948d2944
14 changed files with 1141 additions and 626 deletions
+150 -21
View File
@@ -2,44 +2,87 @@
import AppLayout from "@/Layouts/AppLayout.vue";
import SectionTitle from "@/Components/SectionTitle.vue";
import { Link, router } from "@inertiajs/vue3";
import { ref } from "vue";
import { computed, ref } from "vue";
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { faFolderOpen } from "@fortawesome/free-solid-svg-icons";
import Pagination from "@/Components/Pagination.vue";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
import { FolderOpenIcon } from "lucide-vue-next";
import { Filter, FolderOpenIcon } from "lucide-vue-next";
import CardTitle from "@/Components/ui/card/CardTitle.vue";
import { fmtCurrency, fmtDateDMY } from "@/Utilities/functions";
import { Button } from "@/Components/ui/button";
import AppPopover from "@/Components/app/ui/AppPopover.vue";
import InputLabel from "@/Components/InputLabel.vue";
import { Input } from "@/Components/ui/input";
import DateRangePicker from "@/Components/DateRangePicker.vue";
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
const props = defineProps({
client_cases: Object,
filters: Object,
clients: {
type: Array,
default: () => [],
},
});
// Initial search for DataTable toolbar
const search = ref(props.filters?.search || "");
const dateRange = ref({
start: props.filters?.from || null,
end: props.filters?.to || null,
});
const selectedClients = ref(
Array.isArray(props.filters?.clients)
? props.filters.clients.map((value) => String(value))
: []
);
const filterPopoverOpen = ref(false);
// Format helpers
const fmtCurrency = (v) => {
const n = Number(v ?? 0);
try {
return new Intl.NumberFormat("sl-SI", { style: "currency", currency: "EUR" }).format(
n
);
} catch (e) {
return `${n.toFixed(2)}`;
const appliedFilterCount = computed(() => {
let count = 0;
if (search.value?.trim()) count += 1;
if (dateRange.value?.start || dateRange.value?.end) count += 1;
if (selectedClients.value.length) count += 1;
return count;
});
function applyFilters() {
filterPopoverOpen.value = false;
const params = {};
const searchParams = new URLSearchParams(window.location.search);
const currentPerPage = searchParams.get("perPage");
if (currentPerPage) {
params.perPage = currentPerPage;
}
if (search.value && search.value.trim() !== "") {
params.search = search.value.trim();
}
if (dateRange.value?.start) {
params.from = dateRange.value.start;
}
if (dateRange.value?.end) {
params.to = dateRange.value.end;
}
if (selectedClients.value.length > 0) {
params.clients = selectedClients.value.join(",");
}
};
const fmtDateDMY = (v) => {
if (!v) return "-";
const d = new Date(v);
if (isNaN(d)) return "-";
const dd = String(d.getDate()).padStart(2, "0");
const mm = String(d.getMonth() + 1).padStart(2, "0");
const yyyy = d.getFullYear();
return `${dd}.${mm}.${yyyy}`;
};
router.get(route("clientCase"), params, {
preserveState: true,
replace: true,
preserveScroll: true,
});
}
function clearFilters() {
dateRange.value = { start: null, end: null };
selectedClients.value = [];
search.value = "";
applyFilters();
}
</script>
<template>
<AppLayout title="Client cases">
@@ -80,13 +123,99 @@ const fmtDateDMY = (v) => {
},
]"
:data="client_cases.data || []"
:meta="client_cases"
route-name="clientCase"
page-param-name="clientCasesPage"
per-page-param-name="perPage"
:page-size="client_cases.per_page"
:page-size-options="[10, 15, 25, 50, 100]"
:show-pagination="false"
:show-toolbar="true"
:hoverable="true"
row-key="uuid"
empty-text="Ni najdenih primerov."
>
<template #toolbar-filters>
<AppPopover
v-model:open="filterPopoverOpen"
align="start"
content-class="w-[400px]"
>
<template #trigger>
<Button variant="outline" size="sm" class="gap-2">
<Filter class="h-4 w-4" />
Filtri
<span
v-if="appliedFilterCount > 0"
class="ml-1 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground"
>
{{ appliedFilterCount }}
</span>
</Button>
</template>
<div class="space-y-4">
<div class="space-y-2">
<h4 class="font-medium text-sm">Filtri primerov</h4>
<p class="text-sm text-muted-foreground">
Izberite parametre za zožanje prikaza primerov.
</p>
</div>
<div class="space-y-3">
<div class="space-y-1.5">
<InputLabel>Iskanje</InputLabel>
<Input
v-model="search"
type="text"
placeholder="Išči po imenu, davčni številki ..."
/>
</div>
<div class="space-y-1.5">
<InputLabel>Datumski obseg (ustvarjeno)</InputLabel>
<DateRangePicker
v-model="dateRange"
format="dd.MM.yyyy"
placeholder="Izberi datume"
/>
</div>
<div class="space-y-1.5">
<InputLabel>Stranke</InputLabel>
<AppMultiSelect
v-model="selectedClients"
:items="
(props.clients || []).map((client) => ({
value: String(client.id),
label: client.name,
}))
"
placeholder="Vse stranke"
search-placeholder="Išči stranko..."
empty-text="Ni strank"
chip-variant="secondary"
/>
</div>
<div class="flex justify-end gap-2 pt-2 border-t">
<Button
type="button"
variant="outline"
size="sm"
:disabled="
!dateRange?.start &&
!dateRange?.end &&
selectedClients.length === 0 &&
search === ''
"
@click="clearFilters"
>
Počisti
</Button>
<Button type="button" size="sm" @click="applyFilters">
Uporabi
</Button>
</div>
</div>
</div>
</AppPopover>
</template>
<template #cell-nu="{ row }">
{{ row.person?.nu || "-" }}
</template>
@@ -364,7 +364,7 @@ const copyToClipboard = async (text) => {
</span>
</Button>
</PopoverTrigger>
<PopoverContent class="w-[400px]" align="start">
<PopoverContent class="w-100" align="start">
<div class="space-y-4">
<div class="space-y-2">
<h4 class="font-medium text-sm">Filtri aktivnosti</h4>
+108 -60
View File
@@ -2,6 +2,17 @@
import AppLayout from "@/Layouts/AppLayout.vue";
import { Link } from "@inertiajs/vue3";
import { computed, ref } from "vue";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/Components/ui/card";
import { Input } from "@/Components/ui/input";
import { Badge } from "@/Components/ui/badge";
import { Search } from "lucide-vue-next";
import { fmtCurrency } from "@/Utilities/functions";
const props = defineProps({
segments: Array,
@@ -26,74 +37,111 @@ const filtered = computed(() => {
});
});
function formatCurrencyEUR(value) {
if (value === null || value === undefined) {
return "-";
}
const n = Number(value);
if (isNaN(n)) {
return String(value);
}
return (
n.toLocaleString("sl-SI", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) +
" €"
);
}
const totalBalance = computed(() =>
filtered.value.reduce((sum, segment) => sum + Number(segment.total_balance ?? 0), 0)
);
</script>
<template>
<AppLayout title="Segmenti">
<template #header></template>
<div class="pt-12">
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-4 mb-6">
<label class="block text-sm font-medium text-gray-700 mb-1"
>Iskanje (segment ali opis)</label
>
<input
v-model="search"
type="text"
class="border rounded px-3 py-2 w-full max-w-xl"
placeholder="Išči po nazivu segmenta ali opisu"
/>
</div>
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
<h2 class="text-xl font-semibold mb-4">Aktivni segmenti</h2>
<div
v-if="filtered.length"
class="grid gap-4 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
>
<div
v-for="s in filtered"
:key="s.id"
class="border rounded-lg p-4 shadow-sm hover:shadow transition bg-white"
>
<div class="flex items-start justify-between mb-2">
<h3 class="text-base font-semibold text-gray-900">
<Link :href="route('segments.show', s.id)" class="hover:underline">{{
s.name
}}</Link>
</h3>
<span
class="inline-flex items-center text-xs px-2 py-0.5 rounded-full bg-indigo-50 text-indigo-700 border border-indigo-100"
>
{{ s.contracts_count ?? 0 }} pogodb
</span>
<div class="py-8">
<div class="max-w-6xl mx-auto space-y-6 px-4 sm:px-6 lg:px-8">
<Card>
<CardHeader class="pb-3">
<CardTitle class="text-2xl font-semibold tracking-tight">Segmenti</CardTitle>
<CardDescription>
Pregled vseh aktivnih segmentov in njihovih ključnih kazalnikov.
</CardDescription>
</CardHeader>
<CardContent>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div class="rounded-lg border bg-muted/40 p-4">
<p class="text-xs uppercase text-muted-foreground">Število segmentov</p>
<p class="text-2xl font-semibold">{{ filtered.length }}</p>
</div>
<p class="text-sm text-gray-600 min-h-[1.25rem]">
{{ s.description || "" }}
</p>
<div class="mt-4 flex items-center justify-between">
<div class="text-sm text-gray-500">Vsota stanj</div>
<div class="text-sm font-medium text-gray-900">
{{ formatCurrencyEUR(s.total_balance) }}
</div>
<div class="rounded-lg border bg-muted/40 p-4">
<p class="text-xs uppercase text-muted-foreground">Skupaj pogodb</p>
<p class="text-2xl font-semibold">
{{
filtered.reduce((sum, s) => sum + Number(s.contracts_count ?? 0), 0)
}}
</p>
</div>
<div class="rounded-lg border bg-muted/40 p-4">
<p class="text-xs uppercase text-muted-foreground">Skupaj stanj</p>
<p class="text-2xl font-semibold">{{ fmtCurrency(totalBalance) }}</p>
</div>
</div>
</div>
<div v-else class="text-gray-500">Ni aktivnih segmentov.</div>
</div>
<div class="mt-6">
<label class="text-sm font-medium text-muted-foreground"
>Iskanje (segment ali opis)</label
>
<div class="relative mt-2 max-w-md">
<Search
class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground"
/>
<Input
v-model="search"
type="text"
placeholder="Išči po nazivu segmenta ali opisu"
class="pl-9"
/>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader class="pb-3">
<CardTitle>Aktivni segmenti</CardTitle>
<CardDescription
>Rezultati so razporejeni v kartice. Klik na segment vodi na
podrobnosti.</CardDescription
>
</CardHeader>
<CardContent>
<div
v-if="filtered.length"
class="grid gap-4 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
>
<Card
v-for="s in filtered"
:key="s.id"
class="border border-border/70 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md"
>
<CardHeader class="space-y-1 pb-2">
<div class="flex items-start justify-between gap-2">
<CardTitle class="text-lg font-semibold">
<Link :href="route('segments.show', s.id)" class="hover:underline">
{{ s.name }}
</Link>
</CardTitle>
<Badge variant="secondary" class="text-xs font-medium">
{{ s.contracts_count ?? 0 }} pogodb
</Badge>
</div>
<CardDescription class="min-h-[1.5rem]">{{
s.description || ""
}}</CardDescription>
</CardHeader>
<CardContent>
<dl class="space-y-3 text-sm">
<div class="flex items-center justify-between">
<dt class="text-muted-foreground">Vsota stanj</dt>
<dd class="font-medium">{{ fmtCurrency(s.total_balance) }}</dd>
</div>
<div class="flex items-center justify-between">
<dt class="text-muted-foreground">Aktivne pogodbe</dt>
<dd class="font-medium">{{ s.contracts_count ?? 0 }}</dd>
</div>
</dl>
</CardContent>
</Card>
</div>
<div v-else class="text-sm text-muted-foreground">Ni aktivnih segmentov.</div>
</CardContent>
</Card>
</div>
</div>
</AppLayout>
+409 -163
View File
@@ -1,10 +1,35 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import { Link, router } from "@inertiajs/vue3";
import { ref, computed, watch } from "vue";
import { ref, computed } from "vue";
import axios from "axios";
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
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 { Checkbox } from "@/Components/ui/checkbox";
import { Switch } from "@/Components/ui/switch";
import { Popover, PopoverContent, PopoverTrigger } from "@/Components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/Components/ui/command";
import {
Filter,
ChevronsUpDown,
Check,
FileDown,
LayoutIcon,
UsersIcon,
} from "lucide-vue-next";
import { cn } from "@/lib/utils";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
import { CardTitle } from "@/Components/ui/card";
const props = defineProps({
segment: Object,
@@ -17,16 +42,18 @@ const urlParams = new URLSearchParams(window.location.search);
const search = ref(urlParams.get("search") || "");
const initialClient = urlParams.get("client") || urlParams.get("client_id") || "";
const selectedClient = ref(initialClient);
const filterPopoverOpen = ref(false);
const clientComboboxOpen = ref(false);
// Column definitions for the server-driven table
const columns = [
{ key: "reference", label: "Pogodba", sortable: true },
{ key: "client_case", label: "Primer" },
{ key: "client", label: "Stranka" },
{ key: "type", label: "Vrsta" },
{ key: "start_date", label: "Začetek", sortable: true },
{ key: "end_date", label: "Konec", sortable: true },
{ key: "account", label: "Stanje", align: "right" },
{ key: "reference", label: "Pogodba", sortable: false },
{ key: "client_case", label: "Primer", sortable: false },
{ key: "client", label: "Stranka", sortable: false },
{ key: "type", label: "Vrsta", sortable: false },
{ key: "start_date", label: "Začetek", sortable: false },
{ key: "end_date", label: "Konec", sortable: false },
{ key: "account", label: "Stanje", align: "right", sortable: false },
];
const exportDialogOpen = ref(false);
@@ -35,6 +62,21 @@ const exportColumns = ref(columns.map((col) => col.key));
const exportError = ref("");
const isExporting = ref(false);
const hasActiveFilters = computed(() => {
return Boolean(search.value?.trim()) || Boolean(selectedClient.value);
});
const appliedFilterCount = computed(() => {
let count = 0;
if (search.value?.trim()) {
count += 1;
}
if (selectedClient.value) {
count += 1;
}
return count;
});
const contractsCurrentPage = computed(() => props.contracts?.current_page ?? 1);
const contractsPerPage = computed(() => props.contracts?.per_page ?? 15);
const totalContracts = computed(
@@ -43,12 +85,28 @@ const totalContracts = computed(
const currentPageCount = computed(() => props.contracts?.data?.length ?? 0);
const allColumnsSelected = computed(() => exportColumns.value.length === columns.length);
const exportDisabled = computed(() => exportColumns.value.length === 0 || isExporting.value);
const exportDisabled = computed(
() => exportColumns.value.length === 0 || isExporting.value
);
function toggleAllColumns(checked) {
exportColumns.value = checked ? columns.map((col) => col.key) : [];
}
function handleColumnToggle(key, checked) {
if (checked) {
if (!exportColumns.value.includes(key)) {
exportColumns.value = [...exportColumns.value, key];
}
} else {
exportColumns.value = exportColumns.value.filter((col) => col !== key);
}
}
function setExportScopeFromSwitch(checked) {
exportScope.value = checked ? "all" : "current";
}
function openExportDialog() {
exportDialogOpen.value = true;
exportError.value = "";
@@ -126,18 +184,68 @@ const selectedClientName = computed(() => {
return match?.label || "";
});
// React to client selection changes by visiting the same route with updated query
watch(selectedClient, (val) => {
const query = { search: search.value };
if (val) {
query.client = val;
function isClientFilterEmpty() {
return !selectedClient.value;
}
function selectClient(value) {
selectedClient.value = value ?? "";
}
function closeClientCombobox() {
clientComboboxOpen.value = false;
}
function isClientValueSelected(value) {
return selectedClient.value === value;
}
function handleClientSelect(value) {
selectClient(value);
closeClientCombobox();
}
function buildQueryParams() {
const params = {};
const searchParams = new URLSearchParams(window.location.search);
for (const [key, value] of searchParams.entries()) {
params[key] = value;
}
const trimmedSearch = search.value.trim();
if (trimmedSearch) {
params.search = trimmedSearch;
} else {
delete params.search;
}
if (selectedClient.value) {
params.client = selectedClient.value;
} else {
delete params.client;
}
params.page = 1;
return params;
}
function applyFilters() {
const query = buildQueryParams();
router.get(
route("segments.show", { segment: props.segment?.id ?? props.segment }),
query,
{ preserveState: true, preserveScroll: true, only: ["contracts"], replace: true }
);
});
filterPopoverOpen.value = false;
}
function clearFilters() {
search.value = "";
selectedClient.value = "";
applyFilters();
}
function formatDate(value) {
if (!value) {
@@ -207,193 +315,331 @@ function extractFilenameFromHeaders(headers) {
<template>
<AppLayout :title="`Segment: ${segment?.name || ''}`">
<template #header> </template>
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-4">
<h2 class="text-lg">{{ segment.name }}</h2>
<div class="text-sm text-gray-600 mb-4">{{ segment?.description }}</div>
<!-- Filters -->
<div class="mb-4 flex flex-col sm:flex-row sm:items-end gap-3">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 mb-1">Stranka</label>
<div class="flex items-center gap-2">
<select
v-model="selectedClient"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm"
>
<option value="">Vse stranke</option>
<option
v-for="opt in clientOptions"
:key="opt.value || opt.label"
:value="opt.value"
>
{{ opt.label }}
</option>
</select>
<button
type="button"
class="text-sm text-gray-600 hover:text-gray-900"
@click="selectedClient = ''"
v-if="selectedClient"
>
Počisti
</button>
<div class="py-6">
<div class="max-w-7xl mx-auto space-y-6 px-4 sm:px-6 lg:px-8">
<AppCard
title=""
padding="none"
class="p-5! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
body-class=""
>
<div>
<p class="text-sm uppercase text-muted-foreground">Segment</p>
<h2 class="text-2xl font-semibold text-foreground">{{ segment.name }}</h2>
</div>
<p class="text-sm text-muted-foreground">
{{ segment?.description || "Ni opisa za izbran segment." }}
</p>
<div class="mt-4 grid gap-4 sm:grid-cols-3">
<div class="rounded-lg border bg-muted/40 p-4">
<p class="text-xs uppercase text-muted-foreground">Pogodbe (stran)</p>
<p class="text-2xl font-semibold">{{ currentPageCount }}</p>
</div>
<div class="rounded-lg border bg-muted/40 p-4">
<p class="text-xs uppercase text-muted-foreground">Skupaj pogodb</p>
<p class="text-2xl font-semibold">{{ totalContracts }}</p>
</div>
<div class="rounded-lg border bg-muted/40 p-4">
<p class="text-xs uppercase text-muted-foreground">Izbrana stranka</p>
<p class="text-base font-medium">
{{ selectedClientName || "Vse" }}
</p>
</div>
</div>
</div>
</AppCard>
<DataTableServer
:columns="columns"
:rows="contracts?.data || []"
:meta="contracts || {}"
v-model:search="search"
route-name="segments.show"
:route-params="{ segment: segment?.id ?? segment }"
:query="{ client: selectedClient || undefined }"
:only-props="['contracts']"
:page-size-options="[10, 25, 50]"
empty-text="Ni pogodb v tem segmentu."
row-key="uuid"
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
body-class=""
>
<template #toolbar-extra>
<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"
@click="openExportDialog"
>
Izvozi v Excel
</button>
</template>
<!-- Primer (client_case) cell with link when available -->
<template #cell-client_case="{ row }">
<Link
v-if="row.client_case?.uuid"
:href="
route('clientCase.show', {
client_case: row.client_case.uuid,
segment: segment?.id ?? segment,
})
"
class="text-indigo-600 hover:underline"
>
{{ row.client_case?.person?.full_name || "Primer stranke" }}
</Link>
<span v-else>{{ row.client_case?.person?.full_name || "-" }}</span>
</template>
<!-- Stranka (client) name -->
<template #cell-client="{ row }">
{{ row.client?.person?.full_name || "-" }}
</template>
<!-- Vrsta (type) -->
<template #cell-type="{ row }">
{{ row.type?.name || "-" }}
</template>
<!-- Dates formatted -->
<template #cell-start_date="{ row }">
{{ formatDate(row.start_date) }}
</template>
<template #cell-end_date="{ row }">
{{ formatDate(row.end_date) }}
</template>
<!-- Account balance formatted -->
<template #cell-account="{ row }">
<div class="text-right">
{{ formatCurrency(row.account?.balance_amount) }}
<template #header>
<div class="flex items-center gap-2">
<UsersIcon size="18" />
<CardTitle class="uppercase">Pogodbe</CardTitle>
</div>
</template>
</DataTableServer>
<DataTable
:columns="columns"
:data="contracts?.data || []"
:meta="contracts || {}"
route-name="segments.show"
:route-params="{ segment: segment?.id ?? segment }"
:only-props="['contracts']"
:page-size="contracts?.per_page ?? 15"
:page-size-options="[10, 15, 25, 50]"
row-key="uuid"
empty-text="Ni pogodb v tem segmentu."
per-page-param-name="per_page"
>
<template #toolbar-filters>
<div class="flex flex-wrap items-center gap-2">
<Popover v-model:open="filterPopoverOpen">
<PopoverTrigger as-child>
<Button variant="outline" size="sm" class="gap-2">
<Filter class="h-4 w-4" />
Filtri
<span
v-if="hasActiveFilters"
class="ml-1 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground"
>
{{ appliedFilterCount }}
</span>
</Button>
</PopoverTrigger>
<PopoverContent class="w-105" align="start">
<div class="space-y-4">
<div class="space-y-1">
<h4 class="text-sm font-medium">Filtri pogodb</h4>
<p class="text-sm text-muted-foreground">
Zoži prikaz pogodb po iskalnem nizu in stranki.
</p>
</div>
<div class="space-y-3">
<div class="space-y-1.5">
<label class="text-sm font-medium">Iskanje</label>
<Input
v-model="search"
type="text"
placeholder="Išči po referenci, vrsti ..."
/>
</div>
<div class="space-y-1.5">
<label class="text-sm font-medium">Stranka</label>
<Popover v-model:open="clientComboboxOpen">
<PopoverTrigger as-child>
<Button
variant="outline"
role="combobox"
:aria-expanded="clientComboboxOpen"
class="w-full justify-between"
>
{{ selectedClientName || "Vse stranke" }}
<ChevronsUpDown
class="ml-2 h-4 w-4 shrink-0 opacity-50"
/>
</Button>
</PopoverTrigger>
<PopoverContent class="w-full p-0" align="start">
<Command>
<CommandInput placeholder="Išči stranko..." />
<CommandList>
<CommandEmpty>Ni zadetkov.</CommandEmpty>
<CommandGroup>
<CommandItem
value="vse"
@select="handleClientSelect('')"
>
<Check
:class="
cn(
'mr-2 h-4 w-4',
isClientFilterEmpty
? 'opacity-100'
: 'opacity-0'
)
"
/>
Vse stranke
</CommandItem>
<CommandItem
v-for="client in clientOptions"
:key="client.value || client.label"
:value="client.label"
@select="handleClientSelect(client.value)"
>
<Check
:class="
cn(
'mr-2 h-4 w-4',
isClientValueSelected(client.value)
? 'opacity-100'
: 'opacity-0'
)
"
/>
{{ client.label }}
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
<div class="flex items-center justify-between border-t pt-2">
<Button
v-if="hasActiveFilters"
variant="ghost"
size="sm"
class="gap-2"
@click="clearFilters"
>
Počisti
</Button>
<div v-else></div>
<Button size="sm" class="gap-2" @click="applyFilters">
Uporabi
</Button>
</div>
</div>
</PopoverContent>
</Popover>
<Button
variant="outline"
size="sm"
class="gap-2"
@click="openExportDialog"
>
<FileDown class="h-4 w-4" />
Izvozi v Excel
</Button>
</div>
</template>
<template #cell-client_case="{ row }">
<Link
v-if="row.client_case?.uuid"
:href="
route('clientCase.show', {
client_case: row.client_case.uuid,
segment: segment?.id ?? segment,
})
"
class="text-indigo-600 hover:underline"
>
{{ row.client_case?.person?.full_name || "Primer stranke" }}
</Link>
<span v-else>{{ row.client_case?.person?.full_name || "-" }}</span>
</template>
<template #cell-client="{ row }">
{{ row.client?.person?.full_name || "-" }}
</template>
<template #cell-type="{ row }">
{{ row.type?.name || "-" }}
</template>
<template #cell-start_date="{ row }">
{{ formatDate(row.start_date) }}
</template>
<template #cell-end_date="{ row }">
{{ formatDate(row.end_date) }}
</template>
<template #cell-account="{ row }">
<div class="text-right">
{{ formatCurrency(row.account?.balance_amount) }}
</div>
</template>
</DataTable>
</AppCard>
</div>
</div>
<DialogModal :show="exportDialogOpen" max-width="3xl" @close="closeExportDialog">
<template #title>
<div>
<h3 class="text-lg font-semibold">Izvoz v Excel</h3>
<p class="text-sm text-gray-500">Izberi stolpce in obseg podatkov za izvoz.</p>
<div class="space-y-1">
<h3 class="text-lg font-semibold leading-6 text-foreground">Izvoz v Excel</h3>
<p class="text-sm text-muted-foreground">
Izberi stolpce in obseg podatkov za izvoz.
</p>
</div>
</template>
<template #content>
<form id="segment-export-form" class="space-y-6" @submit.prevent="submitExport">
<div>
<span class="text-sm font-semibold text-gray-700">Obseg podatkov</span>
<div class="mt-2 space-y-2">
<label class="flex items-center gap-2 text-sm text-gray-700">
<input
type="radio"
name="scope"
value="current"
class="text-indigo-600"
v-model="exportScope"
<form id="segment-export-form" class="space-y-5" @submit.prevent="submitExport">
<div class="space-y-3 rounded-lg border bg-muted/40 p-4">
<div class="flex items-start justify-between gap-3">
<div class="space-y-1">
<p class="text-sm font-medium text-foreground">Obseg podatkov</p>
<p class="text-sm text-muted-foreground">
Preklopi, ali izvoziš samo trenutni pogled ali celoten segment.
</p>
</div>
<div
class="flex items-center gap-3 rounded-md bg-background px-3 py-2 shadow-sm"
>
<span class="text-xs font-medium text-muted-foreground">Stran</span>
<Switch
:model-value="exportScope === 'all'"
@update:modelValue="setExportScopeFromSwitch"
aria-label="Preklopi obseg izvoza"
/>
Trenutna stran ({{ currentPageCount }} zapisov)
</label>
<label class="flex items-center gap-2 text-sm text-gray-700">
<input
type="radio"
name="scope"
value="all"
class="text-indigo-600"
v-model="exportScope"
/>
Celoten segment ({{ totalContracts }} zapisov)
</label>
<span class="text-xs font-medium text-muted-foreground">Vse</span>
</div>
</div>
<div class="grid gap-2 sm:grid-cols-2">
<div class="rounded-lg border bg-background p-3 shadow-sm">
<p class="text-sm font-semibold text-foreground">Trenutna stran</p>
<p class="text-xs text-muted-foreground">
{{ currentPageCount }} zapisov
</p>
</div>
<div class="rounded-lg border bg-background p-3 shadow-sm">
<p class="text-sm font-semibold text-foreground">Celoten segment</p>
<p class="text-xs text-muted-foreground">{{ totalContracts }} zapisov</p>
</div>
</div>
</div>
<div>
<div class="flex items-center justify-between">
<span class="text-sm font-semibold text-gray-700">Stolpci</span>
<label class="flex items-center gap-2 text-xs text-gray-600">
<input
type="checkbox"
:checked="allColumnsSelected"
@change="toggleAllColumns($event.target.checked)"
<div class="space-y-4 rounded-lg border bg-muted/40 p-4">
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="space-y-1">
<p class="text-sm font-medium text-foreground">Stolpci</p>
<p class="text-sm text-muted-foreground">
Izberi, katere stolpce želiš vključiti v izvoz.
</p>
</div>
<div class="flex items-center gap-2">
<Checkbox
id="export-columns-all"
:model-value="allColumnsSelected"
@update:modelValue="toggleAllColumns"
aria-label="Označi vse stolpce"
/>
Označi vse
</label>
<Label for="export-columns-all" class="text-sm text-muted-foreground">
Označi vse
</Label>
</div>
</div>
<div class="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
<div class="grid gap-2 sm:grid-cols-2">
<label
v-for="col in columns"
:key="col.key"
class="flex items-center gap-2 rounded border border-gray-200 px-3 py-2 text-sm"
class="flex items-start gap-3 rounded-lg border bg-background px-3 py-3 text-sm shadow-sm transition hover:border-primary/40"
:for="`export-col-${col.key}`"
>
<input
type="checkbox"
name="columns[]"
<Checkbox
:id="`export-col-${col.key}`"
:model-value="exportColumns.includes(col.key)"
:value="col.key"
v-model="exportColumns"
class="text-indigo-600"
@update:modelValue="(checked) => handleColumnToggle(col.key, checked)"
class="mt-0.5"
/>
{{ col.label }}
<div class="space-y-0.5">
<p class="font-medium text-foreground">{{ col.label }}</p>
<p class="text-xs text-muted-foreground">Vključi stolpec v datoteko.</p>
</div>
</label>
</div>
<p v-if="exportError" class="mt-2 text-sm text-red-600">{{ exportError }}</p>
<p v-if="exportError" class="text-sm text-destructive">{{ exportError }}</p>
</div>
</form>
</template>
<template #footer>
<div class="flex flex-row gap-2">
<button
type="button"
class="text-sm text-gray-600 hover:text-gray-900"
@click="closeExportDialog"
>
<Button type="button" variant="ghost" @click="closeExportDialog">
Prekliči
</button>
<button
</Button>
<Button
type="submit"
form="segment-export-form"
class="inline-flex items-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-60"
:disabled="exportDisabled"
class="gap-2"
>
<span v-if="!isExporting">Prenesi Excel</span>
<span v-else>Pripravljam ...</span>
</button>
</Button>
</div>
</template>
</DialogModal>