Teren-app/resources/js/Pages/Imports/Templates/Edit.vue

477 lines
14 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 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" />
<!-- 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>