Dashboard final version, TODO: update main sidebar menu
This commit is contained in:
@@ -1,21 +1,30 @@
|
||||
<script setup>
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import { ref } from "vue";
|
||||
import { Link, router } from "@inertiajs/vue3";
|
||||
import DataTable from "@/Components/DataTable/DataTable.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { Link, router, usePage } from "@inertiajs/vue3";
|
||||
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
} from "@/Components/ui/select"; // kept in case elsewhere but segment filter replaced
|
||||
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
|
||||
import PersonInfoGrid from "@/Components/PersonInfo/PersonInfoGrid.vue";
|
||||
import SectionTitle from "@/Components/SectionTitle.vue";
|
||||
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 } 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";
|
||||
|
||||
const props = defineProps({
|
||||
client: Object,
|
||||
@@ -25,14 +34,29 @@ const props = defineProps({
|
||||
types: Object,
|
||||
});
|
||||
|
||||
const page = usePage();
|
||||
// Expose as a callable computed: use in templates as hasPerm('permission-slug')
|
||||
const hasPerm = computed(() => (permission) =>
|
||||
hasPermission(page.props.auth?.user, permission)
|
||||
);
|
||||
|
||||
const dateRange = ref({
|
||||
start: props.filters?.from || null,
|
||||
end: props.filters?.to || null,
|
||||
});
|
||||
const search = ref(props.filters?.search || "");
|
||||
const selectedSegment = ref(props.filters?.segment || null);
|
||||
// Multi-segment selection (backwards compatible if legacy single 'segment' present)
|
||||
const selectedSegments = ref(
|
||||
Array.isArray(props.filters?.segments)
|
||||
? props.filters.segments.map((s) => String(s))
|
||||
: props.filters?.segment
|
||||
? [String(props.filters.segment)]
|
||||
: []
|
||||
);
|
||||
const filterPopoverOpen = ref(false);
|
||||
|
||||
function applyDateFilter() {
|
||||
filterPopoverOpen.value = false;
|
||||
const params = Object.fromEntries(
|
||||
new URLSearchParams(window.location.search).entries()
|
||||
);
|
||||
@@ -51,32 +75,27 @@ function applyDateFilter() {
|
||||
} else {
|
||||
delete params.search;
|
||||
}
|
||||
if (selectedSegment.value) {
|
||||
params.segment = String(selectedSegment.value);
|
||||
if (selectedSegments.value.length > 0) {
|
||||
// join as comma list for backend; adjust server parsing accordingly
|
||||
params.segments = selectedSegments.value.join(",");
|
||||
} else {
|
||||
delete params.segments;
|
||||
}
|
||||
delete params.page;
|
||||
// remove legacy single segment param if present
|
||||
delete params.segment;
|
||||
router.get(route("client.contracts", { uuid: props.client.uuid }), params, {
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
preserveScroll: true,
|
||||
only: ['contracts'],
|
||||
only: ["contracts"],
|
||||
});
|
||||
}
|
||||
|
||||
function clearDateFilter() {
|
||||
dateRange.value = { start: null, end: null };
|
||||
selectedSegment.value = null;
|
||||
applyDateFilter();
|
||||
}
|
||||
|
||||
function handleDateRangeUpdate() {
|
||||
applyDateFilter();
|
||||
}
|
||||
|
||||
function handleSegmentChange(value) {
|
||||
selectedSegment.value = value;
|
||||
selectedSegments.value = [];
|
||||
search.value = "";
|
||||
applyDateFilter();
|
||||
}
|
||||
|
||||
@@ -111,132 +130,184 @@ function formatDate(value) {
|
||||
<AppLayout title="Pogodbe">
|
||||
<template #header></template>
|
||||
<!-- Header card (matches Client/Show header style) -->
|
||||
<div class="pt-12">
|
||||
<div class="pt-6">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div
|
||||
class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4 border-blue-400"
|
||||
>
|
||||
<div class="mx-auto max-w-4x1 p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<SectionTitle>
|
||||
<template #title>
|
||||
{{ client.person.full_name }}
|
||||
</template>
|
||||
</SectionTitle>
|
||||
</div>
|
||||
<Card class="border-l-4 border-blue-400">
|
||||
<div class="p-3 flex justify-between items-center">
|
||||
<SectionTitle>
|
||||
<template #title>
|
||||
{{ client.person.full_name }}
|
||||
</template>
|
||||
</SectionTitle>
|
||||
<Badge class="bg-blue-500 text-white"> Naročnik </Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client details card (separate container) -->
|
||||
<div class="pt-1">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div
|
||||
class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4 border-blue-400"
|
||||
>
|
||||
<div class="mx-auto max-w-4x1 px-2">
|
||||
<PersonInfoGrid :types="types" :person="client.person" />
|
||||
<Card>
|
||||
<div class="mx-auto max-w-4x1 p-3">
|
||||
<PersonInfoGrid
|
||||
:types="types"
|
||||
:person="client.person"
|
||||
:edit="hasPerm('client-edit')"
|
||||
></PersonInfoGrid>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contracts list card -->
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="px-3 bg-white overflow-hidden shadow-xl sm:rounded-lg">
|
||||
<div class="mx-auto max-w-4x1 py-3">
|
||||
<div class="mb-4">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
as-child
|
||||
:variant="route().current('client.show') ? 'default' : 'ghost'"
|
||||
>
|
||||
<Link :href="route('client.show', { uuid: client.uuid })">
|
||||
Primeri
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
as-child
|
||||
:variant="route().current('client.contracts') ? 'default' : 'ghost'"
|
||||
>
|
||||
<Link :href="route('client.contracts', { uuid: client.uuid })">
|
||||
Pogodbe
|
||||
</Link>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
|
||||
<div class="mx-auto max-w-4x1">
|
||||
<div class="px-3 py-4 flex flex-row items-center gap-3">
|
||||
<Link
|
||||
:class="
|
||||
cn(
|
||||
'border border-gray-200 py-2 px-3 rounded-md hover:bg-accent hover:text-accent-foreground ',
|
||||
route().current('client.show')
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: ''
|
||||
)
|
||||
"
|
||||
:href="route('client.show', { uuid: client.uuid })"
|
||||
>
|
||||
Primeri
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
:class="
|
||||
cn(
|
||||
'border border-gray-200 py-2 px-3 rounded-md hover:bg-accent hover:text-accent-foreground ',
|
||||
route().current('client.contracts')
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: ''
|
||||
)
|
||||
"
|
||||
:href="route('client.contracts', { uuid: client.uuid })"
|
||||
>
|
||||
Pogodbe
|
||||
</Link>
|
||||
</div>
|
||||
<DataTable
|
||||
:show-search="true"
|
||||
:show-page-size="true"
|
||||
:show-filters="true"
|
||||
:has-active-filters="!!(dateRange?.start || dateRange?.end || selectedSegment)"
|
||||
:columns="[
|
||||
{ key: 'select', label: '', sortable: false, width: '50px' },
|
||||
{ key: 'reference', label: 'Referenca', sortable: false },
|
||||
{ key: 'customer', label: 'Stranka', sortable: false },
|
||||
{ key: 'start', label: 'Začetek', sortable: false },
|
||||
{ key: 'segment', label: 'Segment', sortable: false },
|
||||
{ key: 'balance', label: 'Stanje', sortable: false, align: 'right' },
|
||||
]"
|
||||
:rows="contracts.data || []"
|
||||
:meta="{ current_page: contracts.current_page, per_page: contracts.per_page, total: contracts.total, last_page: contracts.last_page, from: contracts.from, to: contracts.to, links: contracts.links }"
|
||||
:data="contracts.data || []"
|
||||
:meta="{
|
||||
current_page: contracts.current_page,
|
||||
per_page: contracts.per_page,
|
||||
total: contracts.total,
|
||||
last_page: contracts.last_page,
|
||||
from: contracts.from,
|
||||
to: contracts.to,
|
||||
links: contracts.links,
|
||||
}"
|
||||
route-name="client.contracts"
|
||||
:route-params="{ uuid: client.uuid }"
|
||||
:search="search"
|
||||
row-key="uuid"
|
||||
:only-props="['contracts']"
|
||||
:page-size-options="[10, 15, 25, 50, 100]"
|
||||
page-param-name="contracts_page"
|
||||
per-page-param-name="contracts_per_page"
|
||||
:show-toolbar="true"
|
||||
>
|
||||
<template #toolbar-filters>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium">Datumska območja</label>
|
||||
<DateRangePicker
|
||||
v-model="dateRange"
|
||||
format="dd.MM.yyyy"
|
||||
@update:model-value="handleDateRangeUpdate"
|
||||
placeholder="Izberi datumska območja"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium">Segment</label>
|
||||
<Select
|
||||
:model-value="selectedSegment"
|
||||
@update:model-value="handleSegmentChange"
|
||||
>
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue placeholder="Vsi segmenti" />
|
||||
</SelectTrigger>
|
||||
<SelectContent class="w-[var(--radix-select-trigger-width)]">
|
||||
<SelectItem :value="null">Vsi segmenti</SelectItem>
|
||||
<SelectItem
|
||||
v-for="segment in segments"
|
||||
:key="segment.id"
|
||||
:value="String(segment.id)"
|
||||
>
|
||||
{{ segment.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="flex justify-end pt-2 border-t">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="!dateRange?.start && !dateRange?.end && !selectedSegment"
|
||||
@click="clearDateFilter"
|
||||
>
|
||||
Počisti
|
||||
<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="
|
||||
dateRange?.start || dateRange?.end || selectedSegments?.length
|
||||
"
|
||||
class="ml-1 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground"
|
||||
>
|
||||
{{
|
||||
[
|
||||
dateRange?.start || dateRange?.end ? 1 : 0,
|
||||
selectedSegments?.length ? 1 : 0,
|
||||
].reduce((a, b) => a + b, 0)
|
||||
}}
|
||||
</span>
|
||||
</Button>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-medium text-sm">Filtri pogodb</h4>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Izberite filtre za prikaz pogodb
|
||||
</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 referenci, stranki..."
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<InputLabel>Datumska območja</InputLabel>
|
||||
<DateRangePicker
|
||||
v-model="dateRange"
|
||||
format="dd.MM.yyyy"
|
||||
placeholder="Izberi datumska območja"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<InputLabel>Segmenti</InputLabel>
|
||||
<AppMultiSelect
|
||||
v-model="selectedSegments"
|
||||
:items="
|
||||
segments.map((s) => ({ value: String(s.id), label: s.name }))
|
||||
"
|
||||
placeholder="Vsi segmenti"
|
||||
search-placeholder="Išči segment..."
|
||||
empty-text="Ni segmentov"
|
||||
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 &&
|
||||
selectedSegments.length === 0 &&
|
||||
search === ''
|
||||
"
|
||||
@click="clearDateFilter"
|
||||
>
|
||||
Počisti
|
||||
</Button>
|
||||
<Button type="button" size="sm" @click="applyDateFilter">
|
||||
Uporabi
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppPopover>
|
||||
</template>
|
||||
<template #cell-reference="{ row }">
|
||||
<Link
|
||||
:href="route('clientCase.show', caseShowParams(row))"
|
||||
class="text-indigo-600 hover:underline"
|
||||
class="font-semibold hover:underline text-primary-700"
|
||||
>
|
||||
{{ row.reference }}
|
||||
</Link>
|
||||
|
||||
Reference in New Issue
Block a user