Changes to import / template pages frontend updated design

This commit is contained in:
Simon Pocrnjič
2025-12-22 20:52:45 +01:00
parent ee641586c3
commit f8623a6071
30 changed files with 2349 additions and 1839 deletions
@@ -0,0 +1,168 @@
<script setup>
import { Card, CardContent, CardHeader, CardTitle } from "@/Components/ui/card";
import { Label } from "@/Components/ui/label";
import { Input } from "@/Components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
import { Checkbox } from "@/Components/ui/checkbox";
import { Button } from "@/Components/ui/button";
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
const props = defineProps({
form: { type: Object, required: true },
clients: { type: Array, default: () => [] },
segments: { type: Array, default: () => [] },
actions: { type: Array, default: () => [] },
decisions: { type: Array, default: () => [] },
canChangeClient: { type: Boolean, default: true },
});
const emit = defineEmits(["save"]);
</script>
<template>
<Card>
<CardHeader>
<CardTitle>Osnovne informacije</CardTitle>
</CardHeader>
<CardContent class="space-y-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="name">Ime predloge</Label>
<Input id="name" v-model="form.name" type="text" />
</div>
<div class="space-y-2">
<Label for="source_type">Vir</Label>
<Select v-model="form.source_type">
<SelectTrigger id="source_type">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="csv">CSV</SelectItem>
<SelectItem value="xml">XML</SelectItem>
<SelectItem value="xls">XLS</SelectItem>
<SelectItem value="xlsx">XLSX</SelectItem>
<SelectItem value="json">JSON</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="default_record_type">Privzeti tip zapisa</Label>
<Input
id="default_record_type"
v-model="form.default_record_type"
type="text"
placeholder="npr.: account, person"
/>
</div>
<div class="space-y-2">
<Label for="client">Naročnik (izbirno)</Label>
<Select v-model="form.client_uuid" :disabled="!canChangeClient">
<SelectTrigger id="client">
<SelectValue placeholder="Globalno (brez naročnika)" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">Globalno (brez naročnika)</SelectItem>
<SelectItem v-for="c in clients || []" :key="c.uuid" :value="c.uuid">
{{ c.name }}
</SelectItem>
</SelectContent>
</Select>
<p v-if="!canChangeClient" class="text-xs text-amber-600">
Ni mogoče spremeniti naročnika, ker ta predloga že vsebuje preslikave.
</p>
</div>
<div class="space-y-2">
<Label for="delimiter">Privzeti ločilni znak (CSV)</Label>
<Select v-model="form.meta.delimiter">
<SelectTrigger id="delimiter">
<SelectValue placeholder="(Auto-detect)" />
</SelectTrigger>
<SelectContent>
<SelectItem value=",">Vejica ,</SelectItem>
<SelectItem value=";">Podpičje ;</SelectItem>
<SelectItem value="\t">Tab \t</SelectItem>
<SelectItem value="|">Pipe |</SelectItem>
<SelectItem value=" ">Presledek </SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground">
Pusti prazno za samodejno zaznavo. Uporabi, ko zaznavanje ne deluje pravilno.
</p>
</div>
<div class="space-y-2">
<Label for="segment">Privzeti segment</Label>
<Select v-model="form.meta.segment_id">
<SelectTrigger id="segment">
<SelectValue placeholder="(brez)" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">(brez)</SelectItem>
<SelectItem v-for="s in segments || []" :key="s.id" :value="s.id">
{{ s.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="action">Privzeto dejanje (post-contract activity)</Label>
<Select v-model="form.meta.action_id">
<SelectTrigger id="action">
<SelectValue placeholder="(brez)" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">(brez)</SelectItem>
<SelectItem v-for="a in actions || []" :key="a.id" :value="a.id">
{{ a.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="decision">Privzeta odločitev (post-contract)</Label>
<Select v-model="form.meta.decision_id" :disabled="!form.meta.action_id">
<SelectTrigger id="decision">
<SelectValue placeholder="(brez)" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">(brez)</SelectItem>
<SelectItem v-for="d in decisions || []" :key="d.id" :value="d.id">
{{ d.name }}
</SelectItem>
</SelectContent>
</Select>
<p v-if="!form.meta.action_id" class="text-xs text-muted-foreground">
Najprej izberi dejanje, nato odločitev.
</p>
</div>
</div>
<div class="flex items-center gap-6">
<div class="flex items-center gap-2">
<Checkbox
id="is_active"
:checked="form.is_active"
@update:checked="form.is_active = $event"
/>
<Label for="is_active" class="cursor-pointer">Aktivna</Label>
</div>
<div class="flex items-center gap-2">
<Checkbox
id="reactivate"
:checked="form.reactivate"
@update:checked="form.reactivate = $event"
/>
<Label for="reactivate" class="cursor-pointer">Reaktivacija</Label>
</div>
<Button @click="emit('save')" class="ml-auto">Shrani</Button>
</div>
</CardContent>
</Card>
</template>
@@ -0,0 +1,301 @@
<script setup>
import { ref } from "vue";
import { router } from "@inertiajs/vue3";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/Components/ui/accordion";
import { Card, CardContent } from "@/Components/ui/card";
import { Label } from "@/Components/ui/label";
import { Input } from "@/Components/ui/input";
import { Textarea } from "@/Components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { Button } from "@/Components/ui/button";
import { Badge } from "@/Components/ui/badge";
import { ArrowUp, ArrowDown } from "lucide-vue-next";
const props = defineProps({
entities: { type: Array, default: () => [] },
entityOptions: { type: Array, default: () => [] },
fieldOptions: { type: Object, default: () => ({}) },
mappings: { type: Array, default: () => [] },
templateUuid: { type: String, required: true },
allSourceColumns: { type: Array, default: () => [] },
entityAliases: { type: Object, default: () => ({}) },
actions: { type: Array, default: () => [] },
decisions: { type: Array, default: () => [] },
});
const emit = defineEmits(["refresh"]);
const newRows = ref({});
const bulkRows = ref({});
function addRow(entity) {
const row = newRows.value[entity];
if (!row || !row.source || !row.field) return;
const target_field = `${entity}.${row.field}`;
const opts = {};
if (row.group) opts.group = row.group;
if (row.field === "meta" && row.metaKey) {
opts.key = String(row.metaKey).trim();
if (row.metaType) opts.type = String(row.metaType).trim();
}
const payload = {
source_column: row.source,
target_field,
transform: row.transform || "",
apply_mode: row.apply_mode || "both",
options: Object.keys(opts).length ? opts : null,
position: (props.mappings?.length || 0) + 1,
};
router.post(
route("importTemplates.mappings.add", { template: props.templateUuid }),
payload,
{
preserveScroll: true,
onSuccess: () => {
newRows.value[entity] = {};
emit("refresh");
},
}
);
}
function updateMapping(m) {
const payload = {
source_column: m.source_column,
target_field: m.target_field,
transform: m.transform || "",
apply_mode: m.apply_mode || "both",
options: m.options || null,
position: m.position,
};
router.put(
route("importTemplates.mappings.update", {
template: props.templateUuid,
mapping: m.id,
}),
payload,
{
preserveScroll: true,
onSuccess: () => emit("refresh"),
}
);
}
function deleteMapping(m) {
router.delete(
route("importTemplates.mappings.delete", {
template: props.templateUuid,
mapping: m.id,
}),
{
preserveScroll: true,
onSuccess: () => emit("refresh"),
}
);
}
function reorder(entity, direction, m) {
const all = [...props.mappings];
const aliases = (props.entityAliases[entity] || [entity]).map((a) => a + ".");
const entityMaps = all.filter((x) => {
const tf = x.target_field || "";
return aliases.some((prefix) => tf.startsWith(prefix));
});
const idx = entityMaps.findIndex((x) => x.id === m.id);
if (idx < 0) return;
const swapIdx = direction === "up" ? idx - 1 : idx + 1;
if (swapIdx < 0 || swapIdx >= entityMaps.length) return;
const a = entityMaps[idx];
const b = entityMaps[swapIdx];
const ordered = all.map((x) => x.id);
const ai = ordered.indexOf(a.id);
const bi = ordered.indexOf(b.id);
if (ai < 0 || bi < 0) return;
[ordered[ai], ordered[bi]] = [ordered[bi], ordered[ai]];
router.post(
route("importTemplates.mappings.reorder", { template: props.templateUuid }),
{ order: ordered },
{
preserveScroll: true,
onSuccess: () => emit("refresh"),
}
);
}
function getEntityMappings(entity) {
const aliases = (props.entityAliases[entity] || [entity]).map((a) => a + ".");
return (props.mappings || []).filter((m) => {
const tf = m.target_field || "";
return aliases.some((prefix) => tf.startsWith(prefix));
});
}
</script>
<template>
<Card>
<CardContent class="p-0">
<Accordion type="multiple" collapsible class="w-full">
<AccordionItem v-for="entity in entities" :key="entity" :value="entity">
<AccordionTrigger class="px-4 hover:no-underline">
<span class="font-medium">
{{ entityOptions.find((e) => e.key === entity)?.label || entity }}
</span>
</AccordionTrigger>
<AccordionContent class="px-4 pb-4 space-y-4">
<!-- Existing mappings -->
<div v-if="getEntityMappings(entity).length > 0" class="space-y-2">
<div
v-for="m in getEntityMappings(entity)"
:key="m.id"
class="p-3 border rounded-lg bg-muted/30"
>
<div class="grid grid-cols-1 sm:grid-cols-5 gap-2 items-center">
<div class="space-y-1">
<Label class="text-xs">Izvor</Label>
<Input v-model="m.source_column" class="text-sm" />
</div>
<div class="space-y-1">
<Label class="text-xs">Cilj</Label>
<Input v-model="m.target_field" class="text-sm" />
</div>
<div class="space-y-1">
<Label class="text-xs">Transform</Label>
<Select v-model="m.transform">
<SelectTrigger class="text-sm">
<SelectValue placeholder="Brez" />
</SelectTrigger>
<SelectContent>
<SelectItem value="trim">trim</SelectItem>
<SelectItem value="upper">upper</SelectItem>
<SelectItem value="lower">lower</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-1">
<Label class="text-xs">Način</Label>
<Select v-model="m.apply_mode">
<SelectTrigger class="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="both">both</SelectItem>
<SelectItem value="insert">insert</SelectItem>
<SelectItem value="update">update</SelectItem>
<SelectItem value="keyref">keyref</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex items-end gap-2">
<div class="flex flex-col gap-1">
<Button
size="icon"
variant="outline"
class="h-6 w-6"
@click="reorder(entity, 'up', m)"
>
<ArrowUp class="h-3 w-3" />
</Button>
<Button
size="icon"
variant="outline"
class="h-6 w-6"
@click="reorder(entity, 'down', m)"
>
<ArrowDown class="h-3 w-3" />
</Button>
</div>
<div class="flex gap-2">
<Button size="sm" @click="updateMapping(m)">Shrani</Button>
<Button size="sm" variant="destructive" @click="deleteMapping(m)">
Izbriši
</Button>
</div>
</div>
</div>
</div>
</div>
<div v-else class="text-sm text-muted-foreground py-4 text-center">
Ni definiranih preslikav za to entiteto.
</div>
<!-- Add new mapping -->
<div class="p-3 bg-muted/50 rounded-lg border">
<div class="space-y-3">
<div class="text-sm font-medium">Dodaj novo preslikavo</div>
<div class="grid grid-cols-1 sm:grid-cols-4 gap-3">
<div class="space-y-2">
<Label class="text-xs">Izvorno polje</Label>
<Input
v-model="(newRows[entity] ||= {}).source"
placeholder="npr.: reference"
list="`src-opts-${entity}`"
/>
<datalist :id="`src-opts-${entity}`">
<option v-for="s in allSourceColumns" :key="s" :value="s" />
</datalist>
</div>
<div class="space-y-2">
<Label class="text-xs">Polje</Label>
<Select v-model="(newRows[entity] ||= {}).field">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="f in fieldOptions[entity] || []"
:key="f"
:value="f"
>
{{ f }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label class="text-xs">Transform</Label>
<Select v-model="(newRows[entity] ||= {}).transform">
<SelectTrigger>
<SelectValue placeholder="Brez" />
</SelectTrigger>
<SelectContent>
<SelectItem value="trim">trim</SelectItem>
<SelectItem value="upper">upper</SelectItem>
<SelectItem value="lower">lower</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label class="text-xs">Način</Label>
<Select v-model="(newRows[entity] ||= {}).apply_mode">
<SelectTrigger>
<SelectValue placeholder="both" />
</SelectTrigger>
<SelectContent>
<SelectItem value="both">both</SelectItem>
<SelectItem value="insert">insert</SelectItem>
<SelectItem value="update">update</SelectItem>
<SelectItem value="keyref">keyref</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Button @click="addRow(entity)" size="sm">Dodaj preslikavo</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</CardContent>
</Card>
</template>
@@ -0,0 +1,73 @@
<script setup>
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/Components/ui/card";
import { Label } from "@/Components/ui/label";
import { Checkbox } from "@/Components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
import { Badge } from "@/Components/ui/badge";
const props = defineProps({
form: { type: Object, required: true },
entities: { type: Array, default: () => [] },
});
</script>
<template>
<Card>
<CardHeader>
<CardTitle>Nastavitve načina uvoza</CardTitle>
<CardDescription>
Konfiguriraj način uvoza za zgodovino ali plačila
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div class="flex items-center gap-6">
<label class="inline-flex items-center gap-2 cursor-pointer">
<Checkbox
:checked="form.meta.history_import"
@update:checked="form.meta.history_import = $event"
/>
<span class="text-sm font-medium">Uvoz zgodovine</span>
</label>
<label class="inline-flex items-center gap-2 cursor-pointer">
<Checkbox
:checked="form.meta.payments_import"
@update:checked="form.meta.payments_import = $event"
/>
<span class="text-sm font-medium">Uvoz plačil</span>
</label>
</div>
<p class="text-xs text-muted-foreground">
Zgodovina dovoljuje oseba/naslov/telefon/pogodbe/aktivnosti/primeri strank; računi so
samodejno dodani s pogodbami. Plačila zaklene entitete na Pogodbe Računi Plačila.
</p>
<div v-if="form.meta.payments_import" class="space-y-2 pt-2">
<Label for="contract_key">Ključ ujemanja pogodb</Label>
<Select v-model="form.meta.contract_key_mode">
<SelectTrigger id="contract_key">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="reference">
Referenca (uporabi samo contract.reference za iskanje zapisov)
</SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground">
Preslika stolpec CSV na contract.reference za reševanje pogodb za tega naročnika.
</p>
</div>
<!-- Entities locked info for payments mode -->
<div v-if="form.meta.payments_import" class="p-3 bg-emerald-50 rounded-lg border border-emerald-200">
<div class="text-sm text-emerald-900 mb-2 font-medium">Entitete so zaklenjene:</div>
<div class="flex flex-wrap gap-2">
<Badge variant="secondary" class="bg-emerald-100 text-emerald-800">Pogodbe</Badge>
<Badge variant="secondary" class="bg-emerald-100 text-emerald-800">Računi</Badge>
<Badge variant="secondary" class="bg-emerald-100 text-emerald-800">Plačila</Badge>
</div>
</div>
</CardContent>
</Card>
</template>
@@ -0,0 +1,253 @@
<script setup>
import { ref } from "vue";
import { useForm } from "@inertiajs/vue3";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/Components/ui/card";
import { Label } from "@/Components/ui/label";
import { Input } from "@/Components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { Button } from "@/Components/ui/button";
import { Badge } from "@/Components/ui/badge";
const props = defineProps({
unassigned: { type: Array, default: () => [] },
templateUuid: { type: String, required: true },
entityOptions: { type: Array, default: () => [] },
fieldOptions: { type: Object, default: () => ({}) },
suggestions: { type: Object, default: () => ({}) },
});
const emit = defineEmits(["refresh"]);
const unassignedState = ref({});
function saveUnassigned(m) {
const st = unassignedState.value[m.id] || {};
if (st.entity && st.field) {
m.target_field = `${st.entity}.${st.field}`;
} else {
m.target_field = null;
}
if (st.group) {
m.options = m.options && typeof m.options === "object" ? m.options : {};
m.options.group = st.group;
}
if (st.field === "meta") {
if (st.metaKey && String(st.metaKey).trim() !== "") {
m.options = m.options && typeof m.options === "object" ? m.options : {};
m.options.key = String(st.metaKey).trim();
}
if (st.metaType && String(st.metaType).trim() !== "") {
m.options = m.options && typeof m.options === "object" ? m.options : {};
m.options.type = String(st.metaType).trim();
}
}
const payload = {
source_column: m.source_column,
target_field: m.target_field,
transform: m.transform,
apply_mode: m.apply_mode,
options: m.options || null,
position: m.position,
};
useForm(payload).put(
route("importTemplates.mappings.update", {
template: props.templateUuid,
mapping: m.id,
}),
{
preserveScroll: true,
onSuccess: () => emit("refresh"),
}
);
}
function deleteMapping(m) {
useForm({}).delete(
route("importTemplates.mappings.delete", {
template: props.templateUuid,
mapping: m.id,
}),
{
preserveScroll: true,
onSuccess: () => emit("refresh"),
}
);
}
function applySuggestion(m, suggestion) {
if (!suggestion || !suggestion.entity || !suggestion.field) return;
const state = unassignedState.value[m.id] || {};
state.entity = suggestion.entity;
state.field = suggestion.field;
unassignedState.value[m.id] = state;
saveUnassigned(m);
}
</script>
<template>
<Card v-if="unassigned && unassigned.length > 0" class="border-amber-200">
<CardHeader class="bg-amber-50">
<CardTitle class="text-amber-900">
Nedodeljene preslikave ({{ unassigned.length }})
</CardTitle>
<CardDescription class="text-amber-700">
Te preslikave nimajo dodeljene ciljne entitete in polja
</CardDescription>
</CardHeader>
<CardContent class="p-4 space-y-3">
<div v-for="m in unassigned" :key="m.id" class="p-3 bg-white border rounded-lg">
<div class="space-y-3">
<!-- Source column with suggestion -->
<div class="flex items-start justify-between">
<div>
<Label class="text-xs text-muted-foreground">Izvorno polje</Label>
<div class="font-medium">{{ m.source_column }}</div>
<div v-if="suggestions && suggestions[m.source_column]" class="mt-1">
<Button
size="sm"
variant="link"
class="h-auto p-0 text-xs text-indigo-700"
@click="applySuggestion(m, suggestions[m.source_column])"
>
Predlog: {{ suggestions[m.source_column].entity }}.{{
suggestions[m.source_column].field
}}
</Button>
</div>
</div>
</div>
<!-- Entity and Field selection -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div class="space-y-2">
<Label for="entity">Entiteta</Label>
<Select v-model="(unassignedState[m.id] ||= {}).entity">
<SelectTrigger>
<SelectValue placeholder="(izberi)" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="opt in entityOptions"
:key="opt.key"
:value="opt.key"
>
{{ opt.label }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="field">Polje</Label>
<Select
v-model="(unassignedState[m.id] ||= {}).field"
:disabled="!(unassignedState[m.id] || {}).entity"
>
<SelectTrigger>
<SelectValue placeholder="(izberi)" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="f in fieldOptions[(unassignedState[m.id] || {}).entity] || []"
:key="f"
:value="f"
>
{{ f }}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<!-- Transform, Apply Mode, Group -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div class="space-y-2">
<Label>Transform</Label>
<Select v-model="m.transform">
<SelectTrigger>
<SelectValue placeholder="Brez" />
</SelectTrigger>
<SelectContent>
<SelectItem value="trim">trim</SelectItem>
<SelectItem value="upper">upper</SelectItem>
<SelectItem value="lower">lower</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>Način</Label>
<Select v-model="m.apply_mode">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="both">both</SelectItem>
<SelectItem value="insert">insert</SelectItem>
<SelectItem value="update">update</SelectItem>
<SelectItem value="keyref">keyref</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>Skupina</Label>
<Input
v-model="(unassignedState[m.id] ||= {}).group"
placeholder="1, 2, home, work"
/>
</div>
</div>
<!-- Meta fields if applicable -->
<div
v-if="(unassignedState[m.id] || {}).field === 'meta'"
class="grid grid-cols-1 sm:grid-cols-2 gap-3"
>
<div class="space-y-2">
<Label>Meta ključ</Label>
<Input
v-model="(unassignedState[m.id] ||= {}).metaKey"
placeholder="npr.: note, category"
/>
</div>
<div class="space-y-2">
<Label>Meta tip</Label>
<Select v-model="(unassignedState[m.id] ||= {}).metaType">
<SelectTrigger>
<SelectValue placeholder="(auto/string)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="string">string</SelectItem>
<SelectItem value="number">number</SelectItem>
<SelectItem value="date">date</SelectItem>
<SelectItem value="boolean">boolean</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<!-- Actions -->
<div class="flex items-center gap-2 pt-2">
<Button size="sm" @click="saveUnassigned(m)">Shrani</Button>
<Button size="sm" variant="destructive" @click="deleteMapping(m)"
>Izbriši</Button
>
</div>
</div>
</div>
</CardContent>
</Card>
</template>