Teren-app/resources/js/Pages/Imports/Partials/SimulationModal.vue
Simon Pocrnjič dea7432deb changes
2025-12-26 22:39:58 +01:00

646 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { Dialog, DialogContent } from "@/Components/ui/dialog";
import { Tabs, TabsList, TabsTrigger } from "@/Components/ui/tabs";
import { Badge } from "@/Components/ui/badge";
import { Button } from "@/Components/ui/button";
import { Checkbox } from "@/Components/ui/checkbox";
import { Label } from "@/Components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { useEurFormat } from "../useEurFormat.js";
import {
CheckCircleIcon,
ExclamationTriangleIcon,
XCircleIcon,
ChevronRightIcon,
} from "@heroicons/vue/24/outline";
import { computed, ref, watch } from "vue";
// Props
const props = defineProps({
show: { type: Boolean, default: false },
rows: { type: Array, default: () => [] },
limit: { type: Number, default: 50 },
loading: { type: Boolean, default: false },
entities: { type: Array, default: () => [] },
verbose: { type: Boolean, default: false },
});
// Emits
const emit = defineEmits(["close", "change-limit", "toggle-verbose"]);
// Entity labels
const entityLabelMap = {
account: "računi",
payment: "plačila",
contract: "pogodbe",
contracts: "pogodbe",
person: "osebe",
client_case: "primeri",
client_cases: "primeri",
address: "naslovi",
person_addresses: "naslovi",
email: "emaili",
emails: "emaili",
phone: "telefoni",
person_phones: "telefoni",
booking: "knjižbe",
activity: "aktivnosti",
activities: "aktivnosti",
};
// Formatting helpers
const { formatEur } = useEurFormat();
const fmt = (v) => formatEur(v);
// State
const activeEntity = ref(null);
const selectedRow = ref(null);
const showOnlyChanged = ref(false);
// Entities with data
const entitiesWithRows = computed(() => {
if (!props.rows?.length || !props.entities?.length) return [];
const present = new Set();
for (const r of props.rows) {
if (!r.entities) continue;
for (const k of Object.keys(r.entities)) {
if (props.entities.includes(k)) present.add(k);
}
}
return props.entities.filter((e) => present.has(e));
});
// Watch for entity changes
watch(
entitiesWithRows,
(val) => {
if (!val.length) {
activeEntity.value = null;
selectedRow.value = null;
return;
}
if (!activeEntity.value || !val.includes(activeEntity.value)) {
activeEntity.value = val[0];
selectedRow.value = null;
}
},
{ immediate: true }
);
// Entity statistics
const entityStats = computed(() => {
const stats = {};
for (const e of entitiesWithRows.value) {
stats[e] = {
total: 0,
create: 0,
update: 0,
invalid: 0,
errors: 0,
warnings: 0,
};
}
for (const r of props.rows || []) {
if (!r.entities) continue;
for (const [k, ent] of Object.entries(r.entities)) {
if (!stats[k]) continue;
// Handle both single entities and arrays
const entities = Array.isArray(ent) ? ent : [ent];
for (const e of entities) {
stats[k].total++;
if (e.action === "create") stats[k].create++;
if (e.action === "update") stats[k].update++;
if (e.action === "invalid") stats[k].invalid++;
}
if (r.errors?.length) stats[k].errors++;
if (r.warnings?.length) stats[k].warnings++;
}
}
return stats;
});
// Visible rows
const visibleRows = computed(() => {
if (!props.rows || !activeEntity.value) return [];
return props.rows
.filter((r) => {
if (!r.entities || !r.entities[activeEntity.value]) return false;
if (showOnlyChanged.value) {
const ent = r.entities[activeEntity.value];
const entities = Array.isArray(ent) ? ent : [ent];
return entities.some((e) => e.action === "create" || e.action === "update");
}
return true;
})
.map((r, idx) => ({ ...r, index: r.row_number || idx + 1 }))
.slice(0, props.limit || props.rows.length);
});
// Row status
function getRowStatus(row) {
if (!row.entities || !activeEntity.value) return "unknown";
const ent = row.entities[activeEntity.value];
const entities = Array.isArray(ent) ? ent : [ent];
if (row.errors?.length || entities.some((e) => e.action === "invalid")) return "error";
if (row.warnings?.length) return "warning";
return "success";
}
// Select row
function selectRow(row) {
selectedRow.value = row;
}
// Handlers
function onLimit(value) {
const val = Number(value ?? props.limit ?? 50);
emit("change-limit", isNaN(val) ? 50 : val);
}
function toggleVerbose() {
emit("toggle-verbose");
}
</script>
<template>
<Dialog :open="show" @update:open="(val) => !val && emit('close')">
<DialogContent class="max-w-7xl max-h-[90vh] overflow-hidden flex flex-col p-0">
<!-- Header -->
<div class="px-6 py-4 border-b bg-linear-to-r from-gray-50 to-white">
<div class="flex items-center justify-between">
<div>
<h2 class="text-xl font-semibold text-gray-900">Simulacija uvoza</h2>
<p class="text-sm text-gray-500 mt-1">Preglejte podatke pred uvozom</p>
</div>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<Label class="text-xs text-gray-600">Prikaži:</Label>
<Select :model-value="String(limit)" @update:model-value="onLimit">
<SelectTrigger class="w-20 h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="25">25</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
<SelectItem value="250">250</SelectItem>
</SelectContent>
</Select>
</div>
<Button variant="outline" size="sm" @click="toggleVerbose" class="text-xs">
{{ props.verbose ? "Manj" : "Več" }} podrobnosti
</Button>
<div class="flex items-center gap-2">
<Checkbox
id="show-only-changed"
:checked="showOnlyChanged"
@update:checked="(val) => (showOnlyChanged = val)"
/>
<Label for="show-only-changed" class="text-xs cursor-pointer">
Samo spremenjeni
</Label>
</div>
</div>
</div>
<!-- Entity Tabs -->
<Tabs
v-if="entitiesWithRows.length"
:model-value="activeEntity"
@update:model-value="
(val) => {
activeEntity = val;
selectedRow = null;
}
"
class="mt-4"
>
<TabsList class="w-full justify-start overflow-x-auto">
<TabsTrigger
v-for="e in entitiesWithRows"
:key="e"
:value="e"
class="flex items-center gap-2"
>
<span class="uppercase tracking-wide text-xs">{{
entityLabelMap[e] || e
}}</span>
<Badge variant="secondary" class="text-[10px]">
{{ entityStats[e]?.total || 0 }}
</Badge>
</TabsTrigger>
</TabsList>
</Tabs>
<!-- Stats Bar -->
<div
v-if="activeEntity && entityStats[activeEntity]"
class="flex items-center gap-4 mt-3 px-4 py-2 bg-white rounded-lg border text-xs"
>
<div class="flex items-center gap-1.5">
<CheckCircleIcon class="h-4 w-4 text-emerald-600" />
<span class="text-gray-600">Novo:</span>
<span class="font-semibold text-emerald-600">
{{ entityStats[activeEntity].create }}
</span>
</div>
<div class="flex items-center gap-1.5">
<CheckCircleIcon class="h-4 w-4 text-blue-600" />
<span class="text-gray-600">Posodobitev:</span>
<span class="font-semibold text-blue-600">
{{ entityStats[activeEntity].update }}
</span>
</div>
<div v-if="entityStats[activeEntity].invalid" class="flex items-center gap-1.5">
<XCircleIcon class="h-4 w-4 text-red-600" />
<span class="text-gray-600">Neveljavno:</span>
<span class="font-semibold text-red-600">
{{ entityStats[activeEntity].invalid }}
</span>
</div>
<div v-if="entityStats[activeEntity].errors" class="flex items-center gap-1.5">
<ExclamationTriangleIcon class="h-4 w-4 text-red-600" />
<span class="text-gray-600">Napake:</span>
<span class="font-semibold text-red-600">
{{ entityStats[activeEntity].errors }}
</span>
</div>
<div
v-if="entityStats[activeEntity].warnings"
class="flex items-center gap-1.5"
>
<ExclamationTriangleIcon class="h-4 w-4 text-amber-600" />
<span class="text-gray-600">Opozorila:</span>
<span class="font-semibold text-amber-600">
{{ entityStats[activeEntity].warnings }}
</span>
</div>
</div>
</div>
<!-- Split View -->
<div class="flex-1 flex overflow-hidden">
<!-- Left Panel - Row List -->
<div class="w-96 border-r bg-gray-50 overflow-y-auto">
<div v-if="loading" class="p-8 text-center text-gray-500">
<div
class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"
></div>
Nalagam...
</div>
<div v-else-if="!visibleRows.length" class="p-8 text-center text-gray-500">
Ni vrstic za prikaz
</div>
<div v-else class="divide-y">
<button
v-for="row in visibleRows"
:key="row.index"
@click="selectRow(row)"
class="w-full px-4 py-3 text-left hover:bg-white transition-colors"
:class="{
'bg-white shadow-sm': selectedRow?.index === row.index,
'bg-red-50': getRowStatus(row) === 'error',
'bg-amber-50': getRowStatus(row) === 'warning',
}"
>
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3 flex-1 min-w-0">
<!-- Status Icon -->
<div class="flex-shrink-0">
<CheckCircleIcon
v-if="getRowStatus(row) === 'success'"
class="h-5 w-5 text-emerald-600"
/>
<ExclamationTriangleIcon
v-else-if="getRowStatus(row) === 'warning'"
class="h-5 w-5 text-amber-600"
/>
<XCircleIcon v-else class="h-5 w-5 text-red-600" />
</div>
<!-- Row Info -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="text-xs font-semibold text-gray-900">
Vrstica #{{ row.index }}
</span>
<template v-if="row.entities && row.entities[activeEntity]">
<template v-if="Array.isArray(row.entities[activeEntity])">
<Badge variant="outline" class="text-[10px]">
{{ row.entities[activeEntity].length }}x
</Badge>
</template>
<template v-else>
<Badge
:variant="
row.entities[activeEntity].action === 'create'
? 'default'
: row.entities[activeEntity].action === 'update'
? 'secondary'
: 'destructive'
"
class="text-[10px]"
>
{{ row.entities[activeEntity].action }}
</Badge>
</template>
</template>
</div>
<div class="text-xs text-gray-600 truncate">
<template v-if="row.entities && row.entities[activeEntity]">
<template v-if="Array.isArray(row.entities[activeEntity])">
{{
row.entities[activeEntity]
.map((e) =>
Object.values(e.data || {})
.filter(Boolean)
.slice(0, 1)
.join(", ")
)
.filter(Boolean)
.join(" • ") || "Brez podatkov"
}}
</template>
<template v-else-if="row.entities[activeEntity].data">
{{
Object.values(row.entities[activeEntity].data)
.filter(Boolean)
.slice(0, 2)
.join(", ") || "Brez podatkov"
}}
</template>
</template>
</div>
</div>
</div>
<!-- Arrow -->
<ChevronRightIcon class="h-4 w-4 text-gray-400 flex-shrink-0" />
</div>
</button>
</div>
</div>
<!-- Right Panel - Row Details -->
<div v-if="selectedRow" class="flex-1 overflow-y-auto p-6">
<!-- Row Header -->
<div class="mb-6">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-gray-900">
Vrstica #{{ selectedRow.index }}
</h3>
<Badge
v-if="selectedRow.entities && selectedRow.entities[activeEntity]"
:variant="
selectedRow.entities[activeEntity].action === 'create'
? 'default'
: selectedRow.entities[activeEntity].action === 'update'
? 'secondary'
: 'destructive'
"
class="text-xs"
>
{{ selectedRow.entities[activeEntity].action }}
</Badge>
</div>
<p class="text-sm text-gray-500">
{{ entityLabelMap[activeEntity] || activeEntity }}
</p>
</div>
<!-- Errors -->
<div
v-if="selectedRow.errors && selectedRow.errors.length"
class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg"
>
<div class="flex items-start gap-2">
<XCircleIcon class="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" />
<div class="flex-1">
<h4 class="font-semibold text-red-900 mb-2">Napake</h4>
<ul class="space-y-1">
<li
v-for="(error, idx) in selectedRow.errors"
:key="idx"
class="text-sm text-red-700"
>
• {{ error }}
</li>
</ul>
</div>
</div>
</div>
<!-- Warnings -->
<div
v-if="selectedRow.warnings && selectedRow.warnings.length"
class="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-lg"
>
<div class="flex items-start gap-2">
<ExclamationTriangleIcon
class="h-5 w-5 text-amber-600 flex-shrink-0 mt-0.5"
/>
<div class="flex-1">
<h4 class="font-semibold text-amber-900 mb-2">Opozorila</h4>
<ul class="space-y-1">
<li
v-for="(warn, idx) in selectedRow.warnings"
:key="idx"
class="text-sm text-amber-700"
>
• {{ warn }}
</li>
</ul>
</div>
</div>
</div>
<!-- Entity Data -->
<div
v-if="selectedRow.entities && selectedRow.entities[activeEntity]"
class="space-y-6"
>
<!-- Handle Array of Entities (Groups) -->
<template v-if="Array.isArray(selectedRow.entities[activeEntity])">
<div
v-for="(entity, idx) in selectedRow.entities[activeEntity]"
:key="idx"
class="border rounded-lg p-4 bg-white"
>
<div class="flex items-center justify-between mb-3">
<h4 class="font-semibold text-gray-900">Instanca #{{ idx + 1 }}</h4>
<Badge
:variant="
entity.action === 'create'
? 'default'
: entity.action === 'update'
? 'secondary'
: 'destructive'
"
class="text-xs"
>
{{ entity.action }}
</Badge>
</div>
<!-- Entity Data -->
<div v-if="entity.data" class="bg-gray-50 rounded-lg p-3">
<dl class="grid grid-cols-1 gap-2">
<div
v-for="(value, field) in entity.data"
:key="field"
class="flex items-start gap-3"
>
<dt class="text-sm font-medium text-gray-600 w-32 flex-shrink-0">
{{ field }}
</dt>
<dd class="text-sm text-gray-900 font-medium flex-1">
{{ value || "—" }}
</dd>
</div>
</dl>
</div>
<!-- Entity Errors -->
<div
v-if="entity.errors && entity.errors.length"
class="mt-3 bg-red-50 rounded-lg p-3"
>
<h5 class="font-semibold text-red-900 text-sm mb-2">Napake</h5>
<ul class="space-y-1">
<li
v-for="(error, eidx) in entity.errors"
:key="eidx"
class="text-xs text-red-700"
>
• {{ error }}
</li>
</ul>
</div>
<!-- Reason for Skip -->
<div v-if="entity.reason" class="mt-3 text-sm text-gray-600">
<strong>Razlog:</strong> {{ entity.reason }}
</div>
</div>
</template>
<!-- Handle Single Entity -->
<template v-else>
<!-- Main Data -->
<div
v-if="selectedRow.entities[activeEntity].data"
class="bg-gray-50 rounded-lg p-4"
>
<h4 class="font-semibold text-gray-900 mb-3">Podatki</h4>
<dl class="grid grid-cols-1 gap-3">
<div
v-for="(value, field) in selectedRow.entities[activeEntity].data"
:key="field"
class="flex items-start gap-3"
>
<dt class="text-sm font-medium text-gray-600 w-40 flex-shrink-0">
{{ field }}
</dt>
<dd class="text-sm text-gray-900 font-medium flex-1">
{{ value || "—" }}
</dd>
</div>
</dl>
</div>
<!-- Reference Info (for updates) -->
<div
v-if="
selectedRow.entities[activeEntity].action === 'update' &&
(selectedRow.entities[activeEntity].reference ||
selectedRow.entities[activeEntity].existing_id)
"
class="bg-blue-50 rounded-lg p-4"
>
<h4 class="font-semibold text-blue-900 mb-3">
Informacije o posodobitvi
</h4>
<dl class="space-y-2">
<div v-if="selectedRow.entities[activeEntity].reference">
<dt class="text-sm font-medium text-blue-700">Referenca</dt>
<dd class="text-sm text-blue-900 font-medium mt-1">
{{ selectedRow.entities[activeEntity].reference }}
</dd>
</div>
<div v-if="selectedRow.entities[activeEntity].existing_id">
<dt class="text-sm font-medium text-blue-700">Obstoječi ID</dt>
<dd class="text-sm text-blue-900 font-medium mt-1">
{{ selectedRow.entities[activeEntity].existing_id }}
</dd>
</div>
</dl>
</div>
<!-- Changes (verbose mode) -->
<div
v-if="
props.verbose &&
selectedRow.entities[activeEntity].action === 'update' &&
selectedRow.entities[activeEntity].changes
"
class="bg-purple-50 rounded-lg p-4"
>
<h4 class="font-semibold text-purple-900 mb-3">Spremembe</h4>
<div class="space-y-2">
<div
v-for="(change, field) in selectedRow.entities[activeEntity].changes"
:key="field"
class="text-sm"
>
<div class="font-medium text-purple-700 mb-1">{{ field }}</div>
<div class="flex items-center gap-2 pl-3">
<span class="text-red-600 line-through">{{ change.old }}</span>
<span class="text-gray-400">→</span>
<span class="text-green-600 font-medium">{{ change.new }}</span>
</div>
</div>
</div>
</div>
<!-- Entity Errors -->
<div
v-if="
selectedRow.entities[activeEntity].errors &&
selectedRow.entities[activeEntity].errors.length
"
class="bg-red-50 rounded-lg p-4"
>
<h4 class="font-semibold text-red-900 mb-3">Napake entitete</h4>
<ul class="space-y-1">
<li
v-for="(error, idx) in selectedRow.entities[activeEntity].errors"
:key="idx"
class="text-sm text-red-700"
>
• {{ error }}
</li>
</ul>
</div>
</template>
</div>
</div>
</div>
<!-- Footer -->
<div class="px-6 py-3 border-t bg-gray-50 text-xs text-gray-500">
Samo simulacija podatki niso bili spremenjeni. Kliknite vrstico za podrobnosti.
</div>
</DialogContent>
</Dialog>
</template>