Option to edit contract metadata

This commit is contained in:
Simon Pocrnjič 2025-11-20 18:04:33 +01:00
parent edbdb64102
commit 44f9f8f9fa
4 changed files with 285 additions and 13 deletions

View File

@ -396,6 +396,21 @@ public function updateContractSegment(ClientCase $clientCase, string $uuid, Requ
return back()->with('success', 'Contract segment updated.'); return back()->with('success', 'Contract segment updated.');
} }
public function patchContractMeta(ClientCase $clientCase, string $uuid, Request $request)
{
$validated = $request->validate([
'meta' => ['required', 'array'],
]);
$contract = $clientCase->contracts()->where('uuid', $uuid)->firstOrFail();
$contract->update([
'meta' => $validated['meta'],
]);
return back()->with('success', __('Meta podatki so bili posodobljeni.'));
}
public function attachSegment(ClientCase $clientCase, Request $request) public function attachSegment(ClientCase $clientCase, Request $request)
{ {
$validated = $request->validate([ $validated = $request->validate([

View File

@ -0,0 +1,211 @@
<script setup>
import { ref, computed, watch } from "vue";
import { useForm } from "@inertiajs/vue3";
import DialogModal from "@/Components/DialogModal.vue";
const props = defineProps({
show: { type: Boolean, default: false },
contract: { type: Object, default: null },
clientCase: { type: Object, required: true },
});
const emit = defineEmits(["close"]);
const metaFields = ref([]);
// Extract meta fields when contract changes
watch(
() => props.contract,
(c) => {
if (!c) {
metaFields.value = [];
return;
}
metaFields.value = extractMetaFields(c?.meta || {});
},
{ immediate: true }
);
function extractMetaFields(meta, parentKey = "") {
const results = [];
const visit = (node, path) => {
if (node === null || node === undefined) {
return;
}
if (Array.isArray(node)) {
node.forEach((el, idx) => visit(el, `${path}[${idx}]`));
return;
}
if (typeof node === "object") {
const hasValue = Object.prototype.hasOwnProperty.call(node, "value");
const hasTitle = Object.prototype.hasOwnProperty.call(node, "title");
if (hasValue || hasTitle) {
const title = (node.title || path || "Meta").toString().trim();
const type = node.type || (typeof node.value === "number" ? "number" : "text");
results.push({
path,
title,
value: node.value ?? "",
type,
});
return;
}
for (const [k, v] of Object.entries(node)) {
const newPath = path ? `${path}.${k}` : k;
visit(v, newPath);
}
return;
}
if (path) {
results.push({ path, title: path, value: node, type: "text" });
}
};
visit(meta, "");
return results;
}
const form = useForm({
meta: {},
});
watch(
() => props.show,
(val) => {
if (val && props.contract) {
// Rebuild meta structure for form
const metaObj = {};
metaFields.value.forEach((field) => {
setNestedValue(metaObj, field.path, {
title: field.title,
value: field.value,
type: field.type,
});
});
form.meta = metaObj;
}
}
);
function setNestedValue(obj, path, value) {
const parts = path.split(/\.|\[|\]/).filter(Boolean);
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!current[part]) {
const nextPart = parts[i + 1];
current[part] = /^\d+$/.test(nextPart) ? [] : {};
}
current = current[part];
}
current[parts[parts.length - 1]] = value;
}
function updateFieldValue(field, newValue) {
field.value = newValue;
}
function closeDialog() {
emit("close");
}
function submitForm() {
if (!props.contract?.uuid) return;
// Rebuild meta object from fields
const metaObj = {};
metaFields.value.forEach((field) => {
setNestedValue(metaObj, field.path, {
title: field.title,
value: field.value,
type: field.type,
});
});
form.meta = metaObj;
form.patch(
route("clientCase.contract.patchMeta", {
client_case: props.clientCase.uuid,
uuid: props.contract.uuid,
}),
{
preserveScroll: true,
onSuccess: () => {
closeDialog();
},
}
);
}
function formatInputType(type) {
if (type === "date") return "date";
if (type === "number") return "number";
return "text";
}
</script>
<template>
<DialogModal :show="show" max-width="2xl" @close="closeDialog">
<template #title>
Uredi meta podatke
<span v-if="contract" class="text-gray-500 font-normal">
- {{ contract.reference }}
</span>
</template>
<template #content>
<div v-if="metaFields.length === 0" class="text-sm text-gray-500">
Ni meta podatkov za urejanje.
</div>
<div v-else class="space-y-3">
<div
v-for="(field, idx) in metaFields"
:key="idx"
class="grid grid-cols-3 gap-3 items-start"
>
<div class="col-span-1">
<label class="block text-sm font-medium text-gray-700">
{{ field.title }}
</label>
<div class="text-xs text-gray-400 mt-0.5">{{ field.path }}</div>
</div>
<div class="col-span-2">
<input
v-if="
field.type !== 'text' || field.type === 'date' || field.type === 'number'
"
:type="formatInputType(field.type)"
v-model="field.value"
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 text-sm"
/>
<textarea
v-else
v-model="field.value"
rows="2"
class="w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 text-sm"
></textarea>
</div>
</div>
</div>
<div v-if="form.errors.meta" class="mt-3 text-sm text-red-600">
{{ form.errors.meta }}
</div>
</template>
<template #footer>
<button
type="button"
class="px-4 py-2 text-sm rounded-md border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 mr-2"
@click="closeDialog"
>
Prekliči
</button>
<button
type="button"
class="px-4 py-2 text-sm rounded-md bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50"
:disabled="form.processing || metaFields.length === 0"
@click="submitForm"
>
Shrani
</button>
</template>
</DialogModal>
</template>

View File

@ -11,6 +11,7 @@ import Dropdown from "@/Components/Dropdown.vue";
import CaseObjectCreateDialog from "./CaseObjectCreateDialog.vue"; import CaseObjectCreateDialog from "./CaseObjectCreateDialog.vue";
import CaseObjectsDialog from "./CaseObjectsDialog.vue"; import CaseObjectsDialog from "./CaseObjectsDialog.vue";
import PaymentDialog from "./PaymentDialog.vue"; import PaymentDialog from "./PaymentDialog.vue";
import ContractMetaPatchDialogForm from "./ContractMetaPatchDialogForm.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import ViewPaymentsDialog from "./ViewPaymentsDialog.vue"; import ViewPaymentsDialog from "./ViewPaymentsDialog.vue";
import { import {
@ -445,6 +446,18 @@ const closePaymentsDialog = () => {
showPaymentsDialog.value = false; showPaymentsDialog.value = false;
selectedContract.value = null; selectedContract.value = null;
}; };
// Meta edit dialog state
const showMetaDialog = ref(false);
const metaContract = ref(null);
const openMetaDialog = (c) => {
metaContract.value = c;
showMetaDialog.value = true;
};
const closeMetaDialog = () => {
showMetaDialog.value = false;
metaContract.value = null;
};
</script> </script>
<template> <template>
@ -632,7 +645,28 @@ const closePaymentsDialog = () => {
</button> </button>
</template> </template>
<template #content> <template #content>
<div class="min-w-[200px] max-w-xs px-3 py-2 text-sm text-gray-700"> <div class="min-w-[200px] max-w-xs text-sm text-gray-700">
<!-- Edit button in top right -->
<div
v-if="edit && hasMeta(c)"
class="px-3 pt-2 pb-1 flex items-center justify-between border-b border-gray-100"
>
<span class="text-xs font-medium text-gray-500"
>META PODATKI</span
>
<button
type="button"
class="inline-flex items-center justify-center h-6 w-6 rounded hover:bg-gray-100"
@click="openMetaDialog(c)"
title="Uredi meta podatke"
>
<FontAwesomeIcon
:icon="faPenToSquare"
class="h-3.5 w-3.5 text-gray-600"
/>
</button>
</div>
<div class="px-3 py-2">
<template v-if="hasMeta(c)"> <template v-if="hasMeta(c)">
<div <div
v-for="(m, idx) in getMetaEntries(c)" v-for="(m, idx) in getMetaEntries(c)"
@ -640,13 +674,16 @@ const closePaymentsDialog = () => {
class="py-1" class="py-1"
> >
<div class="text-gray-500 text-xs mb-0.5">{{ m.title }}</div> <div class="text-gray-500 text-xs mb-0.5">{{ m.title }}</div>
<div class="text-gray-800 font-medium break-all">{{ formatMetaValue(m) }}</div> <div class="text-gray-800 font-medium break-all">
{{ formatMetaValue(m) }}
</div>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="text-gray-500">Ni meta podatkov.</div> <div class="text-gray-500">Ni meta podatkov.</div>
</template> </template>
</div> </div>
</div>
</template> </template>
</Dropdown> </Dropdown>
@ -1160,4 +1197,12 @@ const closePaymentsDialog = () => {
</button> </button>
</template> </template>
</DialogModal> </DialogModal>
<!-- Meta Edit Dialog -->
<ContractMetaPatchDialogForm
:show="showMetaDialog"
:contract="metaContract"
:client-case="client_case"
@close="closeMetaDialog"
/>
</template> </template>

View File

@ -331,6 +331,7 @@
Route::middleware('permission:contract-edit')->group(function () { Route::middleware('permission:contract-edit')->group(function () {
Route::post('client-cases/{client_case:uuid}/contract', [ClientCaseContoller::class, 'storeContract'])->name('clientCase.contract.store'); Route::post('client-cases/{client_case:uuid}/contract', [ClientCaseContoller::class, 'storeContract'])->name('clientCase.contract.store');
Route::put('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'updateContract'])->name('clientCase.contract.update'); Route::put('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'updateContract'])->name('clientCase.contract.update');
Route::patch('client-cases/{client_case:uuid}/contract/{uuid}/meta', [ClientCaseContoller::class, 'patchContractMeta'])->name('clientCase.contract.patchMeta');
Route::delete('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'deleteContract'])->name('clientCase.contract.delete'); Route::delete('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'deleteContract'])->name('clientCase.contract.delete');
}); });