Teren-app/resources/js/Pages/Imports/Create.vue
Simon Pocrnjič 8f2e5e282c Changes to UI
2025-10-18 22:56:51 +02:00

272 lines
9.4 KiB
Vue

<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import { useForm, router } from "@inertiajs/vue3";
import { ref, computed } from "vue";
import Multiselect from "vue-multiselect";
import axios from "axios";
// Props: provided by controller (clients + templates collections)
const props = defineProps({
templates: Array,
clients: Array,
});
// Basic create form (rest of workflow handled on the Continue page)
const form = useForm({
client_uuid: null,
import_template_id: null,
has_header: true,
file: null,
});
// Multiselect bridge: client
const selectedClientOption = computed({
get() {
if (!form.client_uuid) return null;
return (props.clients || []).find((c) => c.uuid === form.client_uuid) || null;
},
set(val) {
form.client_uuid = val ? val.uuid : null;
},
});
// Multiselect bridge: template
const selectedTemplateOption = computed({
get() {
if (form.import_template_id == null) return null;
return (props.templates || []).find((t) => t.id === form.import_template_id) || null;
},
set(val) {
form.import_template_id = val ? val.id : null;
},
});
// Filter templates: show globals when no client; when client selected show only that client's templates (no mixing to avoid confusion)
const filteredTemplates = computed(() => {
const cuuid = form.client_uuid;
const list = props.templates || [];
if (!cuuid) {
return list.filter((t) => t.client_id == null);
}
return list.filter((t) => t.client_uuid === cuuid || t.client_id == null);
});
const uploading = ref(false);
const dragActive = ref(false);
const uploadError = ref(null);
function onFileChange(e) {
const files = e.target.files;
if (files && files.length) {
form.file = files[0];
uploadError.value = null;
}
}
function onFileDrop(e) {
const files = e.dataTransfer?.files;
if (files && files.length) {
form.file = files[0];
uploadError.value = null;
}
dragActive.value = false;
}
async function startImport() {
uploadError.value = null;
if (!form.file) {
uploadError.value = "Najprej izberite datoteko."; // "Select a file first."
return;
}
uploading.value = true;
try {
const fd = new FormData();
fd.append("file", form.file);
if (form.import_template_id != null) {
fd.append("import_template_id", String(form.import_template_id));
}
if (form.client_uuid) {
fd.append("client_uuid", form.client_uuid);
}
fd.append("has_header", form.has_header ? "1" : "0");
const { data } = await axios.post(route("imports.store"), fd, {
headers: { Accept: "application/json" },
withCredentials: true,
});
if (data?.uuid) {
router.visit(route("imports.continue", { import: data.uuid }));
return;
}
if (data?.id) {
// Fallback if only numeric id returned
router.visit(route("imports.continue", { import: data.id }));
return;
}
uploadError.value = "Nepričakovan odgovor strežnika."; // Unexpected server response.
} catch (e) {
if (e.response?.data?.message) {
uploadError.value = e.response.data.message;
} else {
uploadError.value = "Nalaganje ni uspelo."; // Upload failed.
}
console.error("Import upload failed", e.response?.status, e.response?.data || e);
} finally {
uploading.value = false;
}
}
</script>
<template>
<AppLayout title="Nov uvoz">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Nov uvoz</h2>
</template>
<div class="py-6">
<div class="max-w-4xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white shadow sm:rounded-lg p-6 space-y-8">
<!-- Intro / guidance -->
<div class="text-sm text-gray-600 leading-relaxed">
<p class="mb-2">
1) Izberite stranko (opcijsko) in predlogo (če obstaja), 2) izberite
datoteko (CSV, TXT, XLSX*) in 3) kliknite Začni uvoz. Nadaljnje preslikave
in simulacija bodo na naslednji strani.
</p>
<p class="text-xs text-gray-500">
* XLSX podpora je odvisna od konfiguracije strežnika.
</p>
</div>
<!-- Client & Template selection -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Stranka</label>
<Multiselect
v-model="selectedClientOption"
:options="clients"
track-by="uuid"
label="name"
placeholder="Poišči stranko..."
:searchable="true"
:allow-empty="true"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Predloga</label>
<Multiselect
v-model="selectedTemplateOption"
:options="filteredTemplates"
track-by="id"
label="name"
placeholder="Poišči predlogo..."
:searchable="true"
:allow-empty="true"
>
<template #option="{ option }">
<div class="flex items-center justify-between w-full">
<span class="truncate">{{ option.name }}</span>
<span
class="ml-2 text-[10px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-600"
>{{ option.client_id ? "Client" : "Global" }}</span
>
</div>
</template>
</Multiselect>
<p class="text-xs text-gray-500 mt-1" v-if="!form.client_uuid">
Prikazane so samo globalne predloge dokler ne izberete stranke.
</p>
</div>
</div>
<!-- File + Header -->
<div class="grid grid-cols-1 gap-6 items-start">
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Datoteka</label>
<div
class="border-2 border-dashed rounded-md p-6 text-center cursor-pointer transition-colors"
:class="{
'border-indigo-400 bg-indigo-50': dragActive,
'border-gray-300 hover:border-gray-400': !dragActive,
}"
@dragover.prevent="dragActive = true"
@dragleave.prevent="dragActive = false"
@drop.prevent="onFileDrop"
>
<input
type="file"
class="hidden"
id="import-file-input"
@change="onFileChange"
/>
<label for="import-file-input" class="block cursor-pointer select-none">
<div v-if="!form.file" class="text-sm text-gray-600">
Povlecite datoteko sem ali
<span class="text-indigo-600 underline">kliknite za izbiro</span>
</div>
<div v-else class="text-sm text-gray-800 flex flex-col gap-1">
<span class="font-medium">{{ form.file.name }}</span>
<span class="text-xs text-gray-500"
>{{ (form.file.size / 1024).toFixed(1) }} kB</span
>
<span
class="text-[10px] inline-block bg-gray-100 px-1.5 py-0.5 rounded"
>Zamenjaj</span
>
</div>
</label>
</div>
</div>
<div class="space-y-2">
<label class="flex items-center gap-2 text-sm font-medium text-gray-700">
<input type="checkbox" v-model="form.has_header" class="rounded" />
<span>Prva vrstica je glava</span>
</label>
<div class="text-xs text-gray-500 leading-relaxed">
Če ni označeno, bodo stolpci poimenovani po zaporedju (A, B, C ...).
</div>
</div>
</div>
<!-- Errors -->
<div v-if="uploadError" class="text-sm text-red-600">
{{ uploadError }}
</div>
<!-- Actions -->
<div class="flex flex-wrap justify-end gap-3 pt-2">
<button
type="button"
@click="
() => {
form.file = null;
uploadError = null;
}
"
:disabled="uploading || !form.file"
class="px-4 py-2 text-sm rounded border bg-white disabled:opacity-50"
>
Počisti
</button>
<button
type="button"
@click="startImport"
:disabled="uploading"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded bg-indigo-600 disabled:bg-indigo-300 text-white text-sm font-medium shadow-sm"
>
<span
v-if="uploading"
class="h-4 w-4 border-2 border-white/60 border-t-transparent rounded-full animate-spin"
></span>
<span>{{ uploading ? "Nalagam..." : "Začni uvoz" }}</span>
</button>
</div>
<div class="text-xs text-gray-400 pt-4 border-t">
Po nalaganju boste preusmerjeni na nadaljevanje uvoza, kjer lahko izvedete
preslikave, simulacijo in končno obdelavo.
</div>
</div>
</div>
</div>
</AppLayout>
</template>