482 lines
14 KiB
Vue
482 lines
14 KiB
Vue
<script setup>
|
||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||
import { ref, computed, onMounted, watch } from "vue";
|
||
import { useForm, Link, router } from "@inertiajs/vue3";
|
||
import axios from "axios";
|
||
import BasicTemplateInfo from "./Partials/BasicTemplateInfo.vue";
|
||
import ImportModeSettings from "./Partials/ImportModeSettings.vue";
|
||
import UnassignedMappings from "./Partials/UnassignedMappings.vue";
|
||
import EntityMappings from "./Partials/EntityMappings.vue";
|
||
import { Card, CardContent, CardHeader, CardTitle } 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 {
|
||
AlertDialog,
|
||
AlertDialogAction,
|
||
AlertDialogCancel,
|
||
AlertDialogContent,
|
||
AlertDialogDescription,
|
||
AlertDialogFooter,
|
||
AlertDialogHeader,
|
||
AlertDialogTitle,
|
||
} from "@/Components/ui/alert-dialog";
|
||
|
||
const props = defineProps({
|
||
template: Object,
|
||
clients: Array,
|
||
segments: Array,
|
||
decisions: Array,
|
||
actions: Array,
|
||
});
|
||
|
||
const form = useForm({
|
||
name: props.template.name,
|
||
description: props.template.description,
|
||
source_type: props.template.source_type,
|
||
default_record_type: props.template.default_record_type || "",
|
||
is_active: props.template.is_active,
|
||
reactivate: props.template.reactivate ?? false,
|
||
client_uuid: props.template.client_uuid || null,
|
||
sample_headers: props.template.sample_headers || [],
|
||
// Add meta with default delimiter support
|
||
meta: {
|
||
...(props.template.meta || {}),
|
||
payments_import: props.template.meta?.payments_import ?? false,
|
||
history_import: props.template.meta?.history_import ?? false,
|
||
activity_action_id:
|
||
props.template.meta?.activity_action_id ?? props.template.meta?.action_id ?? null,
|
||
activity_decision_id:
|
||
props.template.meta?.activity_decision_id ??
|
||
props.template.meta?.decision_id ??
|
||
null,
|
||
delimiter: (props.template.meta && props.template.meta.delimiter) || "",
|
||
},
|
||
});
|
||
|
||
const decisionsForSelectedAction = computed(() => {
|
||
const act = (props.actions || []).find((a) => a.id === form.meta.action_id);
|
||
return act?.decisions || [];
|
||
});
|
||
|
||
const decisionsForActivitiesAction = computed(() => {
|
||
const act = (props.actions || []).find((a) => a.id === form.meta.activity_action_id);
|
||
return act?.decisions || [];
|
||
});
|
||
|
||
const activityCreatedAtInput = computed({
|
||
get() {
|
||
if (!form.meta.activity_created_at) return "";
|
||
return String(form.meta.activity_created_at).replace(" ", "T");
|
||
},
|
||
set(v) {
|
||
form.meta.activity_created_at = v ? String(v).replace("T", " ") : null;
|
||
},
|
||
});
|
||
|
||
watch(
|
||
() => form.meta.action_id,
|
||
() => {
|
||
form.meta.decision_id = null;
|
||
}
|
||
);
|
||
|
||
watch(
|
||
() => form.meta.activity_action_id,
|
||
() => {
|
||
form.meta.activity_decision_id = null;
|
||
}
|
||
);
|
||
|
||
const entities = computed(() => props.template.meta?.entities || []);
|
||
const hasMappings = computed(() => (props.template.mappings?.length || 0) > 0);
|
||
const canChangeClient = computed(() => !hasMappings.value); // guard reassignment when mappings exist (optional rule)
|
||
|
||
const unassigned = computed(() =>
|
||
(props.template.mappings || []).filter((m) => !m.target_field)
|
||
);
|
||
|
||
const unassignedSourceColumns = computed(() => {
|
||
const set = new Set();
|
||
for (const m of unassigned.value) {
|
||
if (m.source_column) set.add(m.source_column);
|
||
}
|
||
return Array.from(set).sort((a, b) => a.localeCompare(b));
|
||
});
|
||
|
||
const allSourceColumns = computed(() => {
|
||
const set = new Set();
|
||
(props.template.sample_headers || []).forEach((h) => set.add(h));
|
||
(props.template.mappings || []).forEach((m) => {
|
||
if (m.source_column) set.add(m.source_column);
|
||
});
|
||
|
||
return Array.from(set).sort((a, b) => a.localeCompare(b));
|
||
});
|
||
|
||
// Dynamic Import Entity definitions and field options from API
|
||
const entityDefs = ref([]);
|
||
const entityOptions = computed(() =>
|
||
entityDefs.value.map((e) => ({ key: e.key, label: e.label || e.key }))
|
||
);
|
||
const fieldOptions = computed(() =>
|
||
Object.fromEntries(entityDefs.value.map((e) => [e.key, e.fields || []]))
|
||
);
|
||
const ENTITY_ALIASES = computed(() => {
|
||
const map = {};
|
||
for (const e of entityDefs.value) {
|
||
const aliases = Array.isArray(e.aliases) ? [...e.aliases] : [];
|
||
if (!aliases.includes(e.key)) {
|
||
aliases.push(e.key);
|
||
}
|
||
map[e.key] = aliases;
|
||
}
|
||
return map;
|
||
});
|
||
|
||
async function loadEntityDefs() {
|
||
try {
|
||
const { data } = await axios.get("/api/import-entities");
|
||
entityDefs.value = data?.entities || [];
|
||
} catch (e) {
|
||
console.error("Failed to load import entity definitions", e);
|
||
}
|
||
}
|
||
|
||
// Suggestions powered by backend API
|
||
const suggestions = ref({}); // { [sourceColumn]: { entity, field } }
|
||
async function refreshSuggestions(columns) {
|
||
const cols = Array.isArray(columns) ? columns : unassignedSourceColumns.value;
|
||
if (!cols || cols.length === 0) {
|
||
return;
|
||
}
|
||
try {
|
||
const only = props.template.meta?.entities || [];
|
||
const { data } = await axios.post("/api/import-entities/suggest", {
|
||
columns: cols,
|
||
only_entities: only,
|
||
});
|
||
suggestions.value = { ...suggestions.value, ...(data?.suggestions || {}) };
|
||
} catch (e) {
|
||
console.error("Failed to load suggestions", e);
|
||
}
|
||
}
|
||
|
||
// Save basic
|
||
const save = () => {
|
||
const payload = { ...form.data() };
|
||
if (!canChangeClient.value) {
|
||
// drop client change when blocked
|
||
delete payload.client_uuid;
|
||
}
|
||
const hasActivities =
|
||
Array.isArray(payload.meta?.entities) && payload.meta.entities.includes("activities");
|
||
if (
|
||
hasActivities &&
|
||
(!payload.meta?.activity_action_id || !payload.meta?.activity_decision_id)
|
||
) {
|
||
alert(
|
||
"Activity imports require selecting an Action and Decision (Activities section)."
|
||
);
|
||
return;
|
||
}
|
||
// Normalize empty delimiter: remove from meta to allow auto-detect
|
||
if (
|
||
payload.meta &&
|
||
typeof payload.meta.delimiter === "string" &&
|
||
payload.meta.delimiter.trim() === ""
|
||
) {
|
||
delete payload.meta.delimiter;
|
||
}
|
||
useForm(payload).put(
|
||
route("importTemplates.update", { template: props.template.uuid }),
|
||
{ preserveScroll: true }
|
||
);
|
||
};
|
||
|
||
// Refresh page helper for partial components
|
||
function refreshPage() {
|
||
router.reload({ preserveScroll: true });
|
||
}
|
||
|
||
// Non-blocking confirm modal state for delete
|
||
const deleteConfirmOpen = ref(false);
|
||
const deleteForm = useForm({});
|
||
|
||
// Bulk add state
|
||
const bulkForm = ref({
|
||
sources: "",
|
||
entity: null,
|
||
default_field: "",
|
||
apply_mode: "both",
|
||
transform: "",
|
||
group: "",
|
||
});
|
||
|
||
function openDeleteConfirm() {
|
||
deleteConfirmOpen.value = true;
|
||
}
|
||
|
||
function performDelete() {
|
||
deleteForm.delete(route("importTemplates.destroy", { template: props.template.uuid }), {
|
||
onFinish: () => {
|
||
deleteConfirmOpen.value = false;
|
||
},
|
||
});
|
||
}
|
||
|
||
function submitBulkAdd() {
|
||
if (!bulkForm.value.sources || bulkForm.value.sources.trim() === "") {
|
||
alert("Vnesite vsaj en izvorni stolpec");
|
||
return;
|
||
}
|
||
router.post(
|
||
route("importTemplates.mappings.bulk", { template: props.template.uuid }),
|
||
{
|
||
sources: bulkForm.value.sources,
|
||
entity: bulkForm.value.entity || null,
|
||
default_field: bulkForm.value.default_field || null,
|
||
apply_mode: bulkForm.value.apply_mode || "both",
|
||
transform: bulkForm.value.transform || "",
|
||
group: bulkForm.value.group || null,
|
||
},
|
||
{
|
||
preserveScroll: true,
|
||
onSuccess: () => {
|
||
bulkForm.value = {
|
||
sources: "",
|
||
entity: null,
|
||
default_field: "",
|
||
apply_mode: "both",
|
||
transform: "",
|
||
group: "",
|
||
};
|
||
refreshPage();
|
||
},
|
||
}
|
||
);
|
||
}
|
||
|
||
// Load entity definitions and initial suggestions
|
||
onMounted(async () => {
|
||
await loadEntityDefs();
|
||
await refreshSuggestions();
|
||
});
|
||
|
||
// Refresh suggestions when unassigned list changes
|
||
watch(
|
||
() => unassignedSourceColumns.value.join("|"),
|
||
async () => {
|
||
await refreshSuggestions();
|
||
}
|
||
);
|
||
|
||
// Ensure default contract match key when turning on payments mode on existing template
|
||
watch(
|
||
() => form.meta.payments_import,
|
||
(enabled) => {
|
||
if (enabled) {
|
||
if (form.meta.history_import) {
|
||
form.meta.history_import = false;
|
||
}
|
||
}
|
||
if (enabled && !form.meta.contract_key_mode) {
|
||
form.meta.contract_key_mode = "reference";
|
||
}
|
||
}
|
||
);
|
||
|
||
// History mode is mutually exclusive with payments mode
|
||
watch(
|
||
() => form.meta.history_import,
|
||
(enabled) => {
|
||
if (enabled && form.meta.payments_import) {
|
||
form.meta.payments_import = false;
|
||
form.meta.contract_key_mode = null;
|
||
}
|
||
}
|
||
);
|
||
</script>
|
||
|
||
<template>
|
||
<AppLayout :title="`Uredi predlogo: ${props.template.name}`">
|
||
<template #header></template>
|
||
|
||
<div class="py-2">
|
||
<div class="max-w-6xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||
<!-- Header Actions -->
|
||
<div class="flex justify-end">
|
||
<Button variant="destructive" @click="openDeleteConfirm">
|
||
Izbriši predlogo
|
||
</Button>
|
||
</div>
|
||
|
||
<!-- Basic Template Info -->
|
||
<BasicTemplateInfo
|
||
:form="form"
|
||
:clients="props.clients"
|
||
:segments="props.segments"
|
||
:actions="props.actions"
|
||
:decisions="decisionsForSelectedAction"
|
||
:can-change-client="canChangeClient"
|
||
@save="save"
|
||
/>
|
||
|
||
<!-- Import Mode Settings -->
|
||
<ImportModeSettings
|
||
:form="form"
|
||
:entities="entities"
|
||
:actions="props.actions"
|
||
:decisions="props.decisions"
|
||
/>
|
||
|
||
<!-- Unassigned Mappings -->
|
||
<UnassignedMappings
|
||
:unassigned="unassigned"
|
||
:template-uuid="props.template.uuid"
|
||
:entity-options="entityOptions"
|
||
:field-options="fieldOptions"
|
||
:suggestions="suggestions"
|
||
@refresh="refreshPage"
|
||
/>
|
||
|
||
<!-- Bulk Add Mappings -->
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>Bulk dodajanje preslikav</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div class="grid grid-cols-1 gap-4">
|
||
<div class="space-y-2">
|
||
<Label
|
||
>Source columns (ločeno z vejicami, podpičji ali po vrsticah)</Label
|
||
>
|
||
<Textarea
|
||
v-model="bulkForm.sources"
|
||
placeholder="npr.: Pogodba sklic,Številka plačila,Datum,Znesek"
|
||
rows="4"
|
||
/>
|
||
</div>
|
||
<div class="grid grid-cols-1 sm:grid-cols-4 gap-4">
|
||
<div class="space-y-2">
|
||
<Label>Entity (opcijsko)</Label>
|
||
<Select v-model="bulkForm.entity">
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="(brez – pus" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem v-for="e in entityOptions" :key="e.key" :value="e.key">
|
||
{{ e.label }}
|
||
</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div class="space-y-2">
|
||
<Label>Field (opcijsko, za vse)</Label>
|
||
<Input
|
||
v-model="bulkForm.default_field"
|
||
placeholder="(auto from source)"
|
||
/>
|
||
</div>
|
||
<div class="space-y-2">
|
||
<Label>Transform (za vse)</Label>
|
||
<Select v-model="bulkForm.transform">
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="None" />
|
||
</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>Apply (za vse)</Label>
|
||
<Select v-model="bulkForm.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>
|
||
<div class="space-y-2">
|
||
<Label>Group (za vse)</Label>
|
||
<Input v-model="bulkForm.group" placeholder="1, 2, home, work" />
|
||
</div>
|
||
<div>
|
||
<Button @click="submitBulkAdd">Dodaj več</Button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<!-- Entity Mappings -->
|
||
<EntityMappings
|
||
:entities="entities"
|
||
:entity-options="entityOptions"
|
||
:field-options="fieldOptions"
|
||
:mappings="props.template.mappings || []"
|
||
:template-uuid="props.template.uuid"
|
||
:all-source-columns="allSourceColumns"
|
||
:entity-aliases="ENTITY_ALIASES"
|
||
:actions="props.actions"
|
||
:decisions="props.decisions"
|
||
@refresh="refreshPage"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Delete Confirmation Dialog -->
|
||
<AlertDialog v-model:open="deleteConfirmOpen">
|
||
<AlertDialogContent>
|
||
<AlertDialogHeader>
|
||
<AlertDialogTitle>Izbrišem predlogo?</AlertDialogTitle>
|
||
<AlertDialogDescription>
|
||
Tega dejanja ni mogoče razveljaviti. Vse preslikave te predloge bodo
|
||
izbrisane.
|
||
</AlertDialogDescription>
|
||
</AlertDialogHeader>
|
||
<AlertDialogFooter>
|
||
<AlertDialogCancel :disabled="deleteForm.processing">
|
||
Prekliči
|
||
</AlertDialogCancel>
|
||
<AlertDialogAction
|
||
@click="performDelete"
|
||
:disabled="deleteForm.processing"
|
||
class="bg-destructive hover:bg-destructive/90"
|
||
>
|
||
<span v-if="deleteForm.processing">Brisanje…</span>
|
||
<span v-else>Izbriši</span>
|
||
</AlertDialogAction>
|
||
</AlertDialogFooter>
|
||
</AlertDialogContent>
|
||
</AlertDialog>
|
||
</AppLayout>
|
||
</template>
|
||
|
||
<style>
|
||
.fade-enter-active,
|
||
.fade-leave-active {
|
||
transition: opacity 0.2s;
|
||
}
|
||
.fade-enter-from,
|
||
.fade-leave-to {
|
||
opacity: 0;
|
||
}
|
||
</style>
|