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
+23 -21
View File
@@ -3,9 +3,10 @@ import AppLayout from "@/Layouts/AppLayout.vue";
import SectionTitle from "@/Components/SectionTitle.vue";
import { Link, router } from "@inertiajs/vue3";
import { ref } from "vue";
import DataTable from "@/Components/DataTable/DataTable.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";
const props = defineProps({
client_cases: Object,
@@ -50,10 +51,8 @@ const fmtDateDMY = (v) => {
</SectionTitle>
</div>
<DataTable
:show-search="true"
:show-page-size="true"
:columns="[
{ key: 'nu', label: 'Št.', sortable: false, class: 'w-40' },
{ key: 'nu', label: 'Št.', sortable: false },
{ key: 'case', label: 'Primer', sortable: false },
{ key: 'client', label: 'Stranka', sortable: false },
{ key: 'tax', label: 'Davčna', sortable: false },
@@ -71,23 +70,13 @@ const fmtDateDMY = (v) => {
align: 'right',
},
]"
:rows="client_cases.data || []"
:meta="{
current_page: client_cases.current_page,
per_page: client_cases.per_page,
total: client_cases.total,
last_page: client_cases.last_page,
from: client_cases.from,
to: client_cases.to,
links: client_cases.links,
}"
:search="search"
route-name="clientCase"
page-param-name="client-cases-page"
:only-props="['client_cases']"
:empty-icon="faFolderOpen"
empty-text="Ni zadetkov"
empty-description="Ni najdenih primerov. Ustvarite nov primer ali preverite iskalne kriterije."
:data="client_cases.data || []"
:page-size="client_cases.per_page"
:show-pagination="false"
:show-toolbar="true"
:hoverable="true"
row-key="uuid"
empty-text="Ni najdenih primerov."
>
<template #cell-nu="{ row }">
{{ row.person?.nu || "-" }}
@@ -130,6 +119,19 @@ const fmtDateDMY = (v) => {
</div>
</template>
</DataTable>
<div class="border-t border-gray-200 p-4">
<Pagination
:links="client_cases.links"
:from="client_cases.from"
:to="client_cases.to"
:total="client_cases.total"
:per-page="client_cases.per_page || 20"
:last-page="client_cases.last_page"
:current-page="client_cases.current_page"
per-page-param="perPage"
page-param="clientCasesPage"
/>
</div>
</div>
<!-- Pagination handled by DataTableServer -->
</div>
@@ -1,12 +1,28 @@
<script setup>
import { ref, computed } from "vue";
import { ref, computed, useSlots, watch, onMounted } from "vue";
import { router } from "@inertiajs/vue3";
import DataTable from "@/Components/DataTable/DataTable.vue";
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
import DeleteDialog from "@/Components/Dialogs/DeleteDialog.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faTrash, faEllipsisVertical, faCopy } from "@fortawesome/free-solid-svg-icons";
import Dropdown from "@/Components/Dropdown.vue";
import { Button } from "@/Components/ui/button";
import TableActions from "@/Components/DataTable/TableActions.vue";
import ActionMenuItem from "@/Components/DataTable/ActionMenuItem.vue";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/Components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/Components/ui/popover";
import { RangeCalendar } from "@/Components/ui/range-calendar";
import { CalendarIcon, X, Filter, Check, ChevronsUpDown } from "lucide-vue-next";
import { cn } from "@/lib/utils";
import { DateFormatter, getLocalTimeZone, parseDate } from "@internationalized/date";
library.add(faTrash, faEllipsisVertical, faCopy);
@@ -14,15 +30,219 @@ const props = defineProps({
client_case: Object,
activities: Object,
edit: Boolean,
actions: Array,
contracts: [Object, Array],
pageSize: {
type: Number,
default: 20,
},
});
const slots = useSlots();
// Filter state
const filters = ref({
activity_action_id: null,
activity_contract_uuid: null,
activity_user_id: null,
activity_date_from: null,
activity_date_to: null,
});
// Date range for calendar
const dateRange = ref(undefined);
// Filter popover state
const filterPopoverOpen = ref(false);
const actionComboboxOpen = ref(false);
const contractComboboxOpen = ref(false);
const userComboboxOpen = ref(false);
// Date formatter
const df = new DateFormatter("sl-SI", {
dateStyle: "medium",
});
// Get unique users from activities for filter dropdown
const uniqueUsers = computed(() => {
const users = new Map();
if (props.activities?.data) {
props.activities.data.forEach((activity) => {
if (activity.user) {
users.set(activity.user.id, activity.user);
}
});
}
return Array.from(users.values());
});
// Get contract options - handle both array and object with data property
const contractOptions = computed(() => {
if (!props.contracts) return [];
if (Array.isArray(props.contracts)) {
return props.contracts;
}
if (props.contracts.data && Array.isArray(props.contracts.data)) {
return props.contracts.data;
}
return [];
});
// Get action options with their decisions
const actionOptions = computed(() => {
if (!props.actions || !Array.isArray(props.actions)) return [];
return props.actions;
});
// Check if any filters are active
const hasActiveFilters = computed(() => {
return Object.values(filters.value).some((val) => val !== null && val !== "");
});
// Selected items for combobox display
const selectedAction = computed(() =>
actionOptions.value.find((a) => a.id === filters.value.activity_action_id)
);
const selectedContract = computed(() =>
contractOptions.value.find((c) => c.uuid === filters.value.activity_contract_uuid)
);
const selectedUser = computed(() =>
uniqueUsers.value.find((u) => u.id === filters.value.activity_user_id)
);
// Apply filters
const applyFilters = () => {
// Sync date range to filter values
if (dateRange.value?.start) {
filters.value.activity_date_from = dateRange.value.start.toString();
}
if (dateRange.value?.end) {
filters.value.activity_date_to = dateRange.value.end.toString();
}
// Build filter object with only non-null values
const filterObj = {};
if (filters.value.activity_action_id) {
filterObj.action_id = filters.value.activity_action_id;
}
if (filters.value.activity_contract_uuid) {
filterObj.contract_uuid = filters.value.activity_contract_uuid;
}
if (filters.value.activity_user_id) {
filterObj.user_id = filters.value.activity_user_id;
}
if (filters.value.activity_date_from) {
filterObj.date_from = filters.value.activity_date_from;
}
if (filters.value.activity_date_to) {
filterObj.date_to = filters.value.activity_date_to;
}
console.log("Applying filters:", filterObj);
// Build query params object
const queryParams = {};
// Preserve existing query params (like segment)
const searchParams = new URLSearchParams(window.location.search);
for (const [key, value] of searchParams.entries()) {
if (key !== "filter_activities") {
queryParams[key] = value;
}
}
// Only add filter param if there are active filters
if (Object.keys(filterObj).length > 0) {
const compressed = btoa(JSON.stringify(filterObj));
console.log("Compressed filter:", compressed);
queryParams.filter_activities = compressed;
}
console.log("Query params:", queryParams);
router.get(
route("clientCase.show", {
client_case: props.client_case.uuid,
...queryParams,
}),
{},
{
only: ["activities"],
preserveState: true,
preserveScroll: true,
}
);
filterPopoverOpen.value = false;
};
// Clear all filters
const clearFilters = () => {
filters.value = {
activity_action_id: null,
activity_contract_uuid: null,
activity_user_id: null,
activity_date_from: null,
activity_date_to: null,
};
dateRange.value = undefined;
filterPopoverOpen.value = false;
applyFilters();
};
// Load filters from URL on mount
onMounted(() => {
const searchParams = new URLSearchParams(window.location.search);
const filterParam = searchParams.get("filter_activities");
if (filterParam) {
try {
const decompressed = atob(filterParam);
const filterObj = JSON.parse(decompressed);
if (filterObj) {
filters.value.activity_action_id = filterObj.action_id || null;
filters.value.activity_contract_uuid = filterObj.contract_uuid || null;
filters.value.activity_user_id = filterObj.user_id || null;
filters.value.activity_date_from = filterObj.date_from || null;
filters.value.activity_date_to = filterObj.date_to || null;
// Parse dates for calendar
if (filterObj.date_from && filterObj.date_to) {
try {
dateRange.value = {
start: parseDate(filterObj.date_from),
end: parseDate(filterObj.date_to),
};
} catch (e) {
console.error("Failed to parse dates:", e);
}
} else if (filterObj.date_from) {
try {
dateRange.value = {
start: parseDate(filterObj.date_from),
end: parseDate(filterObj.date_from),
};
} catch (e) {
console.error("Failed to parse date_from:", e);
}
}
}
} catch (e) {
console.error("Failed to load filters from URL:", e);
}
}
});
const columns = [
{ key: "decision_dot", label: " ", class: "w-[6%]" },
{ key: "contract", label: "Pogodba", class: "w-[14%]" },
{ key: "decision", label: "Odločitev", class: "w-[26%]" },
{ key: "note", label: "Opomba", class: "w-[14%]" },
{ key: "promise", label: "Obljuba", class: "w-[20%]" },
{ key: "user", label: "Dodal", class: "w-[10%]" },
{ key: "decision_dot", label: "", sortable: false, align: "center" },
{ key: "contract", label: "Pogodba", sortable: false },
{ key: "decision", label: "Odločitev", sortable: false },
{ key: "note", label: "Opomba", sortable: false },
{ key: "promise", label: "Obljuba", sortable: false },
{ key: "user", label: "Dodal", sortable: false },
{ key: "actions", label: "", sortable: false, hideable: false, align: "center" },
];
const rows = computed(() => props.activities?.data || []);
@@ -59,7 +279,9 @@ const fmtDateTime = (d) => {
const fmtCurrency = (v) => {
const n = Number(v ?? 0);
try {
return new Intl.NumberFormat("sl-SI", { style: "currency", currency: "EUR" }).format(n);
return new Intl.NumberFormat("sl-SI", { style: "currency", currency: "EUR" }).format(
n
);
} catch {
return `${n.toFixed(2)}`;
}
@@ -109,141 +331,450 @@ const copyToClipboard = async (text) => {
</script>
<template>
<div class="p-4">
<div class="space-y-4">
<DataTable
:columns="columns"
:rows="rows"
:show-toolbar="true"
:data="rows"
:meta="activities"
route-name="clientCase.show"
:route-params="{ client_case: client_case.uuid }"
:only-props="['activities']"
:page-size="pageSize"
:page-size-options="[10, 15, 25, 50, 100]"
page-param-name="activities_page"
per-page-param-name="activities_per_page"
:show-pagination="false"
:show-search="false"
:show-page-size="false"
:show-add="!!$slots.add"
:show-toolbar="true"
:hoverable="true"
row-key="id"
empty-text="Ni aktivnosti."
class="border-0"
>
<template #toolbar-add>
<slot name="add" />
</template>
<template #toolbar-filters>
<!-- Filter Popover -->
<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"
>
{{ Object.values(filters).filter((v) => v !== null && v !== "").length }}
</span>
</Button>
</PopoverTrigger>
<PopoverContent class="w-[400px]" align="start">
<div class="space-y-4">
<div class="space-y-2">
<h4 class="font-medium text-sm">Filtri aktivnosti</h4>
<p class="text-sm text-muted-foreground">
Izberite filtre za prikaz aktivnosti
</p>
</div>
<template #cell-decision_dot="{ row }">
<div class="flex justify-center">
<span
v-if="row.decision?.color_tag"
class="inline-block h-4 w-4 rounded-full ring-1 ring-gray-300"
:style="{ backgroundColor: row.decision?.color_tag }"
:title="row.decision?.color_tag"
/>
</div>
</template>
<template #cell-contract="{ row }">
<span v-if="row.contract?.reference">{{ row.contract.reference }}</span>
<span v-else class="text-gray-400"></span>
</template>
<template #cell-decision="{ row }">
<div class="flex flex-col gap-1">
<span
v-if="row.action?.name"
class="inline-block w-fit px-2 py-0.5 rounded text-[10px] font-medium bg-indigo-100 text-indigo-700 tracking-wide uppercase"
>
{{ row.action.name }}
</span>
<span class="text-gray-800">{{ row.decision?.name || "" }}</span>
</div>
</template>
<template #cell-note="{ row }">
<div class="max-w-[280px] whitespace-pre-wrap break-words leading-snug">
<template v-if="row.note && row.note.length <= 60">
{{ row.note }}
</template>
<template v-else-if="row.note">
<span>{{ row.note.slice(0, 60) }} </span>
<Dropdown align="left" width="56" :content-classes="['p-2', 'bg-white', 'shadow', 'max-w-xs']">
<template #trigger>
<button type="button" class="inline-flex items-center text-[11px] text-indigo-600 hover:underline">
Več
</button>
</template>
<template #content>
<div class="relative" @click.stop>
<div class="flex items-center justify-between p-1 border-b border-gray-200">
<span class="text-xs font-medium text-gray-600">Opomba</span>
<button
@click="copyToClipboard(row.note)"
class="flex items-center gap-1 px-2 py-1 text-xs text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded transition-colors"
title="Kopiraj v odložišče"
<div class="space-y-3">
<!-- Action Filter -->
<div class="space-y-1.5">
<label class="text-sm font-medium">Akcija</label>
<Popover v-model:open="actionComboboxOpen">
<PopoverTrigger as-child>
<Button
variant="outline"
role="combobox"
:aria-expanded="actionComboboxOpen"
class="w-full justify-between"
>
<FontAwesomeIcon :icon="faCopy" class="w-3 h-3" />
<span>Kopiraj</span>
</button>
</div>
<div class="max-h-60 overflow-auto text-[12px] whitespace-pre-wrap break-words p-2">
{{ row.note }}
</div>
{{ selectedAction?.name || "Vse akcije" }}
<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 akcijo..." />
<CommandList>
<CommandEmpty>Akcija ni najdena.</CommandEmpty>
<CommandGroup>
<CommandItem
:value="'null'"
@select="
() => {
filters.activity_action_id = null;
actionComboboxOpen = false;
}
"
>
<Check
:class="
cn(
'mr-2 h-4 w-4',
filters.activity_action_id === null
? 'opacity-100'
: 'opacity-0'
)
"
/>
Vse akcije
</CommandItem>
<CommandItem
v-for="action in actionOptions"
:key="action.id"
:value="action.name"
@select="
() => {
filters.activity_action_id = action.id;
actionComboboxOpen = false;
}
"
>
<Check
:class="
cn(
'mr-2 h-4 w-4',
filters.activity_action_id === action.id
? 'opacity-100'
: 'opacity-0'
)
"
/>
{{ action.name }}
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<!-- Contract Filter -->
<div class="space-y-1.5">
<label class="text-sm font-medium">Pogodba</label>
<Popover v-model:open="contractComboboxOpen">
<PopoverTrigger as-child>
<Button
variant="outline"
role="combobox"
:aria-expanded="contractComboboxOpen"
class="w-full justify-between"
>
{{ selectedContract?.reference || "Vse pogodbe" }}
<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 pogodbo..." />
<CommandList>
<CommandEmpty>Pogodba ni najdena.</CommandEmpty>
<CommandGroup>
<CommandItem
:value="'null'"
@select="
() => {
filters.activity_contract_uuid = null;
contractComboboxOpen = false;
}
"
>
<Check
:class="
cn(
'mr-2 h-4 w-4',
filters.activity_contract_uuid === null
? 'opacity-100'
: 'opacity-0'
)
"
/>
Vse pogodbe
</CommandItem>
<CommandItem
v-for="contract in contractOptions"
:key="contract.uuid"
:value="contract.reference"
@select="
() => {
filters.activity_contract_uuid = contract.uuid;
contractComboboxOpen = false;
}
"
>
<Check
:class="
cn(
'mr-2 h-4 w-4',
filters.activity_contract_uuid === contract.uuid
? 'opacity-100'
: 'opacity-0'
)
"
/>
{{ contract.reference }}
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<!-- User Filter -->
<div class="space-y-1.5">
<label class="text-sm font-medium">Uporabnik</label>
<Popover v-model:open="userComboboxOpen">
<PopoverTrigger as-child>
<Button
variant="outline"
role="combobox"
:aria-expanded="userComboboxOpen"
class="w-full justify-between"
>
{{ selectedUser?.name || "Vsi uporabniki" }}
<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 uporabnika..." />
<CommandList>
<CommandEmpty>Uporabnik ni najden.</CommandEmpty>
<CommandGroup>
<CommandItem
:value="'null'"
@select="
() => {
filters.activity_user_id = null;
userComboboxOpen = false;
}
"
>
<Check
:class="
cn(
'mr-2 h-4 w-4',
filters.activity_user_id === null
? 'opacity-100'
: 'opacity-0'
)
"
/>
Vsi uporabniki
</CommandItem>
<CommandItem
v-for="user in uniqueUsers"
:key="user.id"
:value="user.name"
@select="
() => {
filters.activity_user_id = user.id;
userComboboxOpen = false;
}
"
>
<Check
:class="
cn(
'mr-2 h-4 w-4',
filters.activity_user_id === user.id
? 'opacity-100'
: 'opacity-0'
)
"
/>
{{ user.name }}
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<!-- Date Range -->
<div class="space-y-1.5">
<label class="text-sm font-medium">Časovno obdobje</label>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="[
'w-full justify-start text-left font-normal',
!dateRange?.start && 'text-muted-foreground',
]"
>
<CalendarIcon class="mr-2 h-4 w-4" />
<template v-if="dateRange?.start">
<template v-if="dateRange?.end">
{{ df.format(dateRange.start.toDate(getLocalTimeZone())) }}
-
{{ df.format(dateRange.end.toDate(getLocalTimeZone())) }}
</template>
<template v-else>
{{ df.format(dateRange.start.toDate(getLocalTimeZone())) }}
</template>
</template>
<template v-else> Izberite obdobje </template>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0" align="start">
<RangeCalendar
v-model="dateRange"
:number-of-months="2"
locale="sl-SI"
/>
</PopoverContent>
</Popover>
</div>
</div>
<!-- Actions -->
<div class="flex items-center justify-between pt-2 border-t">
<Button
v-if="hasActiveFilters"
variant="ghost"
size="sm"
@click="clearFilters"
class="gap-2"
>
<X class="h-4 w-4" />
Počisti
</Button>
<div v-else></div>
<Button variant="default" size="sm" @click="applyFilters" class="gap-2">
Uporabi
</Button>
</div>
</div>
</PopoverContent>
</Popover>
</template>
<template #toolbar-actions>
<slot name="add" />
</template>
<template #cell-decision_dot="{ row }">
<div class="flex justify-center">
<span
v-if="row.decision?.color_tag"
class="inline-block h-4 w-4 rounded-full ring-1 ring-gray-300"
:style="{ backgroundColor: row.decision?.color_tag }"
:title="row.decision?.color_tag"
/>
</div>
</template>
<template #cell-contract="{ row }">
<span v-if="row.contract?.reference">{{ row.contract.reference }}</span>
<span v-else class="text-gray-400"></span>
</template>
<template #cell-decision="{ row }">
<div class="flex flex-col gap-1">
<span
v-if="row.action?.name"
class="inline-block w-fit px-2 py-0.5 rounded text-[10px] font-medium bg-indigo-100 text-indigo-700 tracking-wide uppercase"
>
{{ row.action.name }}
</span>
<span class="text-gray-800">{{ row.decision?.name || "" }}</span>
</div>
</template>
<template #cell-note="{ row }">
<div class="max-w-[280px] whitespace-pre-wrap wrap-break-word leading-snug">
<template v-if="row.note && row.note.length <= 60">
{{ row.note }}
</template>
<template v-else-if="row.note">
<span>{{ row.note.slice(0, 60) }} </span>
<Dropdown
align="left"
width="56"
:content-classes="['p-2', 'bg-white', 'shadow', 'max-w-xs']"
>
<template #trigger>
<button
type="button"
class="inline-flex items-center text-[11px] text-indigo-600 hover:underline"
>
Več
</button>
</template>
<template #content>
<div class="relative" @click.stop>
<div
class="flex items-center justify-between p-1 border-b border-gray-200"
>
<span class="text-xs font-medium text-gray-600">Opomba</span>
<button
@click="copyToClipboard(row.note)"
class="flex items-center gap-1 px-2 py-1 text-xs text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded transition-colors"
title="Kopiraj v odložišče"
>
<FontAwesomeIcon :icon="faCopy" class="w-3 h-3" />
<span>Kopiraj</span>
</button>
</div>
</template>
</Dropdown>
</template>
<template v-else>
<span class="text-gray-400"></span>
</template>
</div>
</template>
<div
class="max-h-60 overflow-auto text-[12px] whitespace-pre-wrap wrap-break-word p-2"
>
{{ row.note }}
</div>
</div>
</template>
</Dropdown>
</template>
<template v-else>
<span class="text-gray-400"></span>
</template>
</div>
</template>
<template #cell-promise="{ row }">
<div class="flex flex-col gap-1 text-[12px]">
<div v-if="row.amount && Number(row.amount) !== 0" class="leading-tight">
<span class="text-gray-500">Z:</span>
<span class="font-medium ml-1">{{ fmtCurrency(row.amount) }}</span>
</div>
<div v-if="row.due_date" class="leading-tight">
<span class="text-gray-500">D:</span>
<span class="ml-1">{{ fmtDate(row.due_date) }}</span>
</div>
<div v-if="!row.due_date && (!row.amount || Number(row.amount) === 0)" class="text-gray-400">
</div>
<template #cell-promise="{ row }">
<div class="flex flex-col gap-1 text-[12px]">
<div v-if="row.amount && Number(row.amount) !== 0" class="leading-tight">
<span class="text-gray-500">Z:</span>
<span class="font-medium ml-1">{{ fmtCurrency(row.amount) }}</span>
</div>
</template>
<div v-if="row.due_date" class="leading-tight">
<span class="text-gray-500">D:</span>
<span class="ml-1">{{ fmtDate(row.due_date) }}</span>
</div>
<div
v-if="!row.due_date && (!row.amount || Number(row.amount) === 0)"
class="text-gray-400"
>
</div>
</div>
</template>
<template #cell-user="{ row }">
<div class="text-gray-800 font-medium leading-tight">
{{ row.user?.name || row.user_name || "" }}
</div>
<div v-if="row.created_at" class="mt-1">
<span class="inline-block px-2 py-0.5 rounded text-[11px] bg-gray-100 text-gray-600 tracking-wide">
{{ fmtDateTime(row.created_at) }}
</span>
</div>
</template>
<template #cell-user="{ row }">
<div class="text-gray-800 font-medium leading-tight">
{{ row.user?.name || row.user_name || "" }}
</div>
<div v-if="row.created_at" class="mt-1">
<span
class="inline-block px-2 py-0.5 rounded text-[11px] bg-gray-100 text-gray-600 tracking-wide"
>
{{ fmtDateTime(row.created_at) }}
</span>
</div>
</template>
<template #actions="{ row }" v-if="edit">
<Dropdown align="right" width="30">
<template #trigger>
<button
type="button"
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 focus:outline-none"
title="Možnosti"
>
<FontAwesomeIcon :icon="faEllipsisVertical" class="h-4 w-4 text-gray-700" />
</button>
</template>
<template #content>
<button
type="button"
class="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-red-50 text-red-600"
@click.stop="openDelete(row)"
>
<FontAwesomeIcon :icon="['fas', 'trash']" class="w-4 h-4" />
<span>Izbriši</span>
</button>
</template>
</Dropdown>
</template>
<template #cell-actions="{ row }" v-if="edit">
<TableActions align="right">
<template #default>
<ActionMenuItem
:icon="faTrash"
label="Izbriši"
danger
@click="openDelete(row)"
/>
</template>
</TableActions>
</template>
</DataTable>
</div>
@@ -256,4 +787,3 @@ const copyToClipboard = async (text) => {
@confirm="confirmDeleteAction"
/>
</template>
@@ -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>
+110 -92
View File
@@ -3,6 +3,7 @@ import PersonInfoGrid from "@/Components/PersonInfo/PersonInfoGrid.vue";
import SectionTitle from "@/Components/SectionTitle.vue";
import AppLayout from "@/Layouts/AppLayout.vue";
import { Button } from "@/Components/ui/button";
import { Card } from "@/Components/ui/card";
import { onBeforeMount, ref, computed } from "vue";
import ContractDrawer from "./Partials/ContractDrawer.vue";
import ContractTable from "./Partials/ContractTable.vue";
@@ -21,38 +22,39 @@ import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import { hasPermission } from "@/Services/permissions";
import ActionMenuItem from "@/Components/DataTable/ActionMenuItem.vue";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { DropdownMenu } from "@/Components/ui/dropdown-menu";
import DropdownMenuContent from "@/Components/ui/dropdown-menu/DropdownMenuContent.vue";
import { Badge } from "@/Components/ui/badge";
const props = defineProps({
client: Object,
client_case: Object,
contracts: [Object, Array], // Can be paginated object or array (backward compatibility)
activities: Object,
contracts: Object, // Resource Collection with data/links/meta
activities: Object, // Resource Collection with data/links/meta
contract_types: Array,
account_types: { type: Array, default: () => [] },
actions: Array,
types: Object,
documents: Array,
documents: Object, // Resource Collection with data property
segments: { type: Array, default: () => [] },
all_segments: { type: Array, default: () => [] },
current_segment: { type: Object, default: null },
contract_doc_templates: { type: Array, default: () => [] },
});
// Extract contracts array from paginated object or use array directly
// Extract contracts array from Resource Collection
const contractsArray = computed(() => {
if (Array.isArray(props.contracts)) {
return props.contracts;
}
// Handle paginated contracts
if (props.contracts?.data) {
return props.contracts.data;
}
return [];
return props.contracts?.data || [];
});
// Check if contracts are paginated
// Contracts are always paginated now (Resource Collection)
const contractsPaginated = computed(() => {
return props.contracts && !Array.isArray(props.contracts) && props.contracts.data;
return props.contracts?.links !== undefined;
});
// Extract documents array from Resource Collection
const documentsArray = computed(() => {
return props.documents?.data || [];
});
const page = usePage();
@@ -245,6 +247,7 @@ const submitAttachSegment = () => {
<AppLayout title="Client case">
<template #header></template>
<div class="pt-12">
<!-- Client details -->
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<!-- Current segment badge (right aligned, above the card) -->
<div v-if="current_segment" class="flex justify-end pb-3">
@@ -257,9 +260,7 @@ const submitAttachSegment = () => {
</span>
</div>
</div>
<div
class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4 border-blue-500"
>
<Card class="border-l-4 border-blue-500">
<div class="mx-auto max-w-4x1 p-3 flex justify-between items-center">
<SectionTitle>
<template #title>
@@ -268,21 +269,16 @@ const submitAttachSegment = () => {
</a>
</template>
</SectionTitle>
<button @click="showClientDetails" :hidden="clientDetails ? false : true">
<AngleUpIcon />
</button>
<button :hidden="clientDetails" @click="hideClietnDetails">
<AngleDownIcon />
</button>
<Badge variant="secondary" class="bg-blue-500 text-white dark:bg-blue-600">
Naročnik
</Badge>
</div>
</div>
</Card>
</div>
</div>
<div class="pt-1" :hidden="clientDetails">
<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"
>
<Card class="border-l-4 border-blue-400">
<div class="mx-auto max-w-4x1 p-3">
<PersonInfoGrid
:types="types"
@@ -290,37 +286,37 @@ const submitAttachSegment = () => {
:edit="hasPerm('client-edit')"
/>
</div>
</div>
</Card>
</div>
</div>
<!-- Case details -->
<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-red-400"
>
<Card class="border-l-4 border-red-400">
<div class="mx-auto max-w-4x1 p-3 flex items-center justify-between">
<SectionTitle>
<template #title> Primer - oseba </template>
<template #title>{{ client_case.person.full_name }}</template>
</SectionTitle>
<div
v-if="client_case && client_case.client_ref"
class="text-xs text-gray-600"
>
<span class="mr-1">Ref:</span>
<span
class="inline-block px-2 py-0.5 rounded border bg-gray-50 font-mono text-gray-700"
>{{ client_case.client_ref }}</span
<div class="flex items-center gap-2">
<div
v-if="client_case && client_case.client_ref"
class="text-xs text-gray-600"
>
<span class="mr-1">Ref:</span>
<span
class="inline-block px-2 py-0.5 rounded border bg-gray-50 font-mono text-gray-700"
>{{ client_case.client_ref }}</span
>
</div>
<Badge variant="destructive" class="text-white"> Primer </Badge>
</div>
</div>
</div>
</Card>
</div>
</div>
<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-red-400"
>
<Card class="border-l-4 border-red-400">
<div class="mx-auto max-w-4x1 p-3">
<PersonInfoGrid
:types="types"
@@ -331,31 +327,18 @@ const submitAttachSegment = () => {
:client-case-uuid="client_case.uuid"
/>
</div>
</div>
</Card>
</div>
</div>
<!-- Contracts section -->
<div class="pt-12">
<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">
<Card>
<div class="mx-auto max-w-4x1">
<div class="flex justify-between p-3">
<div class="p-3">
<SectionTitle>
<template #title> Pogodbe </template>
</SectionTitle>
<div class="flex items-center gap-2" v-if="hasPerm('contract-edit')">
<Button @click="openDrawerCreateContract">Nova</Button>
<Button
variant="outline"
:disabled="availableSegments.length === 0"
@click="openAttachSegment"
>
{{
availableSegments.length
? "Dodaj segment"
: "Ni razpoložljivih segmentov"
}}
</Button>
</div>
</div>
<ContractTable
:client="client"
@@ -363,29 +346,37 @@ const submitAttachSegment = () => {
:contracts="contractsArray"
:contract_types="contract_types"
:segments="segments"
:all_segments="all_segments"
:templates="contract_doc_templates"
:edit="hasPerm('contract-edit')"
:create-doc="hasPerm('create-docs')"
@edit="openDrawerEditContract"
@delete="requestDeleteContract"
@add-activity="openDrawerAddActivity"
@create="openDrawerCreateContract"
@attach-segment="openAttachSegment"
/>
<div v-if="contractsPaginated" class="border-t border-gray-200">
<div v-if="contractsPaginated" class="border-t border-gray-200 p-4">
<Pagination
:links="contracts.links"
:from="contracts.from"
:to="contracts.to"
:total="contracts.total"
:per-page="contracts.per_page || 50"
:last-page="contracts.last_page"
:current-page="contracts.current_page"
per-page-param="contracts_per_page"
page-param="contracts_page"
/>
</div>
</div>
</div>
</Card>
</div>
</div>
<div class="pt-12 pb-6">
<!-- Activities section -->
<div class="pt-12">
<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">
<Card>
<div class="mx-auto max-w-4x1">
<div class="flex justify-between p-4">
<SectionTitle>
@@ -396,39 +387,49 @@ const submitAttachSegment = () => {
:client_case="client_case"
:activities="activities"
:edit="hasPerm('activity-edit')"
:actions="actions"
:contracts="contractsArray"
:page-size="activities.per_page || 20"
>
<template #add>
<ActionMenuItem
label="Nova aktivnost"
:icon="faPlus"
@click="openDrawerAddActivity"
/>
<Button variant="outline" size="sm" @click="openDrawerAddActivity">
Nova aktivnost
</Button>
</template>
</ActivityTable>
<Pagination
:links="activities.links"
:from="activities.from"
:to="activities.to"
:total="activities.total"
/>
<div class="border-t border-gray-200 p-4">
<Pagination
:links="activities.links"
:from="activities.from"
:to="activities.to"
:total="activities.total"
:per-page="activities.per_page || 15"
:last-page="activities.last_page"
:current-page="activities.current_page"
per-page-param="activities_per_page"
page-param="activities_page"
/>
</div>
</div>
</div>
</Card>
</div>
</div>
<!-- Documents section -->
<div class="pt-12">
<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">
<Card>
<div class="mx-auto max-w-4x1">
<div class="flex justify-between p-4">
<div class="p-4">
<SectionTitle>
<template #title>Dokumenti</template>
</SectionTitle>
<Button @click="openUpload">Dodaj</Button>
</div>
<DocumentsTable
:documents="documents"
:client-case="client_case"
:edit="hasPerm('doc-edit')"
:page-size="documents.per_page || 15"
:page-size-options="[10, 15, 25, 50, 100]"
@view="openViewer"
@edit="openDocEdit"
:download-url-builder="
@@ -447,9 +448,26 @@ const submitAttachSegment = () => {
});
}
"
/>
>
<template #add>
<Button variant="outline" @click="openUpload">Novi document</Button>
</template>
</DocumentsTable>
<div class="border-t border-gray-200 p-4">
<Pagination
:links="documents.links"
:from="documents.from"
:to="documents.to"
:total="documents.total"
:per-page="documents.per_page || 15"
:last-page="documents.last_page"
:current-page="documents.current_page"
per-page-param="documentsPerPage"
page-param="documentsPage"
/>
</div>
</div>
</div>
</Card>
</div>
</div>
<DocumentUploadDialog
@@ -483,15 +501,15 @@ const submitAttachSegment = () => {
:client_case="client_case"
:contract="contractEditing"
/>
<ActivityDrawer
:show="drawerAddActivity"
@close="closeDrawer"
:client_case="client_case"
:actions="actions"
:contract-uuid="activityContractUuid"
:documents="documents"
:contracts="contractsArray"
/>
<ActivityDrawer
:show="drawerAddActivity"
@close="closeDrawer"
:client_case="client_case"
:actions="actions"
:contract-uuid="activityContractUuid"
:documents="documentsArray"
:contracts="contractsArray"
/>
<DeleteDialog
:show="confirmDelete.show"
title="Izbriši pogodbo"
+187 -208
View File
@@ -3,11 +3,12 @@ import { computed, ref } from "vue";
import AppLayout from "@/Layouts/AppLayout.vue";
import { Link, router, usePage } from "@inertiajs/vue3";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import DataTable from "@/Components/DataTable/DataTable.vue";
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
import { hasPermission } from "@/Services/permissions";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { faUserGroup } from "@fortawesome/free-solid-svg-icons";
import { Button } from "@/Components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/Components/ui/card";
import { Input } from "@/Components/ui/input";
import ActionMenuItem from "@/Components/DataTable/ActionMenuItem.vue";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
@@ -29,6 +30,7 @@ import { useForm } from "vee-validate";
import { toTypedSchema } from "@vee-validate/zod";
import * as z from "zod";
import ActionMessage from "@/Components/ActionMessage.vue";
import { Mail, Plug2Icon } from "lucide-vue-next";
const props = defineProps({
clients: Object,
@@ -121,29 +123,25 @@ const storeClient = async () => {
},
};
router.post(
route("client.store"),
payload,
{
onSuccess: () => {
closeDrawer();
formClient.resetForm();
processing.value = false;
},
onError: (errors) => {
Object.keys(errors).forEach((field) => {
const errorMessages = Array.isArray(errors[field])
? errors[field]
: [errors[field]];
formClient.setFieldError(field, errorMessages[0]);
});
processing.value = false;
},
onFinish: () => {
processing.value = false;
},
}
);
router.post(route("client.store"), payload, {
onSuccess: () => {
closeDrawer();
formClient.resetForm();
processing.value = false;
},
onError: (errors) => {
Object.keys(errors).forEach((field) => {
const errorMessages = Array.isArray(errors[field])
? errors[field]
: [errors[field]];
formClient.setFieldError(field, errorMessages[0]);
});
processing.value = false;
},
onFinish: () => {
processing.value = false;
},
});
};
const onConfirmCreate = formClient.handleSubmit(() => {
@@ -168,15 +166,14 @@ const fmtCurrency = (v) => {
<template #header> </template>
<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 space-y-3">
<!-- DataTable (server-side) -->
<Card>
<CardHeader>
<CardTitle>Naročniki</CardTitle>
</CardHeader>
<CardContent>
<DataTable
:show-search="true"
:show-page-size="true"
:show-add="true"
:columns="[
{ key: 'nu', label: 'Št.', sortable: false, class: 'w-40' },
{ key: 'nu', label: 'Št.', sortable: false },
{ key: 'name', label: 'Naročnik', sortable: false },
{
key: 'cases',
@@ -191,35 +188,17 @@ const fmtCurrency = (v) => {
align: 'right',
},
]"
:rows="clients.data || []"
:meta="{
current_page: clients.current_page,
per_page: clients.per_page,
total: clients.total,
last_page: clients.last_page,
from: clients.from,
to: clients.to,
links: clients.links,
}"
:sort="{
key: props.filters?.sort || null,
direction: props.filters?.direction || null,
}"
:search="initialSearch"
route-name="client"
:data="clients.data || []"
:show-pagination="false"
:show-toolbar="true"
:hoverable="true"
row-key="uuid"
:only-props="['clients']"
:empty-icon="faUserGroup"
empty-text="Ni zadetkov"
empty-description="Ni najdenih naročnikov. Ustvarite novega naročnika ali preverite iskalne kriterije."
empty-text="Ni najdenih naročnikov."
>
<template #toolbar-add>
<ActionMenuItem
v-if="hasPerm('client-edit')"
label="Dodaj naročnika"
:icon="faPlus"
@click="openDrawerCreateClient"
/>
<Button @click="openDrawerCreateClient">
<Plug2Icon class="w-4 h-4 mr-2" /> Dodaj
</Button>
</template>
<template #cell-nu="{ row }">
{{ row.person?.nu || "-" }}
@@ -253,8 +232,8 @@ const fmtCurrency = (v) => {
</div>
</template>
</DataTable>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</AppLayout>
@@ -267,161 +246,161 @@ const fmtCurrency = (v) => {
@confirm="onConfirmCreate"
>
<form @submit.prevent="onConfirmCreate">
<div class="space-y-4">
<FormField v-slot="{ componentField }" name="full_name">
<FormItem>
<FormLabel>Naziv</FormLabel>
<div class="space-y-4">
<FormField v-slot="{ componentField }" name="full_name">
<FormItem>
<FormLabel>Naziv</FormLabel>
<FormControl>
<Input
id="fullname"
type="text"
autocomplete="full-name"
placeholder="Naziv"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="tax_number">
<FormItem>
<FormLabel>Davčna</FormLabel>
<FormControl>
<Input
id="taxnumber"
type="text"
autocomplete="tax-number"
placeholder="Davčna številka"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="social_security_number">
<FormItem>
<FormLabel>Matična / Emšo</FormLabel>
<FormControl>
<Input
id="socialSecurityNumber"
type="text"
autocomplete="social-security-number"
placeholder="Matična / Emšo"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="address.address">
<FormItem>
<FormLabel>Naslov</FormLabel>
<FormControl>
<Input
id="address"
type="text"
autocomplete="address"
placeholder="Naslov"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="address.country">
<FormItem>
<FormLabel>Država</FormLabel>
<FormControl>
<Input
id="addressCountry"
type="text"
autocomplete="address-country"
placeholder="Država"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="address.type_id">
<FormItem>
<FormLabel>Vrsta naslova</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<Input
id="fullname"
type="text"
autocomplete="full-name"
placeholder="Naziv"
v-bind="componentField"
/>
<SelectTrigger>
<SelectValue placeholder="Izberi vrsto naslova" />
</SelectTrigger>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<SelectContent>
<SelectItem :value="1">Stalni</SelectItem>
<SelectItem :value="2">Začasni</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="tax_number">
<FormItem>
<FormLabel>Davčna</FormLabel>
<FormField v-slot="{ value, handleChange }" name="phone.country_code">
<FormItem>
<FormLabel>Koda države tel.</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<Input
id="taxnumber"
type="text"
autocomplete="tax-number"
placeholder="Davčna številka"
v-bind="componentField"
/>
<SelectTrigger>
<SelectValue placeholder="Izberi kodo države" />
</SelectTrigger>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<SelectContent>
<SelectItem value="00386">+386 (Slovenija)</SelectItem>
<SelectItem value="00385">+385 (Hrvaška)</SelectItem>
<SelectItem value="0039">+39 (Italija)</SelectItem>
<SelectItem value="0036">+36 (Madžarska)</SelectItem>
<SelectItem value="0043">+43 (Avstrija)</SelectItem>
<SelectItem value="00381">+381 (Srbija)</SelectItem>
<SelectItem value="00387">+387 (Bosna in Hercegovina)</SelectItem>
<SelectItem value="00382">+382 (Črna gora)</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="social_security_number">
<FormItem>
<FormLabel>Matična / Emšo</FormLabel>
<FormControl>
<Input
id="socialSecurityNumber"
type="text"
autocomplete="social-security-number"
placeholder="Matična / Emšo"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="phone.nu">
<FormItem>
<FormLabel>Telefonska št.</FormLabel>
<FormControl>
<Input
id="phoneNu"
type="text"
autocomplete="phone-nu"
placeholder="Telefonska številka"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="address.address">
<FormItem>
<FormLabel>Naslov</FormLabel>
<FormControl>
<Input
id="address"
type="text"
autocomplete="address"
placeholder="Naslov"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="address.country">
<FormItem>
<FormLabel>Država</FormLabel>
<FormControl>
<Input
id="addressCountry"
type="text"
autocomplete="address-country"
placeholder="Država"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="address.type_id">
<FormItem>
<FormLabel>Vrsta naslova</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Izberi vrsto naslova" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem :value="1">Stalni</SelectItem>
<SelectItem :value="2">Začasni</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="phone.country_code">
<FormItem>
<FormLabel>Koda države tel.</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Izberi kodo države" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="00386">+386 (Slovenija)</SelectItem>
<SelectItem value="00385">+385 (Hrvaška)</SelectItem>
<SelectItem value="0039">+39 (Italija)</SelectItem>
<SelectItem value="0036">+36 (Madžarska)</SelectItem>
<SelectItem value="0043">+43 (Avstrija)</SelectItem>
<SelectItem value="00381">+381 (Srbija)</SelectItem>
<SelectItem value="00387">+387 (Bosna in Hercegovina)</SelectItem>
<SelectItem value="00382">+382 (Črna gora)</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="phone.nu">
<FormItem>
<FormLabel>Telefonska št.</FormLabel>
<FormControl>
<Input
id="phoneNu"
type="text"
autocomplete="phone-nu"
placeholder="Telefonska številka"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="description">
<FormItem>
<FormLabel>Opis</FormLabel>
<FormControl>
<Input
id="description"
type="text"
autocomplete="description"
placeholder="Opis"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</div>
</form>
<FormField v-slot="{ componentField }" name="description">
<FormItem>
<FormLabel>Opis</FormLabel>
<FormControl>
<Input
id="description"
type="text"
autocomplete="description"
placeholder="Opis"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</div>
</form>
</CreateDialog>
</template>
@@ -0,0 +1,219 @@
<script setup>
import { ref } from 'vue';
import { Head } from '@inertiajs/vue3';
import AppLayout from '@/Layouts/AppLayout.vue';
import DataTableNew2 from '@/Components/DataTable/DataTableNew2.vue';
import { Badge } from '@/Components/ui/badge';
import { Button } from '@/Components/ui/button';
// Simple columns example
const simpleColumns = [
{ key: 'id', label: 'ID', sortable: true, class: 'w-20' },
{ key: 'name', label: 'Name', sortable: true },
{ key: 'email', label: 'Email', sortable: true },
{ key: 'status', label: 'Status', sortable: false },
{ key: 'amount', label: 'Amount', sortable: true, align: 'right', class: 'text-right' },
];
// Sample data
const data = ref([
{ id: 1, name: 'Alice Johnson', email: 'alice@example.com', status: 'active', amount: 1250.50 },
{ id: 2, name: 'Bob Smith', email: 'bob@example.com', status: 'inactive', amount: 890.25 },
{ id: 3, name: 'Carol Williams', email: 'carol@example.com', status: 'active', amount: 2100.00 },
{ id: 4, name: 'David Brown', email: 'david@example.com', status: 'pending', amount: 450.75 },
{ id: 5, name: 'Eve Davis', email: 'eve@example.com', status: 'active', amount: 1875.30 },
{ id: 6, name: 'Frank Miller', email: 'frank@example.com', status: 'inactive', amount: 670.00 },
{ id: 7, name: 'Grace Wilson', email: 'grace@example.com', status: 'active', amount: 3200.50 },
{ id: 8, name: 'Henry Moore', email: 'henry@example.com', status: 'pending', amount: 520.25 },
{ id: 9, name: 'Ivy Taylor', email: 'ivy@example.com', status: 'active', amount: 1950.00 },
{ id: 10, name: 'Jack Anderson', email: 'jack@example.com', status: 'inactive', amount: 780.40 },
{ id: 11, name: 'Kate Thomas', email: 'kate@example.com', status: 'active', amount: 2450.75 },
{ id: 12, name: 'Leo Jackson', email: 'leo@example.com', status: 'pending', amount: 930.60 },
]);
const selectedRows = ref([]);
function handleRowClick(row) {
console.log('Row clicked:', row);
}
function handleSelectionChange(keys) {
selectedRows.value = keys;
console.log('Selection changed:', keys);
}
function getStatusVariant(status) {
const variants = {
active: 'default',
inactive: 'secondary',
pending: 'outline',
};
return variants[status] || 'outline';
}
function formatCurrency(value) {
return new Intl.NumberFormat('sl-SI', {
style: 'currency',
currency: 'EUR',
}).format(value);
}
</script>
<template>
<AppLayout title="DataTable Example">
<Head title="DataTable Example" />
<div class="py-12">
<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-6">
<div class="mb-6">
<h2 class="text-2xl font-bold text-gray-900">
DataTable Component Example
</h2>
<p class="mt-1 text-sm text-gray-600">
This is a working example of the new shadcn-vue style DataTable component
</p>
</div>
<!-- Example 1: Basic Table -->
<div class="mb-12">
<h3 class="text-lg font-semibold mb-4">Basic Table with Simple Columns</h3>
<DataTableNew2
:columns="simpleColumns"
:data="data"
filter-column="email"
filter-placeholder="Search by email..."
@row:click="handleRowClick"
>
<!-- Custom cell for status using slot -->
<template #cell-status="{ value }">
<Badge :variant="getStatusVariant(value)">
{{ value }}
</Badge>
</template>
<!-- Custom cell for amount using slot -->
<template #cell-amount="{ value }">
<span class="font-medium">{{ formatCurrency(value) }}</span>
</template>
</DataTableNew2>
</div>
<!-- Example 2: Table with Row Selection -->
<div class="mb-12">
<h3 class="text-lg font-semibold mb-4">Table with Row Selection</h3>
<div v-if="selectedRows.length" class="mb-4 p-4 bg-blue-50 rounded-lg">
<p class="text-sm font-medium text-blue-900">
Selected {{ selectedRows.length }} row(s)
</p>
</div>
<DataTableNew2
:columns="simpleColumns"
:data="data"
enable-row-selection
filter-column="name"
filter-placeholder="Search by name..."
@selection:change="handleSelectionChange"
>
<template #cell-status="{ value }">
<Badge :variant="getStatusVariant(value)">
{{ value }}
</Badge>
</template>
<template #cell-amount="{ value }">
<span class="font-medium">{{ formatCurrency(value) }}</span>
</template>
</DataTableNew2>
</div>
<!-- Example 3: Striped & Hoverable -->
<div class="mb-12">
<h3 class="text-lg font-semibold mb-4">Striped Table with Hover</h3>
<DataTableNew2
:columns="simpleColumns"
:data="data.slice(0, 5)"
:page-size="5"
striped
hoverable
>
<template #cell-status="{ value }">
<Badge :variant="getStatusVariant(value)">
{{ value }}
</Badge>
</template>
<template #cell-amount="{ value }">
<span class="font-medium">{{ formatCurrency(value) }}</span>
</template>
</DataTableNew2>
</div>
<!-- Example 4: Custom Toolbar -->
<div>
<h3 class="text-lg font-semibold mb-4">Table with Custom Toolbar Actions</h3>
<DataTableNew2
:columns="simpleColumns"
:data="data"
filter-column="email"
filter-placeholder="Search emails..."
>
<!-- Add custom buttons to toolbar -->
<template #toolbar-actions="{ table }">
<Button variant="outline" size="sm" @click="() => console.log('Export clicked')">
Export
</Button>
<Button variant="default" size="sm" @click="() => console.log('Add clicked')">
Add New
</Button>
</template>
<template #cell-status="{ value }">
<Badge :variant="getStatusVariant(value)">
{{ value }}
</Badge>
</template>
<template #cell-amount="{ value }">
<span class="font-medium">{{ formatCurrency(value) }}</span>
</template>
</DataTableNew2>
</div>
<!-- Example 5: Full Custom Toolbar -->
<div class="mt-12">
<h3 class="text-lg font-semibold mb-4">Table with Custom Filters in Toolbar</h3>
<DataTableNew2
:columns="simpleColumns"
:data="data"
filter-column="name"
filter-placeholder="Search names..."
>
<!-- Add custom filter controls -->
<template #toolbar-filters="{ table }">
<select
class="h-8 rounded-md border border-input bg-background px-3 py-1 text-sm"
@change="(e) => {
const column = table.getColumn('status');
column?.setFilterValue(e.target.value || undefined);
}"
>
<option value="">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="pending">Pending</option>
</select>
</template>
<template #cell-status="{ value }">
<Badge :variant="getStatusVariant(value)">
{{ value }}
</Badge>
</template>
<template #cell-amount="{ value }">
<span class="font-medium">{{ formatCurrency(value) }}</span>
</template>
</DataTableNew2>
</div>
</div>
</div>
</div>
</AppLayout>
</template>
+4 -41
View File
@@ -2,6 +2,7 @@
import { ref } from "vue";
import AppLayout from "@/Layouts/AppLayout.vue";
import DataTableClient from "@/Components/DataTable/DataTableClient.vue";
import DataTableExample from "../Examples/DataTableExample.vue";
const props = defineProps({
example: { type: String, default: "Demo" },
@@ -55,45 +56,7 @@ function onRowClick(row) {
</script>
<template>
<AppLayout title="Testing Sandbox">
<div class="space-y-6 p-6">
<div class="prose max-w-none">
<h1 class="text-2xl font-semibold">Testing Page</h1>
<p>
This page is for quick UI or component experiments. Remove or adapt as needed.
</p>
<p class="text-slate-700 text-sm">
Prop example value: <span class="font-mono">{{ props.example }}</span>
</p>
</div>
<div
class="rounded-lg border border-slate-200 bg-white/70 p-4 shadow-sm"
>
<h2
class="text-sm font-semibold tracking-wide uppercase text-slate-500 mb-3"
>
DataTable (Client-side)
</h2>
<DataTableClient
:columns="columns"
:rows="rows"
v-model:sort="sort"
v-model:search="search"
v-model:page="page"
v-model:pageSize="pageSize"
:search-keys="searchKeys"
@row:click="onRowClick"
>
<template #actions="{ row }">
<button
class="px-2 py-1 rounded border border-gray-300 hover:bg-gray-50 text-xs"
>
Akcija
</button>
</template>
</DataTableClient>
</div>
</div>
</AppLayout>
<DataTableExample></DataTableExample>
</template>