Teren-app/resources/js/Pages/Imports/Templates/Partials/EntityMappings.vue

302 lines
11 KiB
Vue

<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>