update case index page segment index and show page
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user