Document gen fixed
This commit is contained in:
@@ -228,16 +228,27 @@
|
||||
class="input input-bordered input-sm w-full col-span-4"
|
||||
placeholder="custom ključ (npr. order_id)"
|
||||
/>
|
||||
<input
|
||||
v-model="row.value"
|
||||
type="text"
|
||||
class="input input-bordered input-sm w-full col-span-5"
|
||||
placeholder="privzeta vrednost"
|
||||
/>
|
||||
<template v-if="row.type === 'text'">
|
||||
<textarea
|
||||
v-model="row.value"
|
||||
rows="3"
|
||||
class="textarea textarea-bordered w-full text-xs col-span-5"
|
||||
placeholder="privzeta vrednost"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<input
|
||||
v-model="row.value"
|
||||
type="text"
|
||||
class="input input-bordered input-sm w-full col-span-5"
|
||||
placeholder="privzeta vrednost"
|
||||
/>
|
||||
</template>
|
||||
<select v-model="row.type" class="select select-bordered select-sm w-full col-span-2">
|
||||
<option value="string">string</option>
|
||||
<option value="number">number</option>
|
||||
<option value="date">date</option>
|
||||
<option value="text">text</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-ghost btn-xs col-span-1" @click="removeCustomDefault(idx)">✕</button>
|
||||
</div>
|
||||
@@ -393,10 +404,21 @@ function toggleActive() {
|
||||
// Custom defaults rows state
|
||||
const baseDefaults = (props.template.meta && props.template.meta.custom_defaults) || {};
|
||||
const baseTypes = (props.template.meta && props.template.meta.custom_default_types) || {};
|
||||
// Gather detected custom tokens from template.tokens
|
||||
const detectedCustoms = Array.isArray(props.template.tokens)
|
||||
? props.template.tokens.filter((t) => typeof t === 'string' && t.startsWith('custom.')).map((t) => t.replace(/^custom\./, ''))
|
||||
: [];
|
||||
// Build a union of keys from defaults, types, and detected tokens
|
||||
const allKeysSet = new Set([
|
||||
...Object.keys(baseDefaults || {}),
|
||||
...Object.keys(baseTypes || {}),
|
||||
...detectedCustoms,
|
||||
]);
|
||||
const allKeys = Array.from(allKeysSet);
|
||||
const customRows = reactive(
|
||||
Object.keys(baseDefaults).length
|
||||
? Object.entries(baseDefaults).map(([k, v]) => ({ key: k, value: v, type: baseTypes[k] || 'string' }))
|
||||
: [{ key: "", value: "", type: 'string' }]
|
||||
allKeys.length
|
||||
? allKeys.map((k) => ({ key: k, value: baseDefaults[k] ?? '', type: baseTypes[k] || 'string' }))
|
||||
: [{ key: '', value: '', type: 'string' }]
|
||||
);
|
||||
|
||||
function addCustomDefault() {
|
||||
@@ -405,6 +427,6 @@ function addCustomDefault() {
|
||||
|
||||
function removeCustomDefault(idx) {
|
||||
customRows.splice(idx, 1);
|
||||
if (!customRows.length) customRows.push({ key: "", value: "" });
|
||||
if (!customRows.length) customRows.push({ key: "", value: "", type: 'string' });
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -189,6 +189,12 @@
|
||||
{{ template.active ? "Deaktiviraj" : "Aktiviraj" }}
|
||||
</button>
|
||||
</form>
|
||||
<form @submit.prevent="rescan">
|
||||
<button type="submit" :class="[btnBase, btnOutline]" :disabled="rescanForm.processing">
|
||||
<span v-if="rescanForm.processing">Pregledujem…</span>
|
||||
<span v-else>Ponovno preglej tokene</span>
|
||||
</button>
|
||||
</form>
|
||||
<Link
|
||||
:href="route('admin.document-templates.index')"
|
||||
:class="[btnBase, btnOutline]"
|
||||
@@ -212,9 +218,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Link } from "@inertiajs/vue3";
|
||||
import { Link, useForm } from "@inertiajs/vue3";
|
||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||
import { useForm } from "@inertiajs/vue3";
|
||||
|
||||
// Button style utility classes
|
||||
const btnBase =
|
||||
@@ -228,10 +233,18 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const toggleForm = useForm({});
|
||||
const rescanForm = useForm({});
|
||||
|
||||
function toggleActive() {
|
||||
toggleForm.post(route("admin.document-templates.toggle", template.id), {
|
||||
toggleForm.post(route("admin.document-templates.toggle", props.template.id), {
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
|
||||
function rescan() {
|
||||
rescanForm.post(route("admin.document-templates.rescan", props.template.id), {
|
||||
preserveScroll: true,
|
||||
only: ["template"],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -28,11 +28,13 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const props = defineProps({
|
||||
client: { type: Object, default: null },
|
||||
client_case: Object,
|
||||
contract_types: Array,
|
||||
contracts: { type: Array, default: () => [] },
|
||||
segments: { type: Array, default: () => [] },
|
||||
all_segments: { type: Array, default: () => [] },
|
||||
templates: { type: Array, default: () => [] }, // active document templates (latest per slug)
|
||||
});
|
||||
|
||||
// Debug: log incoming contract balances (remove after fix)
|
||||
@@ -153,28 +155,108 @@ const onAddActivity = (c) => emit("add-activity", c);
|
||||
import { ref, computed } from "vue";
|
||||
import { router, useForm } from "@inertiajs/vue3";
|
||||
import axios from "axios";
|
||||
// Document generation state
|
||||
// Document generation state/dialog
|
||||
const generating = ref({}); // contract_uuid => boolean
|
||||
const generatedDocs = ref({}); // contract_uuid => { uuid, path }
|
||||
const generationError = ref({}); // contract_uuid => message
|
||||
const showGenerateDialog = ref(false);
|
||||
const generateFor = ref(null); // selected contract
|
||||
const selectedTemplateSlug = ref(null);
|
||||
const templateTokens = ref([]);
|
||||
const templateCustomDefaults = ref({});
|
||||
const templateCustomTypes = ref({});
|
||||
const customInputs = ref({}); // { key: value } for custom.* tokens
|
||||
// Separate selectors for address overrides
|
||||
const clientAddressSource = ref("client"); // for client.person.person_address.*
|
||||
const personAddressSource = ref("case_person"); // for person.person_address.*
|
||||
|
||||
// Hard-coded slug for now; could be made a prop or dynamic select later
|
||||
const templateSlug = "contract-summary";
|
||||
const clientAddress = computed(() => {
|
||||
const addr = props.client?.person?.addresses?.[0] || null;
|
||||
return addr
|
||||
? { address: addr.address || "", post_code: addr.post_code || "", city: addr.city || "" }
|
||||
: { address: "", post_code: "", city: "" };
|
||||
});
|
||||
const casePersonAddress = computed(() => {
|
||||
const addr = props.client_case?.person?.addresses?.[0] || null;
|
||||
return addr
|
||||
? { address: addr.address || "", post_code: addr.post_code || "", city: addr.city || "" }
|
||||
: { address: "", post_code: "", city: "" };
|
||||
});
|
||||
|
||||
async function generateDocument(c) {
|
||||
const customTokenList = computed(() => (templateTokens.value || []).filter((t) => t.startsWith("custom.")));
|
||||
|
||||
function openGenerateDialog(c) {
|
||||
generateFor.value = c;
|
||||
// Prefer a template that actually has tokens; fallback to the first available
|
||||
const first = (props.templates || []).find(t => Array.isArray(t?.tokens) && t.tokens.length > 0) || (props.templates || [])[0] || null;
|
||||
selectedTemplateSlug.value = first?.slug || null;
|
||||
templateTokens.value = Array.isArray(first?.tokens) ? first.tokens : [];
|
||||
templateCustomDefaults.value = (first?.meta && first.meta.custom_defaults) || {};
|
||||
templateCustomTypes.value = (first?.meta && first.meta.custom_default_types) || {};
|
||||
// Prefill customs with defaults
|
||||
customInputs.value = {};
|
||||
for (const t of customTokenList.value) {
|
||||
const key = t.replace(/^custom\./, "");
|
||||
customInputs.value[key] = templateCustomDefaults.value?.[key] ?? "";
|
||||
}
|
||||
clientAddressSource.value = "client";
|
||||
personAddressSource.value = "case_person";
|
||||
showGenerateDialog.value = true;
|
||||
}
|
||||
|
||||
function onTemplateChange() {
|
||||
const tpl = (props.templates || []).find((t) => t.slug === selectedTemplateSlug.value);
|
||||
templateTokens.value = Array.isArray(tpl?.tokens) ? tpl.tokens : [];
|
||||
templateCustomDefaults.value = (tpl?.meta && tpl.meta.custom_defaults) || {};
|
||||
templateCustomTypes.value = (tpl?.meta && tpl.meta.custom_default_types) || {};
|
||||
// reset customs
|
||||
customInputs.value = {};
|
||||
for (const t of customTokenList.value) {
|
||||
const key = t.replace(/^custom\./, "");
|
||||
customInputs.value[key] = templateCustomDefaults.value?.[key] ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
async function submitGenerate() {
|
||||
const c = generateFor.value;
|
||||
if (!c?.uuid || generating.value[c.uuid]) return;
|
||||
const tpl = (props.templates || []).find((t) => t.slug === selectedTemplateSlug.value);
|
||||
if (!tpl) return;
|
||||
generating.value[c.uuid] = true;
|
||||
generationError.value[c.uuid] = null;
|
||||
try {
|
||||
const clientAddr = clientAddressSource.value === "case_person" ? casePersonAddress.value : clientAddress.value;
|
||||
const personAddr = personAddressSource.value === "case_person" ? casePersonAddress.value : clientAddress.value;
|
||||
const token_overrides = {
|
||||
"client.person.person_address.address": clientAddr.address,
|
||||
"client.person.person_address.post_code": clientAddr.post_code,
|
||||
"client.person.person_address.city": clientAddr.city,
|
||||
"person.person_address.address": personAddr.address,
|
||||
"person.person_address.post_code": personAddr.post_code,
|
||||
"person.person_address.city": personAddr.city,
|
||||
};
|
||||
const payload = {
|
||||
template_slug: tpl.slug,
|
||||
template_version: tpl.version,
|
||||
custom: { ...customInputs.value },
|
||||
token_overrides,
|
||||
unresolved_policy: 'fail',
|
||||
};
|
||||
const { data } = await axios.post(
|
||||
route("contracts.generate-document", { contract: c.uuid }),
|
||||
{
|
||||
template_slug: templateSlug,
|
||||
}
|
||||
payload
|
||||
);
|
||||
if (data.status === "ok") {
|
||||
generatedDocs.value[c.uuid] = { uuid: data.document_uuid, path: data.path };
|
||||
// optimistic: reload documents list (if parent provides it) – partial reload optional
|
||||
// if no tokens were found/replaced, surface a gentle warning inline
|
||||
const stats = data.stats || null;
|
||||
// Show warning only when zero tokens were found in the template (most common real issue)
|
||||
if (stats && stats.tokensFound === 0) {
|
||||
generationError.value[c.uuid] = "Opozorilo: V predlogi niso bili najdeni tokeni.";
|
||||
} else {
|
||||
generationError.value[c.uuid] = null;
|
||||
}
|
||||
showGenerateDialog.value = false;
|
||||
router.reload({ only: ["documents"] });
|
||||
} else {
|
||||
generationError.value[c.uuid] = data.message || "Napaka pri generiranju.";
|
||||
@@ -189,6 +271,10 @@ async function generateDocument(c) {
|
||||
generating.value[c.uuid] = false;
|
||||
}
|
||||
}
|
||||
function closeGenerateDialog() {
|
||||
showGenerateDialog.value = false;
|
||||
generateFor.value = null;
|
||||
}
|
||||
const showObjectDialog = ref(false);
|
||||
const showObjectsList = ref(false);
|
||||
const selectedContract = ref(null);
|
||||
@@ -653,17 +739,15 @@ const closePaymentsDialog = () => {
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
|
||||
:disabled="generating[c.uuid]"
|
||||
@click="generateDocument(c)"
|
||||
:disabled="generating[c.uuid] || !templates || templates.length===0"
|
||||
@click="openGenerateDialog(c)"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="generating[c.uuid] ? faSpinner : faFileWord"
|
||||
class="h-4 w-4 text-gray-600"
|
||||
:class="generating[c.uuid] ? 'animate-spin' : ''"
|
||||
/>
|
||||
<span>{{
|
||||
generating[c.uuid] ? "Generiranje..." : "Generiraj povzetek"
|
||||
}}</span>
|
||||
<span>{{ generating[c.uuid] ? 'Generiranje...' : (templates && templates.length ? 'Generiraj dokument' : 'Ni predlog') }}</span>
|
||||
</button>
|
||||
<a
|
||||
v-if="generatedDocs[c.uuid]?.path"
|
||||
@@ -855,4 +939,110 @@ const closePaymentsDialog = () => {
|
||||
:contract="selectedContract"
|
||||
@close="closePaymentsDialog"
|
||||
/>
|
||||
|
||||
<!-- Generate document dialog -->
|
||||
<div v-if="showGenerateDialog" class="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
<div class="bg-white rounded-lg shadow-lg p-4 w-full max-w-lg">
|
||||
<div class="text-base font-medium text-gray-900 mb-2">Generiraj dokument</div>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Predloga</label>
|
||||
<select v-model="selectedTemplateSlug" @change="onTemplateChange" class="mt-1 w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option v-if="!templates || templates.length===0" :value="null" disabled>Ni aktivnih predlog</option>
|
||||
<option v-for="t in templates" :key="t.slug" :value="t.slug">{{ t.name }} ({{ t.version }})</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Naslovi</label>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 mb-1">client.person.person_address.*</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="radio" value="client" v-model="clientAddressSource" />
|
||||
<span>Stranka</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="radio" value="case_person" v-model="clientAddressSource" />
|
||||
<span>Oseba primera</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-2 grid grid-cols-3 gap-2 text-sm">
|
||||
<div>
|
||||
<div class="text-gray-500">Naslov</div>
|
||||
<div class="text-gray-900 truncate">{{ (clientAddressSource==='case_person'?casePersonAddress:clientAddress).address || '-' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-500">Pošta</div>
|
||||
<div class="text-gray-900 truncate">{{ (clientAddressSource==='case_person'?casePersonAddress:clientAddress).post_code || '-' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-500">Kraj</div>
|
||||
<div class="text-gray-900 truncate">{{ (clientAddressSource==='case_person'?casePersonAddress:clientAddress).city || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 mb-1">person.person_address.*</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="radio" value="client" v-model="personAddressSource" />
|
||||
<span>Stranka</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input type="radio" value="case_person" v-model="personAddressSource" />
|
||||
<span>Oseba primera</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-2 grid grid-cols-3 gap-2 text-sm">
|
||||
<div>
|
||||
<div class="text-gray-500">Naslov</div>
|
||||
<div class="text-gray-900 truncate">{{ (personAddressSource==='case_person'?casePersonAddress:clientAddress).address || '-' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-500">Pošta</div>
|
||||
<div class="text-gray-900 truncate">{{ (personAddressSource==='case_person'?casePersonAddress:clientAddress).post_code || '-' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-500">Kraj</div>
|
||||
<div class="text-gray-900 truncate">{{ (personAddressSource==='case_person'?casePersonAddress:clientAddress).city || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="customTokenList.length" class="pt-2">
|
||||
<div class="text-sm font-medium text-gray-700 mb-1">Dodatna polja</div>
|
||||
<div class="space-y-2">
|
||||
<div v-for="tok in customTokenList" :key="tok" class="grid grid-cols-3 gap-2 items-start">
|
||||
<div class="col-span-1 text-sm text-gray-600">{{ tok }}</div>
|
||||
<div class="col-span-2">
|
||||
<template v-if="templateCustomTypes[tok.replace(/^custom\./,'')] === 'text'">
|
||||
<textarea
|
||||
class="w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 text-sm"
|
||||
rows="3"
|
||||
v-model="customInputs[tok.replace(/^custom\./,'')]"
|
||||
:placeholder="templateCustomDefaults[tok.replace(/^custom\./,'')] ?? 'privzeta vrednost'"
|
||||
></textarea>
|
||||
</template>
|
||||
<template v-else>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
v-model="customInputs[tok.replace(/^custom\./,'')]"
|
||||
:placeholder="templateCustomDefaults[tok.replace(/^custom\./,'')] ?? 'privzeta vrednost'"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300" @click="closeGenerateDialog">Prekliči</button>
|
||||
<button class="px-4 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700" :disabled="!selectedTemplateSlug || generating[generateFor?.uuid]" @click="submitGenerate">Generiraj</button>
|
||||
</div>
|
||||
<div v-if="generationError[generateFor?.uuid]" class="mt-2 text-sm text-rose-600">{{ generationError[generateFor?.uuid] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -31,6 +31,7 @@ const props = defineProps({
|
||||
segments: { type: Array, default: () => [] },
|
||||
all_segments: { type: Array, default: () => [] },
|
||||
current_segment: { type: Object, default: null },
|
||||
contract_doc_templates: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const showUpload = ref(false);
|
||||
@@ -287,10 +288,12 @@ const submitAttachSegment = () => {
|
||||
</div>
|
||||
</div>
|
||||
<ContractTable
|
||||
:client="client"
|
||||
:client_case="client_case"
|
||||
:contracts="contracts"
|
||||
:contract_types="contract_types"
|
||||
:segments="segments"
|
||||
:templates="contract_doc_templates"
|
||||
@edit="openDrawerEditContract"
|
||||
@delete="requestDeleteContract"
|
||||
@add-activity="openDrawerAddActivity"
|
||||
|
||||
Reference in New Issue
Block a user