Import export templates
This commit is contained in:
parent
36b63a180d
commit
b9f66cbfbe
|
|
@ -23,6 +23,16 @@ public function index()
|
|||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
// Preload options for import mapping
|
||||
$clients = Client::query()
|
||||
->join('person', 'person.id', '=', 'clients.person_id')
|
||||
->orderBy('person.full_name')
|
||||
->get(['clients.uuid', DB::raw('person.full_name as name')]);
|
||||
|
||||
$segments = Segment::query()->orderBy('name')->get(['id', 'name']);
|
||||
$decisions = Decision::query()->orderBy('name')->get(['id', 'name']);
|
||||
$actions = Action::query()->orderBy('name')->get(['id', 'name']);
|
||||
|
||||
return Inertia::render('Imports/Templates/Index', [
|
||||
'templates' => $templates->map(fn ($t) => [
|
||||
'uuid' => $t->uuid,
|
||||
|
|
@ -35,6 +45,10 @@ public function index()
|
|||
'name' => $t->client->person?->full_name,
|
||||
] : null,
|
||||
]),
|
||||
'clients' => $clients,
|
||||
'segments' => $segments,
|
||||
'decisions' => $decisions,
|
||||
'actions' => $actions,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -664,4 +678,138 @@ public function destroy(ImportTemplate $template)
|
|||
|
||||
return redirect()->route('importTemplates.index')->with('success', 'Template deleted');
|
||||
}
|
||||
|
||||
// Export template as JSON file
|
||||
public function export(ImportTemplate $template)
|
||||
{
|
||||
$template->load('mappings');
|
||||
|
||||
$data = [
|
||||
'name' => $template->name,
|
||||
'description' => $template->description,
|
||||
'source_type' => $template->source_type,
|
||||
'default_record_type' => $template->default_record_type,
|
||||
'sample_headers' => $template->sample_headers,
|
||||
'is_active' => $template->is_active,
|
||||
'reactivate' => $template->reactivate,
|
||||
'meta' => $template->meta,
|
||||
'mappings' => $template->mappings->map(fn($m) => [
|
||||
'source_column' => $m->source_column,
|
||||
'entity' => $m->entity,
|
||||
'target_field' => $m->target_field,
|
||||
'transform' => $m->transform,
|
||||
'apply_mode' => $m->apply_mode,
|
||||
'options' => $m->options,
|
||||
'position' => $m->position,
|
||||
])->values()->toArray(),
|
||||
];
|
||||
|
||||
$filename = Str::slug($template->name) . '-' . now()->format('Y-m-d') . '.json';
|
||||
|
||||
return response()->json($data)
|
||||
->header('Content-Disposition', 'attachment; filename="' . $filename . '"');
|
||||
}
|
||||
|
||||
// Import template from JSON file
|
||||
public function import(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'file' => 'required|file|mimes:json,txt|max:10240',
|
||||
'client_uuid' => 'nullable|string|exists:clients,uuid',
|
||||
'segment_id' => 'nullable|integer|exists:segments,id',
|
||||
'decision_id' => 'nullable|integer|exists:decisions,id',
|
||||
'action_id' => 'nullable|integer|exists:actions,id',
|
||||
'activity_action_id' => 'nullable|integer|exists:actions,id',
|
||||
'activity_decision_id' => 'nullable|integer|exists:decisions,id',
|
||||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
$contents = file_get_contents($file->getRealPath());
|
||||
$json = json_decode($contents, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return back()->withErrors(['file' => 'Invalid JSON file']);
|
||||
}
|
||||
|
||||
// Validate structure
|
||||
$validator = validator($json, [
|
||||
'name' => 'required|string|max:100',
|
||||
'description' => 'nullable|string|max:255',
|
||||
'source_type' => 'required|string|in:csv,xml,xls,xlsx,json',
|
||||
'default_record_type' => 'nullable|string|max:50',
|
||||
'sample_headers' => 'nullable|array',
|
||||
'is_active' => 'nullable|boolean',
|
||||
'reactivate' => 'nullable|boolean',
|
||||
'meta' => 'nullable|array',
|
||||
'mappings' => 'nullable|array',
|
||||
'mappings.*.source_column' => 'required|string',
|
||||
'mappings.*.entity' => 'nullable|string',
|
||||
'mappings.*.target_field' => 'nullable|string',
|
||||
'mappings.*.transform' => 'nullable|string',
|
||||
'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both,keyref',
|
||||
'mappings.*.options' => 'nullable|array',
|
||||
'mappings.*.position' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return back()->withErrors($validator)->withInput();
|
||||
}
|
||||
|
||||
$clientId = null;
|
||||
if (!empty($data['client_uuid'])) {
|
||||
$clientId = Client::where('uuid', $data['client_uuid'])->value('id');
|
||||
}
|
||||
|
||||
// Replace IDs in meta if provided
|
||||
$meta = $json['meta'] ?? [];
|
||||
if (!empty($data['segment_id'])) {
|
||||
$meta['segment_id'] = $data['segment_id'];
|
||||
}
|
||||
if (!empty($data['decision_id'])) {
|
||||
$meta['decision_id'] = $data['decision_id'];
|
||||
}
|
||||
if (!empty($data['action_id'])) {
|
||||
$meta['action_id'] = $data['action_id'];
|
||||
}
|
||||
if (!empty($data['activity_action_id'])) {
|
||||
$meta['activity_action_id'] = $data['activity_action_id'];
|
||||
}
|
||||
if (!empty($data['activity_decision_id'])) {
|
||||
$meta['activity_decision_id'] = $data['activity_decision_id'];
|
||||
}
|
||||
|
||||
$template = null;
|
||||
DB::transaction(function () use (&$template, $request, $json, $clientId, $meta) {
|
||||
$template = ImportTemplate::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => $json['name'],
|
||||
'description' => $json['description'] ?? null,
|
||||
'source_type' => $json['source_type'],
|
||||
'default_record_type' => $json['default_record_type'] ?? null,
|
||||
'sample_headers' => $json['sample_headers'] ?? null,
|
||||
'user_id' => $request->user()?->id,
|
||||
'client_id' => $clientId,
|
||||
'is_active' => $json['is_active'] ?? true,
|
||||
'reactivate' => $json['reactivate'] ?? false,
|
||||
'meta' => $meta,
|
||||
]);
|
||||
|
||||
foreach (($json['mappings'] ?? []) as $m) {
|
||||
ImportTemplateMapping::create([
|
||||
'import_template_id' => $template->id,
|
||||
'entity' => $m['entity'] ?? null,
|
||||
'source_column' => $m['source_column'],
|
||||
'target_field' => $m['target_field'] ?? null,
|
||||
'transform' => $m['transform'] ?? null,
|
||||
'apply_mode' => $m['apply_mode'] ?? 'both',
|
||||
'options' => $m['options'] ?? null,
|
||||
'position' => $m['position'] ?? null,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
return redirect()
|
||||
->route('importTemplates.edit', ['template' => $template->uuid])
|
||||
->with('success', 'Template imported successfully');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup>
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import { Link, useForm } from "@inertiajs/vue3";
|
||||
import { ref } from "vue";
|
||||
import { Link, useForm, router } from "@inertiajs/vue3";
|
||||
import { ref, computed } from "vue";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
|
|
@ -21,18 +21,54 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/Components/ui/alert-dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/Components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/Components/ui/select";
|
||||
import InputLabel from "@/Components/InputLabel.vue";
|
||||
import { Separator } from "@/Components/ui/separator";
|
||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
||||
import { ListIndentIncreaseIcon } from "lucide-vue-next";
|
||||
import { ListIndentIncreaseIcon, DownloadIcon, UploadIcon } from "lucide-vue-next";
|
||||
import TableActions from "@/Components/DataTable/TableActions.vue";
|
||||
import ActionMenuItem from "@/Components/DataTable/ActionMenuItem.vue";
|
||||
import { faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faPencil,
|
||||
faTrash,
|
||||
faFileExport,
|
||||
faFileImport,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
// Non-blocking confirm modal state
|
||||
const confirmOpen = ref(false);
|
||||
const confirmUuid = ref(null);
|
||||
const deleteForm = useForm({});
|
||||
|
||||
// Import modal state
|
||||
const importModalOpen = ref(false);
|
||||
const importForm = useForm({
|
||||
file: null,
|
||||
client_uuid: null,
|
||||
segment_id: null,
|
||||
decision_id: null,
|
||||
action_id: null,
|
||||
activity_action_id: null,
|
||||
activity_decision_id: null,
|
||||
});
|
||||
|
||||
const fileInputRef = ref(null);
|
||||
const importedData = ref(null);
|
||||
|
||||
function requestDelete(uuid) {
|
||||
confirmUuid.value = uuid;
|
||||
confirmOpen.value = true;
|
||||
|
|
@ -54,8 +90,57 @@ function cancelDelete() {
|
|||
confirmUuid.value = null;
|
||||
}
|
||||
|
||||
function exportTemplate(uuid) {
|
||||
window.location.href = route("importTemplates.export", { template: uuid });
|
||||
}
|
||||
|
||||
function openImportModal() {
|
||||
importModalOpen.value = true;
|
||||
}
|
||||
|
||||
function cancelImport() {
|
||||
importModalOpen.value = false;
|
||||
importForm.reset();
|
||||
importedData.value = null;
|
||||
}
|
||||
|
||||
function handleFileChange(event) {
|
||||
const file = event.target.files[0];
|
||||
importForm.file = file;
|
||||
|
||||
// Parse JSON to show existing IDs
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const json = JSON.parse(e.target.result);
|
||||
importedData.value = json;
|
||||
} catch (error) {
|
||||
console.error("Failed to parse JSON:", error);
|
||||
importedData.value = null;
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
|
||||
function performImport() {
|
||||
importForm.post(route("importTemplates.import"), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
importModalOpen.value = false;
|
||||
importForm.reset();
|
||||
importedData.value = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
templates: Array,
|
||||
clients: Array,
|
||||
segments: Array,
|
||||
decisions: Array,
|
||||
actions: Array,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -89,9 +174,15 @@ const props = defineProps({
|
|||
}}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button as-child>
|
||||
<Link :href="route('importTemplates.create')"> Nova predloga </Link>
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" @click="openImportModal">
|
||||
<UploadIcon class="w-4 h-4 mr-2" />
|
||||
Uvozi predlogo
|
||||
</Button>
|
||||
<Button as-child>
|
||||
<Link :href="route('importTemplates.create')"> Nova predloga </Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -125,6 +216,11 @@ const props = defineProps({
|
|||
)
|
||||
"
|
||||
/>
|
||||
<ActionMenuItem
|
||||
:icon="faFileExport"
|
||||
label="Izvozi"
|
||||
@click="exportTemplate(t.uuid)"
|
||||
/>
|
||||
<ActionMenuItem
|
||||
:icon="faTrash"
|
||||
label="Izbriši"
|
||||
|
|
@ -186,5 +282,198 @@ const props = defineProps({
|
|||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<!-- Import Template Dialog -->
|
||||
<Dialog v-model:open="importModalOpen">
|
||||
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Uvozi predlogo uvoza</DialogTitle>
|
||||
<DialogDescription>
|
||||
Izberite JSON datoteko za uvoz predloge uvoza. Po potrebi zamenjajte ID-je z
|
||||
lokalnimi vrednostmi.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4 py-4">
|
||||
<div>
|
||||
<InputLabel>Datoteka</InputLabel>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
@change="handleFileChange"
|
||||
class="w-full mt-1"
|
||||
/>
|
||||
<p v-if="importForm.errors.file" class="text-sm text-destructive mt-2">
|
||||
{{ importForm.errors.file }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator v-if="importedData?.meta" />
|
||||
|
||||
<!-- Show ID mapping options if file is loaded and has meta -->
|
||||
<div v-if="importedData?.meta" class="space-y-4">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Predloga vsebuje naslednje ID-je. Izberite nove vrednosti, če želite
|
||||
zamenjati originalne ID-je:
|
||||
</p>
|
||||
|
||||
<!-- Client -->
|
||||
<div v-if="importedData.meta.client_id || clients.length > 0">
|
||||
<InputLabel for="client_uuid">Stranka</InputLabel>
|
||||
<Select v-model="importForm.client_uuid">
|
||||
<SelectTrigger id="client_uuid">
|
||||
<SelectValue placeholder="Izberi stranko (opcijsko)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="client in clients"
|
||||
:key="client.uuid"
|
||||
:value="client.uuid"
|
||||
>
|
||||
{{ client.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- Segment -->
|
||||
<div v-if="importedData.meta.segment_id">
|
||||
<InputLabel for="segment_id">
|
||||
Segment
|
||||
<span class="text-muted-foreground"
|
||||
>(trenutno: {{ importedData.meta.segment_id }})</span
|
||||
>
|
||||
</InputLabel>
|
||||
<Select v-model="importForm.segment_id">
|
||||
<SelectTrigger id="segment_id">
|
||||
<SelectValue placeholder="Obdrži original ali izberi nov" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="segment in segments"
|
||||
:key="segment.id"
|
||||
:value="segment.id.toString()"
|
||||
>
|
||||
{{ segment.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- Decision -->
|
||||
<div v-if="importedData.meta.decision_id">
|
||||
<InputLabel for="decision_id">
|
||||
Odločitev
|
||||
<span class="text-muted-foreground"
|
||||
>(trenutno: {{ importedData.meta.decision_id }})</span
|
||||
>
|
||||
</InputLabel>
|
||||
<Select v-model="importForm.decision_id">
|
||||
<SelectTrigger id="decision_id">
|
||||
<SelectValue placeholder="Obdrži original ali izberi nov" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="decision in decisions"
|
||||
:key="decision.id"
|
||||
:value="decision.id.toString()"
|
||||
>
|
||||
{{ decision.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- Action -->
|
||||
<div v-if="importedData.meta.action_id">
|
||||
<InputLabel for="action_id">
|
||||
Akcija
|
||||
<span class="text-muted-foreground"
|
||||
>(trenutno: {{ importedData.meta.action_id }})</span
|
||||
>
|
||||
</InputLabel>
|
||||
<Select v-model="importForm.action_id">
|
||||
<SelectTrigger id="action_id">
|
||||
<SelectValue placeholder="Obdrži original ali izberi nov" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="action in actions"
|
||||
:key="action.id"
|
||||
:value="action.id.toString()"
|
||||
>
|
||||
{{ action.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- Activity Action -->
|
||||
<div v-if="importedData.meta.activity_action_id">
|
||||
<InputLabel for="activity_action_id">
|
||||
Aktivnost - Akcija
|
||||
<span class="text-muted-foreground"
|
||||
>(trenutno: {{ importedData.meta.activity_action_id }})</span
|
||||
>
|
||||
</InputLabel>
|
||||
<Select v-model="importForm.activity_action_id">
|
||||
<SelectTrigger id="activity_action_id">
|
||||
<SelectValue placeholder="Obdrži original ali izberi nov" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="action in actions"
|
||||
:key="action.id"
|
||||
:value="action.id.toString()"
|
||||
>
|
||||
{{ action.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- Activity Decision -->
|
||||
<div v-if="importedData.meta.activity_decision_id">
|
||||
<InputLabel for="activity_decision_id">
|
||||
Aktivnost - Odločitev
|
||||
<span class="text-muted-foreground"
|
||||
>(trenutno: {{ importedData.meta.activity_decision_id }})</span
|
||||
>
|
||||
</InputLabel>
|
||||
<Select v-model="importForm.activity_decision_id">
|
||||
<SelectTrigger id="activity_decision_id">
|
||||
<SelectValue placeholder="Obdrži original ali izberi nov" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="decision in decisions"
|
||||
:key="decision.id"
|
||||
:value="decision.id.toString()"
|
||||
>
|
||||
{{ decision.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
@click="cancelImport"
|
||||
:disabled="importForm.processing"
|
||||
>
|
||||
Prekliči
|
||||
</Button>
|
||||
<Button
|
||||
@click="performImport"
|
||||
:disabled="importForm.processing || !importForm.file"
|
||||
>
|
||||
<span v-if="importForm.processing">Uvažam…</span>
|
||||
<span v-else>Uvozi</span>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -434,7 +434,9 @@
|
|||
Route::get('imports/templates', [ImportTemplateController::class, 'index'])->name('importTemplates.index');
|
||||
Route::get('imports/templates/create', [ImportTemplateController::class, 'create'])->name('importTemplates.create');
|
||||
Route::post('imports/templates', [ImportTemplateController::class, 'store'])->name('importTemplates.store');
|
||||
Route::post('imports/templates/import', [ImportTemplateController::class, 'import'])->name('importTemplates.import');
|
||||
Route::get('imports/templates/edit/{template:uuid}', [ImportTemplateController::class, 'edit'])->name('importTemplates.edit');
|
||||
Route::get('imports/templates/{template:uuid}/export', [ImportTemplateController::class, 'export'])->name('importTemplates.export');
|
||||
Route::put('imports/templates/{template:uuid}', [ImportTemplateController::class, 'update'])->name('importTemplates.update');
|
||||
Route::delete('imports/templates/{template:uuid}', [ImportTemplateController::class, 'destroy'])->name('importTemplates.destroy');
|
||||
Route::post('imports/templates/{template:uuid}/mappings', [ImportTemplateController::class, 'addMapping'])->name('importTemplates.mappings.add');
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user