This commit is contained in:
Simon Pocrnjič
2025-12-26 22:39:58 +01:00
parent f8623a6071
commit dea7432deb
55 changed files with 7977 additions and 1983 deletions
@@ -5,6 +5,9 @@ import {
BeakerIcon,
ArrowDownOnSquareIcon,
} from "@heroicons/vue/24/outline";
import { Button } from '@/Components/ui/button';
import { Badge } from '@/Components/ui/badge';
const props = defineProps({
importId: [Number, String],
isCompleted: Boolean,
@@ -17,47 +20,50 @@ const emits = defineEmits(["preview", "save-mappings", "process-import", "simula
</script>
<template>
<div class="flex flex-wrap gap-2 items-center" v-if="!isCompleted">
<button
<Button
variant="secondary"
@click.prevent="$emit('preview')"
:disabled="!importId"
class="px-4 py-2 bg-gray-600 disabled:bg-gray-300 text-white rounded flex items-center gap-2"
>
<EyeIcon class="h-4 w-4" />
<EyeIcon class="h-4 w-4 mr-2" />
Predogled vrstic
</button>
<button
</Button>
<Button
variant="default"
class="bg-orange-600 hover:bg-orange-700"
@click.prevent="$emit('save-mappings')"
:disabled="!importId || processing || savingMappings || isCompleted"
class="px-4 py-2 bg-orange-600 disabled:bg-gray-300 text-white rounded flex items-center gap-2"
title="Shrani preslikave za ta uvoz"
>
<span
v-if="savingMappings"
class="inline-block h-4 w-4 border-2 border-white/70 border-t-transparent rounded-full animate-spin"
class="inline-block h-4 w-4 mr-2 border-2 border-white/70 border-t-transparent rounded-full animate-spin"
></span>
<ArrowPathIcon v-else class="h-4 w-4" />
<ArrowPathIcon v-else class="h-4 w-4 mr-2" />
<span>Shrani preslikave</span>
<span
<Badge
v-if="selectedMappingsCount"
class="ml-1 text-xs bg-white/20 px-1.5 py-0.5 rounded"
>{{ selectedMappingsCount }}</span
>
</button>
<button
variant="secondary"
class="ml-2 text-xs"
>{{ selectedMappingsCount }}</Badge>
</Button>
<Button
variant="default"
class="bg-purple-600 hover:bg-purple-700"
@click.prevent="$emit('process-import')"
:disabled="!canProcess"
class="px-4 py-2 bg-purple-600 disabled:bg-gray-300 text-white rounded flex items-center gap-2"
>
<BeakerIcon class="h-4 w-4" />
<BeakerIcon class="h-4 w-4 mr-2" />
{{ processing ? "Obdelava…" : "Obdelaj uvoz" }}
</button>
<button
</Button>
<Button
variant="default"
class="bg-blue-600 hover:bg-blue-700"
@click.prevent="$emit('simulate')"
:disabled="!importId || processing"
class="px-4 py-2 bg-blue-600 disabled:bg-gray-300 text-white rounded flex items-center gap-2"
>
<ArrowDownOnSquareIcon class="h-4 w-4" />
<ArrowDownOnSquareIcon class="h-4 w-4 mr-2" />
Simulacija vnosa
</button>
</Button>
</div>
</template>
@@ -1,16 +1,21 @@
<script setup>
import { CheckCircleIcon } from '@heroicons/vue/24/solid'
import { Badge } from '@/Components/ui/badge'
const props = defineProps({ steps: Array, missingCritical: Array })
</script>
<template>
<div class="bg-gray-50 border rounded p-3 text-xs flex flex-col gap-1 h-fit">
<div class="font-semibold text-gray-700 mb-1">Kontrolni seznam</div>
<div v-for="s in steps" :key="s.label" class="flex items-center gap-2" :class="s.done ? 'text-emerald-700' : 'text-gray-500'">
<div class="bg-muted/50 border rounded-lg p-4 text-xs flex flex-col gap-2 h-fit">
<div class="font-semibold text-foreground mb-1">Kontrolni seznam</div>
<div v-for="s in steps" :key="s.label" class="flex items-center gap-2" :class="s.done ? 'text-emerald-700' : 'text-muted-foreground'">
<CheckCircleIcon v-if="s.done" class="h-4 w-4 text-emerald-600" />
<span v-else class="h-4 w-4 rounded-full border border-gray-300 inline-block"></span>
<span v-else class="h-4 w-4 rounded-full border-2 border-muted-foreground/30 inline-block"></span>
<span>{{ s.label }}</span>
</div>
<div v-if="missingCritical?.length" class="mt-2 text-red-600 font-medium">Manjkajo kritične: {{ missingCritical.join(', ') }}</div>
<div v-else class="mt-2 text-emerald-600">Kritične preslikave prisotne</div>
<div v-if="missingCritical?.length" class="mt-2">
<Badge variant="destructive" class="text-[10px]">Manjkajo kritične: {{ missingCritical.join(', ') }}</Badge>
</div>
<div v-else class="mt-2">
<Badge variant="default" class="text-[10px] bg-emerald-600">Kritične preslikave prisotne</Badge>
</div>
</div>
</template>
@@ -1,5 +1,10 @@
<script setup>
import Modal from '@/Components/Modal.vue'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
import { Button } from "@/Components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/Components/ui/table";
import { Badge } from "@/Components/ui/badge";
import { Label } from "@/Components/ui/label";
const props = defineProps({
show: Boolean,
limit: Number,
@@ -13,49 +18,69 @@ const emits = defineEmits(['close','change-limit','refresh'])
function onLimit(e){ emits('change-limit', Number(e.target.value)); emits('refresh') }
</script>
<template>
<Modal :show="show" max-width="wide" @close="$emit('close')">
<div class="p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold text-lg">CSV Preview ({{ rows.length }} / {{ limit }})</h3>
<button class="text-sm px-2 py-1 rounded border" @click="$emit('close')">Close</button>
</div>
<div class="mb-2 flex items-center gap-3 text-sm">
<div>
<label class="mr-1 text-gray-600">Limit:</label>
<select :value="limit" class="border rounded p-1" @change="onLimit">
<option :value="50">50</option>
<option :value="100">100</option>
<option :value="200">200</option>
<option :value="300">300</option>
<option :value="500">500</option>
</select>
<Dialog :open="show" @update:open="(val) => !val && $emit('close')">
<DialogContent class="max-w-6xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>CSV Preview ({{ rows.length }} / {{ limit }})</DialogTitle>
</DialogHeader>
<div class="flex items-center gap-3 pb-3 border-b">
<div class="flex items-center gap-2">
<Label for="limit-select" class="text-sm text-gray-600">Limit:</Label>
<Select :model-value="String(limit)" @update:model-value="(val) => { emits('change-limit', Number(val)); emits('refresh'); }">
<SelectTrigger id="limit-select" class="w-24 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
<SelectItem value="200">200</SelectItem>
<SelectItem value="300">300</SelectItem>
<SelectItem value="500">500</SelectItem>
</SelectContent>
</Select>
</div>
<button @click="$emit('refresh')" class="px-2 py-1 border rounded" :disabled="loading">{{ loading ? 'Loading…' : 'Refresh' }}</button>
<span v-if="truncated" class="text-xs text-amber-600">Truncated at limit</span>
<Button @click="$emit('refresh')" variant="outline" size="sm" :disabled="loading">
{{ loading ? 'Loading…' : 'Refresh' }}
</Button>
<Badge v-if="truncated" variant="outline" class="bg-amber-50 text-amber-700 border-amber-200">
Truncated at limit
</Badge>
</div>
<div class="overflow-auto max-h-[60vh] border rounded">
<table class="min-w-full text-xs">
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="p-2 border bg-white">#</th>
<th v-for="col in columns" :key="col" class="p-2 border text-left">{{ col }}</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td :colspan="columns.length + 1" class="p-4 text-center text-gray-500">Loading</td>
</tr>
<tr v-for="(r, idx) in rows" :key="idx" class="border-t hover:bg-gray-50">
<td class="p-2 border text-gray-500">{{ idx + 1 }}</td>
<td v-for="col in columns" :key="col" class="p-2 border whitespace-pre-wrap">{{ r[col] }}</td>
</tr>
<tr v-if="!loading && !rows.length">
<td :colspan="columns.length + 1" class="p-4 text-center text-gray-500">No rows</td>
</tr>
</tbody>
</table>
<div class="flex-1 overflow-auto border rounded-lg">
<Table>
<TableHeader class="sticky top-0 bg-white z-10">
<TableRow>
<TableHead class="w-16">#</TableHead>
<TableHead v-for="col in columns" :key="col">{{ col }}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="loading">
<TableCell :colspan="columns.length + 1" class="text-center text-gray-500">
Loading…
</TableCell>
</TableRow>
<TableRow v-for="(r, idx) in rows" :key="idx">
<TableCell class="text-gray-500 font-medium">{{ idx + 1 }}</TableCell>
<TableCell v-for="col in columns" :key="col" class="whitespace-pre-wrap">
{{ r[col] }}
</TableCell>
</TableRow>
<TableRow v-if="!loading && !rows.length">
<TableCell :colspan="columns.length + 1" class="text-center text-gray-500">
No rows
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<p class="mt-2 text-xs text-gray-500">Showing up to {{ limit }} rows from source file. Header detection: {{ hasHeader ? 'header present' : 'no header' }}.</p>
</div>
</Modal>
<div class="text-xs text-gray-500 pt-3 border-t">
Showing up to {{ limit }} rows from source file.
Header detection: <span class="font-medium">{{ hasHeader ? 'header present' : 'no header' }}</span>
</div>
</DialogContent>
</Dialog>
</template>
@@ -0,0 +1,63 @@
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
import { Button } from "@/Components/ui/button";
import { Badge } from "@/Components/ui/badge";
const props = defineProps({
show: { type: Boolean, default: false },
contracts: { type: Array, default: () => [] },
});
const emit = defineEmits(["close"]);
</script>
<template>
<Dialog :open="show" @update:open="(val) => !val && emit('close')">
<DialogContent class="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>Obstoječe pogodbe najdene v zgodovinskem uvozu</DialogTitle>
</DialogHeader>
<div class="flex-1 overflow-auto">
<div v-if="!contracts.length" class="py-12 text-center">
<p class="text-sm text-gray-500">Ni zadetkov.</p>
</div>
<div v-else class="divide-y">
<div
v-for="item in contracts"
:key="item.contract_uuid || item.reference"
class="p-4 hover:bg-gray-50 transition-colors"
>
<div class="flex items-start justify-between gap-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<code class="text-sm font-medium text-gray-900">{{ item.reference }}</code>
<Badge variant="outline" class="text-[10px]">Najdena</Badge>
</div>
<div class="text-xs text-gray-600">
<span>{{ item.full_name || "—" }}</span>
</div>
</div>
<Button
v-if="item.case_uuid"
variant="outline"
size="sm"
as="a"
:href="route('clientCase.show', { client_case: item.case_uuid })"
class="shrink-0"
>
Odpri primer
</Button>
</div>
</div>
</div>
</div>
<div class="border-t pt-4 flex justify-end">
<Button variant="secondary" @click="emit('close')">Zapri</Button>
</div>
</DialogContent>
</Dialog>
</template>
+184 -63
View File
@@ -1,6 +1,12 @@
<script setup>
import { ref, computed } from "vue";
import Dropdown from "@/Components/Dropdown.vue";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/Components/ui/table';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/Components/ui/select';
import { Button } from '@/Components/ui/button';
import { Badge } from '@/Components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/Components/ui/dialog';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/Components/ui/accordion';
const props = defineProps({
events: Array,
@@ -8,8 +14,8 @@ const props = defineProps({
limit: Number,
});
const emits = defineEmits(["update:limit", "refresh"]);
function onLimit(e) {
emits("update:limit", Number(e.target.value));
function onLimit(val) {
emits("update:limit", Number(val));
emits("refresh");
}
@@ -46,6 +52,32 @@ function toggleExpand(id) {
expanded.value = new Set(expanded.value);
}
// Entity details dialog
const detailsDialog = ref(false);
const selectedEvent = ref(null);
function hasEntityDetails(ev) {
const ctx = tryJson(ev.context);
return ctx && Array.isArray(ctx.entity_details) && ctx.entity_details.length > 0;
}
function showEntityDetails(ev) {
selectedEvent.value = ev;
detailsDialog.value = true;
}
function getEntityDetails(ev) {
if (!ev) return [];
const ctx = tryJson(ev.context);
return ctx?.entity_details || [];
}
function getRawData(ev) {
if (!ev) return {};
const ctx = tryJson(ev.context);
return ctx?.raw_data || {};
}
function isLong(msg) {
return msg && String(msg).length > 160;
}
@@ -138,68 +170,72 @@ function formattedContext(ctx) {
<div class="flex items-center justify-between mb-2">
<h3 class="font-semibold">Logs</h3>
<div class="flex items-center flex-wrap gap-2 text-sm">
<label class="text-gray-600">Show</label>
<select :value="limit" class="border rounded p-1" @change="onLimit">
<option :value="50">50</option>
<option :value="100">100</option>
<option :value="200">200</option>
<option :value="500">500</option>
</select>
<label class="text-gray-600 ml-2">Level</label>
<select v-model="levelFilter" class="border rounded p-1">
<option v-for="opt in levelOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
<button
<span class="text-muted-foreground">Show</span>
<Select :model-value="limit.toString()" @update:model-value="onLimit">
<SelectTrigger class="w-20 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
<SelectItem value="200">200</SelectItem>
<SelectItem value="500">500</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<span class="text-muted-foreground ml-2">Level</span>
<Select v-model="levelFilter">
<SelectTrigger class="w-32 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="opt in levelOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
@click.prevent="$emit('refresh')"
class="px-2 py-1 border rounded text-sm"
:disabled="loading"
>
{{ loading ? "Refreshing…" : "Refresh" }}
</button>
</Button>
</div>
</div>
<div class="overflow-x-auto max-h-[30rem] overflow-y-auto rounded border">
<table class="min-w-full bg-white text-sm table-fixed">
<colgroup>
<col class="w-40" />
<col class="w-20" />
<col class="w-40" />
<col />
<col class="w-16" />
</colgroup>
<thead class="bg-gray-50 sticky top-0 z-10 shadow">
<tr class="text-left text-xs uppercase text-gray-600">
<th class="p-2 border">Time</th>
<th class="p-2 border">Level</th>
<th class="p-2 border">Event</th>
<th class="p-2 border">Message</th>
<th class="p-2 border">Row</th>
</tr>
</thead>
<tbody>
<tr v-for="ev in filteredEvents" :key="ev.id" class="border-t align-top">
<td class="p-2 border whitespace-nowrap">
<div class="overflow-x-auto max-h-[30rem] overflow-y-auto rounded-lg border">
<Table>
<TableHeader class="sticky top-0 z-10">
<TableRow>
<TableHead class="w-[160px]">Time</TableHead>
<TableHead class="w-[80px]">Level</TableHead>
<TableHead class="w-[160px]">Event</TableHead>
<TableHead>Message</TableHead>
<TableHead class="w-[64px]">Row</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="ev in filteredEvents" :key="ev.id">
<TableCell class="whitespace-nowrap">
{{ new Date(ev.created_at).toLocaleString() }}
</td>
<td class="p-2 border">
<span
</TableCell>
<TableCell>
<Badge
:variant="ev.level === 'error' ? 'destructive' : ev.level === 'warning' ? 'default' : 'secondary'"
:class="[
'px-2 py-0.5 rounded text-xs',
ev.level === 'error'
? 'bg-red-100 text-red-800'
: ev.level === 'warning'
? 'bg-amber-100 text-amber-800'
: 'bg-gray-100 text-gray-700',
'text-xs',
ev.level === 'warning' ? 'bg-amber-100 text-amber-800 hover:bg-amber-100' : ''
]"
>{{ ev.level }}</span
>
</td>
<td class="p-2 border break-words max-w-[9rem]">
>{{ ev.level }}</Badge>
</TableCell>
<TableCell class="max-w-[9rem]">
<span class="block truncate" :title="ev.event">{{ ev.event }}</span>
</td>
<td class="p-2 border align-top max-w-[28rem]">
</TableCell>
<TableCell class="max-w-[28rem]">
<div class="space-y-1 break-words">
<div class="leading-snug whitespace-pre-wrap">
<span v-if="!isLong(ev.message)">{{ ev.message }}</span>
@@ -215,7 +251,15 @@ function formattedContext(ctx) {
</button>
</span>
</div>
<div v-if="ev.context" class="text-xs text-gray-600">
<div v-if="ev.context" class="text-xs text-gray-600 flex items-center gap-2">
<button
v-if="hasEntityDetails(ev)"
type="button"
class="px-2 py-1 rounded border border-indigo-300 bg-indigo-50 hover:bg-indigo-100 text-indigo-700 transition text-[11px] font-medium"
@click="showEntityDetails(ev)"
>
📋 Entity Details
</button>
<Dropdown
align="left"
width="wide"
@@ -255,14 +299,91 @@ function formattedContext(ctx) {
</Dropdown>
</div>
</div>
</td>
<td class="p-2 border">{{ ev.import_row_id ?? "—" }}</td>
</tr>
<tr v-if="!filteredEvents.length">
<td class="p-3 text-center text-gray-500" colspan="5">No events yet</td>
</tr>
</tbody>
</table>
</TableCell>
<TableCell>{{ ev.import_row_id ?? "—" }}</TableCell>
</TableRow>
<TableRow v-if="!filteredEvents.length">
<TableCell colspan="5" class="text-center text-muted-foreground">No events yet</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<!-- Entity Details Dialog -->
<Dialog v-model:open="detailsDialog">
<DialogContent class="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Entity Processing Details</DialogTitle>
<DialogDescription v-if="selectedEvent">
Row {{ tryJson(selectedEvent.context)?.row || '' }} - {{ selectedEvent.event }}
</DialogDescription>
</DialogHeader>
<div v-if="selectedEvent" class="space-y-3 mt-4">
<div
v-for="(detail, idx) in getEntityDetails(selectedEvent)"
:key="idx"
class="p-3 rounded-lg border"
:class="{
'bg-red-50 border-red-200': detail.level === 'error',
'bg-amber-50 border-amber-200': detail.level === 'warning',
'bg-green-50 border-green-200': detail.level === 'info' && detail.action === 'inserted',
'bg-blue-50 border-blue-200': detail.level === 'info' && detail.action === 'updated',
'bg-gray-50 border-gray-200': detail.level === 'info' && detail.action === 'skipped'
}"
>
<div class="flex items-start justify-between mb-2">
<div class="font-medium text-sm capitalize">{{ detail.entity }}</div>
<Badge
:variant="detail.level === 'error' ? 'destructive' : detail.level === 'warning' ? 'default' : 'secondary'"
:class="[
'text-xs',
detail.level === 'warning' ? 'bg-amber-100 text-amber-800 hover:bg-amber-100' : '',
detail.action === 'inserted' ? 'bg-green-100 text-green-800 hover:bg-green-100' : '',
detail.action === 'updated' ? 'bg-blue-100 text-blue-800 hover:bg-blue-100' : '',
detail.action === 'skipped' ? 'bg-gray-200 text-gray-700 hover:bg-gray-200' : ''
]"
>
{{ detail.action }}{{ detail.count > 1 ? ` (${detail.count})` : '' }}
</Badge>
</div>
<div v-if="detail.message" class="text-sm text-gray-700 mb-1">
{{ detail.message }}
</div>
<div v-if="detail.errors && detail.errors.length" class="mt-2 space-y-1">
<div class="text-xs font-medium text-red-700">Errors:</div>
<div
v-for="(err, errIdx) in detail.errors"
:key="errIdx"
class="text-xs text-red-600 pl-3"
>
{{ err }}
</div>
</div>
<div v-if="detail.exception" class="mt-2 p-2 bg-red-100 rounded border border-red-200">
<div class="text-xs font-semibold text-red-800 mb-1">Exception:</div>
<div class="text-xs text-red-700">{{ detail.exception.message }}</div>
<div v-if="detail.exception.file" class="text-xs text-red-600 mt-1">
{{ detail.exception.file }}:{{ detail.exception.line }}
</div>
</div>
</div>
<div v-if="getEntityDetails(selectedEvent).length === 0" class="text-center text-muted-foreground py-4">
No entity details available
</div>
<!-- Raw Row Data Accordion -->
<Accordion type="single" collapsible class="mt-4 border-t pt-4">
<AccordionItem value="raw-data" class="border-b-0">
<AccordionTrigger class="text-sm font-medium hover:no-underline py-2">
📄 Raw Row Data (JSON)
</AccordionTrigger>
<AccordionContent>
<pre class="text-xs bg-gray-900 text-gray-100 p-3 rounded overflow-x-auto mt-2">{{ JSON.stringify(getRawData(selectedEvent), null, 2) }}</pre>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</DialogContent>
</Dialog>
</div>
</template>
@@ -1,4 +1,11 @@
<script setup>
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/Components/ui/table';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/Components/ui/select';
import { Checkbox } from '@/Components/ui/checkbox';
import { Input } from '@/Components/ui/input';
import { Badge } from '@/Components/ui/badge';
import { ScrollArea } from '@/Components/ui/scroll-area';
const props = defineProps({
rows: Array,
entityOptions: Array,
@@ -17,97 +24,145 @@ const emits = defineEmits(['update:rows','save'])
function duplicateTarget(row){
if(!row || !row.entity || !row.field) return false
// parent already marks duplicates in duplicateTargets set keyed as record.field
return props.duplicateTargets?.has?.(row.entity + '.' + row.field) || false
}
</script>
<template>
<div v-if="show && rows?.length" class="pt-4">
<h3 class="font-semibold mb-2">
Detected Columns ({{ detected?.has_header ? 'header' : 'positional' }})
<span class="ml-2 text-xs text-gray-500">detected: {{ detected?.columns?.length || 0 }}, rows: {{ rows.length }}, delimiter: {{ detected?.delimiter || 'auto' }}</span>
</h3>
<p v-if="detectedNote" class="text-xs text-gray-500 mb-2">{{ detectedNote }}</p>
<div class="relative border rounded overflow-auto max-h-[420px]">
<table class="min-w-full bg-white">
<thead class="sticky top-0 z-10">
<tr class="bg-gray-50/95 backdrop-blur text-left text-xs uppercase text-gray-600">
<th class="p-2 border">Source column</th>
<th class="p-2 border">Entity</th>
<th class="p-2 border">Field</th>
<th class="p-2 border">Meta key</th>
<th class="p-2 border">Meta type</th>
<th class="p-2 border">Transform</th>
<th class="p-2 border">Apply mode</th>
<th class="p-2 border">Skip</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in rows" :key="idx" class="border-t" :class="duplicateTarget(row) ? 'bg-red-50' : ''">
<td class="p-2 border text-sm">{{ row.source_column }}</td>
<td class="p-2 border">
<select v-model="row.entity" class="border rounded p-1 w-full" :disabled="isCompleted">
<option value=""></option>
<option v-for="opt in entityOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
</select>
</td>
<td class="p-2 border">
<select v-model="row.field" :class="['border rounded p-1 w-full', duplicateTarget(row) ? 'border-red-500 bg-red-50' : '']" :disabled="isCompleted">
<option value=""></option>
<option v-for="f in fieldsForEntity(row.entity)" :key="f" :value="f">{{ f }}</option>
</select>
</td>
<td class="p-2 border">
<input
<div class="flex items-center justify-between mb-2">
<h3 class="font-semibold">
Detected Columns
<Badge variant="outline" class="ml-2 text-[10px]">{{ detected?.has_header ? 'header' : 'positional' }}</Badge>
</h3>
<div class="text-xs text-muted-foreground">
detected: {{ detected?.columns?.length || 0 }}, rows: {{ rows.length }}, delimiter: {{ detected?.delimiter || 'auto' }}
</div>
</div>
<p v-if="detectedNote" class="text-xs text-muted-foreground mb-2">{{ detectedNote }}</p>
<div class="relative border rounded-lg">
<ScrollArea class="h-[420px]">
<Table>
<TableHeader class="sticky top-0 z-10 bg-background">
<TableRow class="hover:bg-transparent">
<TableHead class="w-[180px] bg-muted/95 backdrop-blur">Source column</TableHead>
<TableHead class="w-[150px] bg-muted/95 backdrop-blur">Entity</TableHead>
<TableHead class="w-[150px] bg-muted/95 backdrop-blur">Field</TableHead>
<TableHead class="w-[140px] bg-muted/95 backdrop-blur">Meta key</TableHead>
<TableHead class="w-[120px] bg-muted/95 backdrop-blur">Meta type</TableHead>
<TableHead class="w-[120px] bg-muted/95 backdrop-blur">Transform</TableHead>
<TableHead class="w-[130px] bg-muted/95 backdrop-blur">Apply mode</TableHead>
<TableHead class="w-[60px] text-center bg-muted/95 backdrop-blur">Skip</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="(row, idx) in rows" :key="idx" :class="duplicateTarget(row) ? 'bg-destructive/10' : ''">
<TableCell class="font-medium">{{ row.source_column }}</TableCell>
<TableCell>
<Select :model-value="row.entity || ''" @update:model-value="(val) => row.entity = val || ''" :disabled="isCompleted">
<SelectTrigger class="h-8 text-xs">
<SelectValue placeholder="Select entity..." />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="opt in entityOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Select
:model-value="row.field || ''"
@update:model-value="(val) => row.field = val || ''"
:disabled="isCompleted"
:class="duplicateTarget(row) ? 'border-destructive' : ''"
>
<SelectTrigger class="h-8 text-xs" :class="duplicateTarget(row) ? 'border-destructive bg-destructive/10' : ''">
<SelectValue placeholder="Select field..." />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="f in fieldsForEntity(row.entity)" :key="f" :value="f">{{ f }}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Input
v-if="row.field === 'meta'"
v-model="(row.options ||= {}).key"
type="text"
class="border rounded p-1 w-full"
class="h-8 text-xs"
placeholder="e.g. monthly_rent"
:disabled="isCompleted"
/>
<span v-else class="text-gray-400 text-xs"></span>
</td>
<td class="p-2 border">
<select
<span v-else class="text-muted-foreground text-xs">—</span>
</TableCell>
<TableCell>
<Select
v-if="row.field === 'meta'"
v-model="(row.options ||= {}).type"
class="border rounded p-1 w-full"
:model-value="(row.options ||= {}).type || 'string'"
@update:model-value="(val) => (row.options ||= {}).type = val"
:disabled="isCompleted"
>
<option :value="null">Default (string)</option>
<option value="string">string</option>
<option value="number">number</option>
<option value="date">date</option>
<option value="boolean">boolean</option>
</select>
<span v-else class="text-gray-400 text-xs"></span>
</td>
<td class="p-2 border">
<select v-model="row.transform" class="border rounded p-1 w-full" :disabled="isCompleted">
<option value="">None</option>
<option value="trim">Trim</option>
<option value="upper">Uppercase</option>
<option value="lower">Lowercase</option>
</select>
</td>
<td class="p-2 border">
<select v-model="row.apply_mode" class="border rounded p-1 w-full" :disabled="isCompleted">
<option value="keyref">Keyref</option>
<option value="both">Both</option>
<option value="insert">Insert only</option>
<option value="update">Update only</option>
</select>
</td>
<td class="p-2 border text-center">
<input type="checkbox" v-model="row.skip" :disabled="isCompleted" />
</td>
</tr>
</tbody>
</table>
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="string">string</SelectItem>
<SelectItem value="number">number</SelectItem>
<SelectItem value="date">date</SelectItem>
<SelectItem value="boolean">boolean</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<span v-else class="text-muted-foreground text-xs">—</span>
</TableCell>
<TableCell>
<Select :model-value="row.transform || 'none'" @update:model-value="(val) => row.transform = val === 'none' ? '' : val" :disabled="isCompleted">
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="none">None</SelectItem>
<SelectItem value="trim">Trim</SelectItem>
<SelectItem value="upper">Uppercase</SelectItem>
<SelectItem value="lower">Lowercase</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Select :model-value="row.apply_mode || 'both'" @update:model-value="(val) => row.apply_mode = val" :disabled="isCompleted">
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="keyref">Keyref</SelectItem>
<SelectItem value="both">Both</SelectItem>
<SelectItem value="insert">Insert only</SelectItem>
<SelectItem value="update">Update only</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</TableCell>
<TableCell class="text-center">
<Checkbox :checked="row.skip" @update:checked="(val) => row.skip = val" :disabled="isCompleted" />
</TableCell>
</TableRow>
</TableBody>
</Table>
</ScrollArea>
</div>
<div v-if="mappingSaved" class="text-sm text-emerald-700 mt-2 flex items-center gap-2">
<Badge variant="default" class="bg-emerald-600">Saved</Badge>
<span>{{ mappingSavedCount }} mappings saved</span>
</div>
<div v-else-if="mappingError" class="text-sm text-destructive mt-2">{{ mappingError }}</div>
<div v-if="missingCritical?.length" class="mt-2">
<Badge variant="destructive" class="text-xs">Missing critical: {{ missingCritical.join(', ') }}</Badge>
</div>
<div v-if="mappingSaved" class="text-sm text-emerald-700 mt-2">Mappings saved ({{ mappingSavedCount }}).</div>
<div v-else-if="mappingError" class="text-sm text-red-600 mt-2">{{ mappingError }}</div>
<div v-if="missingCritical?.length" class="text-xs text-amber-600 mt-1">Missing critical: {{ missingCritical.join(', ') }}</div>
</div>
</template>
@@ -0,0 +1,78 @@
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
import { Button } from "@/Components/ui/button";
import { Badge } from "@/Components/ui/badge";
import { Skeleton } from "@/Components/ui/skeleton";
const props = defineProps({
show: { type: Boolean, default: false },
loading: { type: Boolean, default: false },
contracts: { type: Array, default: () => [] },
formatMoney: { type: Function, required: true },
});
const emit = defineEmits(["close"]);
</script>
<template>
<Dialog :open="show" @update:open="(val) => !val && emit('close')">
<DialogContent class="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>Manjkajoče pogodbe (aktivne, ne-arhivirane)</DialogTitle>
</DialogHeader>
<div class="flex-1 overflow-auto">
<div v-if="loading" class="space-y-3 p-4">
<Skeleton v-for="i in 5" :key="i" class="h-16 w-full" />
</div>
<div v-else-if="!contracts.length" class="py-12 text-center">
<p class="text-sm text-gray-500">Ni zadetkov.</p>
</div>
<div v-else class="divide-y">
<div
v-for="row in contracts"
:key="row.uuid"
class="p-4 hover:bg-gray-50 transition-colors"
>
<div class="flex items-start justify-between gap-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<code class="text-sm font-medium text-gray-900">{{
row.reference
}}</code>
<Badge variant="secondary" class="text-[10px]">Aktivna</Badge>
</div>
<div class="text-xs text-gray-600 space-y-0.5">
<div class="flex items-center gap-2">
<span class="font-medium">Primer:</span>
<span class="truncate">{{ row.full_name || "—" }}</span>
</div>
<div v-if="row.balance_amount != null" class="flex items-center gap-2">
<span class="font-medium">Stanje:</span>
<span class="font-mono">{{ formatMoney(row.balance_amount) }}</span>
</div>
</div>
</div>
<Button
variant="outline"
size="sm"
as="a"
:href="route('clientCase.show', { client_case: row.case_uuid })"
class="shrink-0"
>
Odpri primer
</Button>
</div>
</div>
</div>
</div>
<div class="border-t pt-4 flex justify-end">
<Button variant="secondary" @click="emit('close')">Zapri</Button>
</div>
</DialogContent>
</Dialog>
</template>
@@ -1,9 +1,14 @@
<script setup>
const props = defineProps({ result: [String, Object] })
import { Badge } from "@/Components/ui/badge";
const props = defineProps({ result: [String, Object] });
</script>
<template>
<div v-if="result" class="pt-4">
<h3 class="font-semibold mb-2">Import Result</h3>
<pre class="bg-gray-50 border rounded p-3 text-sm overflow-x-auto">{{ result }}</pre>
<div class="flex items-center gap-2 mb-2">
<h3 class="font-semibold">Import Result</h3>
<Badge variant="default" class="bg-emerald-600">Complete</Badge>
</div>
<pre class="bg-muted border rounded-lg p-4 text-sm overflow-x-auto">{{ result }}</pre>
</div>
</template>
@@ -1,44 +1,53 @@
<script setup>
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/Components/ui/table';
import { Badge } from '@/Components/ui/badge';
const props = defineProps({ mappings: Array });
</script>
<template>
<div v-if="mappings?.length" class="pt-4">
<h3 class="font-semibold mb-2">Current Saved Mappings</h3>
<div class="overflow-x-auto">
<table class="min-w-full border bg-white text-sm">
<thead>
<tr class="bg-gray-50 text-left text-xs uppercase text-gray-600">
<th class="p-2 border">Source column</th>
<th class="p-2 border">Target field</th>
<th class="p-2 border">Transform</th>
<th class="p-2 border">Mode</th>
<th class="p-2 border">Options</th>
</tr>
</thead>
<tbody>
<tr
<div class="overflow-x-auto rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Source column</TableHead>
<TableHead>Target field</TableHead>
<TableHead>Transform</TableHead>
<TableHead>Mode</TableHead>
<TableHead>Options</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="m in mappings"
:key="m.id || m.source_column + m.target_field"
class="border-t"
>
<td class="p-2 border">{{ m.source_column }}</td>
<td class="p-2 border">{{ m.target_field }}</td>
<td class="p-2 border">{{ m.transform || "—" }}</td>
<td class="p-2 border">{{ m.apply_mode || "both" }}</td>
<td class="p-2 border">
<TableCell class="font-medium">{{ m.source_column }}</TableCell>
<TableCell>{{ m.target_field }}</TableCell>
<TableCell>
<Badge v-if="m.transform" variant="outline" class="text-xs">{{ m.transform }}</Badge>
<span v-else class="text-muted-foreground"></span>
</TableCell>
<TableCell>
<Badge variant="secondary" class="text-xs">{{ m.apply_mode || "both" }}</Badge>
</TableCell>
<TableCell>
<template v-if="m.options">
<span v-if="m.options.key" class="inline-block mr-2"
>key: <strong>{{ m.options.key }}</strong></span
>
<span v-if="m.options.type" class="inline-block"
>type: <strong>{{ m.options.type }}</strong></span
>
<div class="flex flex-wrap gap-1">
<Badge v-if="m.options.key" variant="outline" class="text-[10px]">
key: {{ m.options.key }}
</Badge>
<Badge v-if="m.options.type" variant="outline" class="text-[10px]">
type: {{ m.options.type }}
</Badge>
</div>
</template>
<span v-else></span>
</td>
</tr>
</tbody>
</table>
<span v-else class="text-muted-foreground"></span>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
</template>
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,10 @@
<script setup>
import Multiselect from "vue-multiselect";
import { computed } from "vue";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
import { Button } from "@/Components/ui/button";
import { Label } from "@/Components/ui/label";
import { Input } from "@/Components/ui/input";
import { Badge } from "@/Components/ui/badge";
const props = defineProps({
isCompleted: Boolean,
@@ -19,11 +23,11 @@ const emits = defineEmits([
"preview",
]);
function onHeaderChange(e) {
emits("update:hasHeader", e.target.value === "true");
function onHeaderChange(val) {
emits("update:hasHeader", val === "true");
}
function onDelimiterMode(e) {
emits("update:delimiterMode", e.target.value);
function onDelimiterMode(val) {
emits("update:delimiterMode", val);
}
function onDelimiterCustom(e) {
emits("update:delimiterCustom", e.target.value);
@@ -44,116 +48,119 @@ const selectedTemplateProxy = computed({
<div class="space-y-4">
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700">Template</label>
<Multiselect
v-model="selectedTemplateProxy"
:options="filteredTemplates"
track-by="id"
label="name"
placeholder="Izberi predlogo..."
:searchable="true"
:allow-empty="true"
class="mt-1"
:custom-label="(o) => o.name"
:disabled="filteredTemplates?.length === 0"
:show-no-results="true"
:clear-on-select="false"
<Label class="text-sm font-medium">Template</Label>
<Select
:model-value="selectedTemplateProxy?.id?.toString()"
@update:model-value="(val) => {
const tpl = filteredTemplates.find(t => t.id.toString() === val);
selectedTemplateProxy = tpl || null;
}"
>
<template #option="{ option }">
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
<span>{{ option.name }}</span>
<span v-if="option.source_type" class="ml-2 text-xs text-gray-500"
>({{ option.source_type }})</span
>
<SelectTrigger class="mt-1">
<SelectValue placeholder="Izberi predlogo...">
<div v-if="selectedTemplateProxy" class="flex items-center gap-2">
<span>{{ selectedTemplateProxy.name }}</span>
<span v-if="selectedTemplateProxy.source_type" class="text-xs text-muted-foreground">({{ selectedTemplateProxy.source_type }})</span>
<Badge variant="outline" class="text-[10px]">{{ selectedTemplateProxy.client_id ? 'Client' : 'Global' }}</Badge>
</div>
<span class="text-[10px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">{{
option.client_id ? "Client" : "Global"
}}</span>
</div>
</template>
<template #singleLabel="{ option }">
<div class="flex items-center gap-2">
<span>{{ option.name }}</span>
<span v-if="option.source_type" class="ml-1 text-xs text-gray-500"
>({{ option.source_type }})</span
>
<span class="text-[10px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">{{
option.client_id ? "Client" : "Global"
}}</span>
</div>
</template>
<template #noResult>
<div class="px-2 py-1 text-xs text-gray-500">Ni predlog.</div>
</template>
</Multiselect>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="option in filteredTemplates" :key="option.id" :value="option.id.toString()">
<div class="flex items-center justify-between w-full gap-3">
<div class="flex items-center gap-2">
<span>{{ option.name }}</span>
<span v-if="option.source_type" class="text-xs text-muted-foreground">({{ option.source_type }})</span>
</div>
<Badge variant="outline" class="text-[10px]">{{
option.client_id ? "Client" : "Global"
}}</Badge>
</div>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div v-if="isCompleted" class="mt-2">
<button
type="button"
<Button
variant="default"
size="sm"
class="w-full sm:w-auto"
@click="$emit('preview')"
class="px-3 py-1.5 bg-indigo-600 text-white rounded text-sm hover:bg-indigo-500 w-full sm:w-auto"
>
Ogled CSV
</button>
</Button>
</div>
</div>
</div>
<div v-if="!isCompleted" class="flex flex-col gap-3">
<div class="flex flex-col sm:flex-row gap-3">
<div class="flex-1">
<label class="block text-xs font-medium text-gray-600">Header row</label>
<select
:value="hasHeader"
@change="onHeaderChange"
class="mt-1 block w-full border rounded p-2 text-sm"
<Label class="text-xs font-medium">Header row</Label>
<Select
:model-value="hasHeader.toString()"
@update:model-value="onHeaderChange"
>
<option value="true">Has header</option>
<option value="false">No header (positional)</option>
</select>
<SelectTrigger class="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="true">Has header</SelectItem>
<SelectItem value="false">No header (positional)</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div class="flex-1">
<label class="block text-xs font-medium text-gray-600">Delimiter</label>
<select
:value="delimiterState.mode"
@change="onDelimiterMode"
class="mt-1 block w-full border rounded p-2 text-sm"
<Label class="text-xs font-medium">Delimiter</Label>
<Select
:model-value="delimiterState.mode"
@update:model-value="onDelimiterMode"
>
<option value="auto">Auto-detect</option>
<option value="comma">Comma ,</option>
<option value="semicolon">Semicolon ;</option>
<option value="tab">Tab \t</option>
<option value="pipe">Pipe |</option>
<option value="space">Space </option>
<option value="custom">Custom</option>
</select>
<SelectTrigger class="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="auto">Auto-detect</SelectItem>
<SelectItem value="comma">Comma ,</SelectItem>
<SelectItem value="semicolon">Semicolon ;</SelectItem>
<SelectItem value="tab">Tab \t</SelectItem>
<SelectItem value="pipe">Pipe |</SelectItem>
<SelectItem value="space">Space </SelectItem>
<SelectItem value="custom">Custom</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
<div v-if="delimiterState.mode === 'custom'" class="flex items-end gap-3">
<div class="w-40">
<label class="block text-xs font-medium text-gray-600">Custom delimiter</label>
<input
:value="delimiterState.custom"
<Label class="text-xs font-medium">Custom delimiter</Label>
<Input
:model-value="delimiterState.custom"
@input="onDelimiterCustom"
maxlength="4"
placeholder=","
class="mt-1 block w-full border rounded p-2 text-sm"
class="mt-1"
/>
</div>
<p class="text-xs text-gray-500">
<p class="text-xs text-muted-foreground">
Template default: {{ selectedTemplateOption?.meta?.delimiter || "auto" }}
</p>
</div>
<p v-else class="text-xs text-gray-500">
<p v-else class="text-xs text-muted-foreground">
Template default: {{ selectedTemplateOption?.meta?.delimiter || "auto" }}
</p>
</div>
<button
v-if="!isCompleted"
class="px-3 py-1.5 bg-emerald-600 text-white rounded text-sm"
:disabled="!form.import_template_id"
<Button
v-if="!isCompleted && form.import_template_id"
variant="default"
@click="$emit('apply-template')"
class="w-full"
>
{{ templateApplied ? "Ponovno uporabi predlogo" : "Uporabi predlogo" }}
</button>
{{ templateApplied ? 'Re-apply Template' : 'Apply Template' }}
</Button>
</div>
</template>
@@ -0,0 +1,80 @@
<script setup>
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
import { Button } from "@/Components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/Components/ui/table";
import { Skeleton } from "@/Components/ui/skeleton";
import { ArrowDownTrayIcon } from "@heroicons/vue/24/outline";
const props = defineProps({
show: { type: Boolean, default: false },
loading: { type: Boolean, default: false },
columns: { type: Array, default: () => [] },
rows: { type: Array, default: () => [] },
importId: { type: Number, required: true },
});
const emit = defineEmits(["close"]);
function downloadCsv() {
if (!props.importId) return;
window.location.href = route("imports.missing-keyref-csv", { import: props.importId });
}
</script>
<template>
<Dialog :open="show" @update:open="(val) => !val && emit('close')">
<DialogContent class="max-w-6xl max-h-[85vh] overflow-hidden flex flex-col">
<DialogHeader>
<div class="flex items-center justify-between">
<DialogTitle>Vrstice z neobstoječim contract.reference (KEYREF)</DialogTitle>
<Button
variant="outline"
size="sm"
@click="downloadCsv"
class="gap-2"
>
<ArrowDownTrayIcon class="h-4 w-4" />
Prenesi CSV
</Button>
</div>
</DialogHeader>
<div class="flex-1 overflow-auto">
<div v-if="loading" class="space-y-3 p-4">
<Skeleton v-for="i in 10" :key="i" class="h-12 w-full" />
</div>
<div v-else-if="!rows.length" class="py-12 text-center">
<p class="text-sm text-gray-500">Ni zadetkov.</p>
</div>
<div v-else class="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-24"># vrstica</TableHead>
<TableHead v-for="(c, i) in columns" :key="i">{{ c }}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="r in rows" :key="r.id">
<TableCell class="font-medium text-gray-500">{{ r.row_number }}</TableCell>
<TableCell
v-for="(c, i) in columns"
:key="i"
class="whitespace-pre-wrap wrap-break-word"
>
{{ r.values?.[i] ?? "" }}
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
<div class="border-t pt-4 flex justify-end gap-2">
<Button variant="secondary" @click="emit('close')">Zapri</Button>
</div>
</DialogContent>
</Dialog>
</template>