Option to edit contract metadata
This commit is contained in:
parent
edbdb64102
commit
44f9f8f9fa
|
|
@ -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([
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user