Segment view contract export option

This commit is contained in:
Simon Pocrnjič
2025-12-10 20:41:10 +01:00
parent d2287ef963
commit fa54cf48f3
8 changed files with 744 additions and 57 deletions
+229
View File
@@ -2,7 +2,9 @@
import AppLayout from "@/Layouts/AppLayout.vue";
import { Link, router } from "@inertiajs/vue3";
import { ref, computed, watch } from "vue";
import axios from "axios";
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
import DialogModal from "@/Components/DialogModal.vue";
const props = defineProps({
segment: Object,
@@ -27,6 +29,82 @@ const columns = [
{ key: "account", label: "Stanje", align: "right" },
];
const exportDialogOpen = ref(false);
const exportScope = ref("current");
const exportColumns = ref(columns.map((col) => col.key));
const exportError = ref("");
const isExporting = ref(false);
const contractsCurrentPage = computed(() => props.contracts?.current_page ?? 1);
const contractsPerPage = computed(() => props.contracts?.per_page ?? 15);
const totalContracts = computed(
() => props.contracts?.total ?? props.contracts?.data?.length ?? 0
);
const currentPageCount = computed(() => props.contracts?.data?.length ?? 0);
const allColumnsSelected = computed(() => exportColumns.value.length === columns.length);
const exportDisabled = computed(() => exportColumns.value.length === 0 || isExporting.value);
function toggleAllColumns(checked) {
exportColumns.value = checked ? columns.map((col) => col.key) : [];
}
function openExportDialog() {
exportDialogOpen.value = true;
exportError.value = "";
}
function closeExportDialog() {
exportDialogOpen.value = false;
}
async function submitExport() {
if (exportColumns.value.length === 0) {
exportError.value = "Izberi vsaj en stolpec.";
return;
}
try {
exportError.value = "";
isExporting.value = true;
const payload = {
scope: exportScope.value,
columns: [...exportColumns.value],
search: search.value || "",
client: selectedClient.value || "",
page: contractsCurrentPage.value,
per_page: contractsPerPage.value,
};
const response = await axios.post(
route("segments.export", { segment: props.segment?.id ?? props.segment }),
payload,
{ responseType: "blob" }
);
const blob = new Blob([response.data], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
const filename =
extractFilenameFromHeaders(response.headers) || buildDefaultFilename();
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
exportDialogOpen.value = false;
} catch (error) {
exportError.value = "Izvoz je spodletel. Poskusi znova.";
} finally {
isExporting.value = false;
}
}
// Build client options from the full list provided by the server, so the dropdown isn't limited by current filters
const clientOptions = computed(() => {
const list = Array.isArray(props.clients) ? props.clients : [];
@@ -37,6 +115,17 @@ const clientOptions = computed(() => {
return opts.sort((a, b) => (a.label || "").localeCompare(b.label || ""));
});
const selectedClientName = computed(() => {
if (!selectedClient.value) {
return "";
}
const options = clientOptions.value || [];
const match = options.find((opt) => opt.value === selectedClient.value);
return match?.label || "";
});
// React to client selection changes by visiting the same route with updated query
watch(selectedClient, (val) => {
const query = { search: search.value };
@@ -71,6 +160,48 @@ function formatCurrency(value) {
" €"
);
}
function slugify(value) {
if (!value) {
return "data";
}
const slug = value.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "");
return slug || "data";
}
function buildDefaultFilename() {
const now = new Date();
const dd = String(now.getDate()).padStart(2, "0");
const mm = String(now.getMonth() + 1).padStart(2, "0");
const yy = String(now.getFullYear()).slice(-2);
let base = `${dd}${mm}${yy}_${slugify(props.segment?.name || "segment")}-Pogodbe`;
const clientName = selectedClientName.value;
if (clientName) {
base += `_${slugify(clientName)}`;
}
return `${base}.xlsx`;
}
function extractFilenameFromHeaders(headers) {
if (!headers) {
return null;
}
const disposition =
headers["content-disposition"] || headers["Content-Disposition"] || "";
if (!disposition) {
return null;
}
const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8Match?.[1]) {
try {
return decodeURIComponent(utf8Match[1]);
} catch (error) {
return utf8Match[1];
}
}
const asciiMatch = disposition.match(/filename="?([^";]+)"?/i);
return asciiMatch?.[1] || null;
}
</script>
<template>
@@ -124,6 +255,15 @@ function formatCurrency(value) {
empty-text="Ni pogodb v tem segmentu."
row-key="uuid"
>
<template #toolbar-extra>
<button
type="button"
class="inline-flex items-center rounded-md border border-indigo-200 bg-white px-3 py-2 text-sm font-medium text-indigo-700 shadow-sm hover:bg-indigo-50"
@click="openExportDialog"
>
Izvozi v Excel
</button>
</template>
<!-- Primer (client_case) cell with link when available -->
<template #cell-client_case="{ row }">
<Link
@@ -168,5 +308,94 @@ function formatCurrency(value) {
</DataTableServer>
</div>
</div>
<DialogModal :show="exportDialogOpen" max-width="3xl" @close="closeExportDialog">
<template #title>
<div>
<h3 class="text-lg font-semibold">Izvoz v Excel</h3>
<p class="text-sm text-gray-500">Izberi stolpce in obseg podatkov za izvoz.</p>
</div>
</template>
<template #content>
<form id="segment-export-form" class="space-y-6" @submit.prevent="submitExport">
<div>
<span class="text-sm font-semibold text-gray-700">Obseg podatkov</span>
<div class="mt-2 space-y-2">
<label class="flex items-center gap-2 text-sm text-gray-700">
<input
type="radio"
name="scope"
value="current"
class="text-indigo-600"
v-model="exportScope"
/>
Trenutna stran ({{ currentPageCount }} zapisov)
</label>
<label class="flex items-center gap-2 text-sm text-gray-700">
<input
type="radio"
name="scope"
value="all"
class="text-indigo-600"
v-model="exportScope"
/>
Celoten segment ({{ totalContracts }} zapisov)
</label>
</div>
</div>
<div>
<div class="flex items-center justify-between">
<span class="text-sm font-semibold text-gray-700">Stolpci</span>
<label class="flex items-center gap-2 text-xs text-gray-600">
<input
type="checkbox"
:checked="allColumnsSelected"
@change="toggleAllColumns($event.target.checked)"
/>
Označi vse
</label>
</div>
<div class="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
<label
v-for="col in columns"
:key="col.key"
class="flex items-center gap-2 rounded border border-gray-200 px-3 py-2 text-sm"
>
<input
type="checkbox"
name="columns[]"
:value="col.key"
v-model="exportColumns"
class="text-indigo-600"
/>
{{ col.label }}
</label>
</div>
<p v-if="exportError" class="mt-2 text-sm text-red-600">{{ exportError }}</p>
</div>
</form>
</template>
<template #footer>
<div class="flex flex-row gap-2">
<button
type="button"
class="text-sm text-gray-600 hover:text-gray-900"
@click="closeExportDialog"
>
Prekliči
</button>
<button
type="submit"
form="segment-export-form"
class="inline-flex items-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-60"
:disabled="exportDisabled"
>
<span v-if="!isExporting">Prenesi Excel</span>
<span v-else>Pripravljam ...</span>
</button>
</div>
</template>
</DialogModal>
</AppLayout>
</template>
+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title inertia>{{ config('app.name', 'Laravel') }}</title>