Other permissions changed

This commit is contained in:
Simon Pocrnjič 2025-10-31 12:26:58 +01:00
parent 07b1deda21
commit 0d9c8c8b30
7 changed files with 180 additions and 133 deletions

View File

@ -35,6 +35,7 @@ const props = defineProps({
downloadUrlBuilder: { type: Function, default: null }, downloadUrlBuilder: { type: Function, default: null },
// Optional: direct delete URL builder; if absent we emit 'delete' // Optional: direct delete URL builder; if absent we emit 'delete'
deleteUrlBuilder: { type: Function, default: null }, deleteUrlBuilder: { type: Function, default: null },
edit: { type: Boolean, default: false },
}); });
// Derive a human-friendly source for a document: Case or Contract reference // Derive a human-friendly source for a document: Case or Contract reference
const sourceLabel = (doc) => { const sourceLabel = (doc) => {
@ -322,6 +323,7 @@ function closeActions() {
type="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" class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
@click="emit('edit', doc)" @click="emit('edit', doc)"
v-if="edit"
> >
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-600" /> <FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-600" />
<span>Uredi</span> <span>Uredi</span>
@ -338,6 +340,7 @@ function closeActions() {
type="button" type="button"
class="w-full px-3 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2" class="w-full px-3 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
@click="askDelete(doc)" @click="askDelete(doc)"
v-if="edit"
> >
<FontAwesomeIcon :icon="faTrash" class="h-4 w-4" /> <FontAwesomeIcon :icon="faTrash" class="h-4 w-4" />
<span>Izbriši</span> <span>Izbriši</span>

View File

@ -12,6 +12,7 @@ const props = defineProps({
show: { type: Boolean, default: false }, show: { type: Boolean, default: false },
client_case: { type: Object, required: true }, client_case: { type: Object, required: true },
contract: { type: Object, default: null }, contract: { type: Object, default: null },
edit: { type: Boolean, default: true },
}); });
const emit = defineEmits(["close"]); const emit = defineEmits(["close"]);
@ -79,7 +80,7 @@ const deleteObject = (o) => {
<div class="text-xs uppercase text-gray-500">Ref.</div> <div class="text-xs uppercase text-gray-500">Ref.</div>
<div class="font-semibold text-gray-900">{{ o.reference || "-" }}</div> <div class="font-semibold text-gray-900">{{ o.reference || "-" }}</div>
</div> </div>
<div class="ml-3 flex items-center gap-2"> <div class="ml-3 flex items-center gap-2" v-if="edit">
<span <span
class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-700" class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-700"
>{{ o.type || "—" }}</span >{{ o.type || "—" }}</span

View File

@ -36,6 +36,7 @@ const props = defineProps({
all_segments: { type: Array, default: () => [] }, all_segments: { type: Array, default: () => [] },
templates: { type: Array, default: () => [] }, // active document templates (latest per slug) templates: { type: Array, default: () => [] }, // active document templates (latest per slug)
edit: { type: Boolean, default: () => false }, edit: { type: Boolean, default: () => false },
createDoc: { type: Boolean, default: () => false },
}); });
// Debug: log incoming contract balances (remove after fix) // Debug: log incoming contract balances (remove after fix)
@ -485,7 +486,7 @@ const closePaymentsDialog = () => {
> >
Opis</FwbTableHeadCell Opis</FwbTableHeadCell
> >
<FwbTableHeadCell class="w-px" v-if="edit" /> <FwbTableHeadCell class="w-px" />
</FwbTableHead> </FwbTableHead>
<FwbTableBody> <FwbTableBody>
<template v-for="(c, i) in contracts" :key="c.uuid || i"> <template v-for="(c, i) in contracts" :key="c.uuid || i">
@ -702,7 +703,7 @@ const closePaymentsDialog = () => {
</Dropdown> </Dropdown>
</div> </div>
</FwbTableCell> </FwbTableCell>
<FwbTableCell class="text-right whitespace-nowrap" v-if="edit"> <FwbTableCell class="text-right whitespace-nowrap">
<Dropdown align="right" width="56"> <Dropdown align="right" width="56">
<template #trigger> <template #trigger>
<button <button
@ -718,23 +719,25 @@ const closePaymentsDialog = () => {
</template> </template>
<template #content> <template #content>
<!-- Urejanje --> <!-- Urejanje -->
<div <template v-if="edit">
class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400" <div
> class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400"
Urejanje >
</div> Urejanje
<button </div>
type="button" <button
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2" type="button"
v-if="c.active" class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
@click="onEdit(c)" v-if="c.active"
> @click="onEdit(c)"
<FontAwesomeIcon >
:icon="faPenToSquare" <FontAwesomeIcon
class="h-4 w-4 text-gray-600" :icon="faPenToSquare"
/> class="h-4 w-4 text-gray-600"
<span>Uredi</span> />
</button> <span>Uredi</span>
</button>
</template>
<button <button
type="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" class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
@ -747,46 +750,50 @@ const closePaymentsDialog = () => {
<div class="my-1 border-t border-gray-100" /> <div class="my-1 border-t border-gray-100" />
<!-- Dokumenti --> <!-- Dokumenti -->
<div <template v-if="createDoc">
class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400" <div
> class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400"
Dokument >
</div> Dokument
<button </div>
type="button" <button
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2" type="button"
:disabled="generating[c.uuid] || !templates || templates.length === 0" class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
@click="openGenerateDialog(c)" :disabled="
> generating[c.uuid] || !templates || templates.length === 0
<FontAwesomeIcon "
:icon="generating[c.uuid] ? faSpinner : faFileWord" @click="openGenerateDialog(c)"
class="h-4 w-4 text-gray-600" >
:class="generating[c.uuid] ? 'animate-spin' : ''" <FontAwesomeIcon
/> :icon="generating[c.uuid] ? faSpinner : faFileWord"
<span>{{ class="h-4 w-4 text-gray-600"
generating[c.uuid] :class="generating[c.uuid] ? 'animate-spin' : ''"
? "Generiranje..." />
: templates && templates.length <span>{{
? "Generiraj dokument" generating[c.uuid]
: "Ni predlog" ? "Generiranje..."
}}</span> : templates && templates.length
</button> ? "Generiraj dokument"
<a : "Ni predlog"
v-if="generatedDocs[c.uuid]?.path" }}</span>
:href="'/storage/' + generatedDocs[c.uuid].path" </button>
target="_blank" <a
class="w-full px-3 py-2 text-left text-sm text-indigo-600 hover:bg-indigo-50 flex items-center gap-2" v-if="generatedDocs[c.uuid]?.path"
> :href="'/storage/' + generatedDocs[c.uuid].path"
<FontAwesomeIcon :icon="faFileWord" class="h-4 w-4" /> target="_blank"
<span>Prenesi zadnji</span> class="w-full px-3 py-2 text-left text-sm text-indigo-600 hover:bg-indigo-50 flex items-center gap-2"
</a> >
<div <FontAwesomeIcon :icon="faFileWord" class="h-4 w-4" />
v-if="generationError[c.uuid]" <span>Prenesi zadnji</span>
class="px-3 py-2 text-xs text-rose-600 whitespace-pre-wrap" </a>
> <div
{{ generationError[c.uuid] }} v-if="generationError[c.uuid]"
</div> class="px-3 py-2 text-xs text-rose-600 whitespace-pre-wrap"
<div class="my-1 border-t border-gray-100" /> >
{{ generationError[c.uuid] }}
</div>
<div class="my-1 border-t border-gray-100" />
</template>
<!-- Predmeti --> <!-- Predmeti -->
<div <div
class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400" class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400"
@ -797,6 +804,7 @@ const closePaymentsDialog = () => {
type="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" class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
@click="openObjectsList(c)" @click="openObjectsList(c)"
:edit="edit"
> >
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-600" /> <FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-600" />
<span>Seznam predmetov</span> <span>Seznam predmetov</span>
@ -835,66 +843,76 @@ const closePaymentsDialog = () => {
<FontAwesomeIcon :icon="faPlus" class="h-4 w-4 text-gray-600" /> <FontAwesomeIcon :icon="faPlus" class="h-4 w-4 text-gray-600" />
<span>Dodaj plačilo</span> <span>Dodaj plačilo</span>
</button> </button>
<template v-if="edit">
<div class="my-1 border-t border-gray-100" />
<!-- Arhiviranje / Ponovna aktivacija -->
<div
class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400"
>
{{ c.active ? "Arhiviranje" : "Ponovna aktivacija" }}
</div>
<div class="my-1 border-t border-gray-100" /> <button
<!-- Arhiviranje / Ponovna aktivacija --> v-if="c.active"
<div type="button"
class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400" class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
> @click="
{{ c.active ? "Arhiviranje" : "Ponovna aktivacija" }} router.post(
</div> route('clientCase.contract.archive', {
<button client_case: client_case.uuid,
v-if="c.active" uuid: c.uuid,
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" {},
@click=" {
router.post( preserveScroll: true,
route('clientCase.contract.archive', { only: ['contracts', 'activities', 'documents'],
client_case: client_case.uuid, }
uuid: c.uuid, )
}), "
{}, >
{ <FontAwesomeIcon
preserveScroll: true, :icon="faBoxArchive"
only: ['contracts', 'activities', 'documents'], class="h-4 w-4 text-gray-600"
} />
) <span>Arhiviraj</span>
" </button>
> <button
<FontAwesomeIcon :icon="faBoxArchive" class="h-4 w-4 text-gray-600" /> v-else
<span>Arhiviraj</span> type="button"
</button> class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
<button @click="
v-else router.post(
type="button" route('clientCase.contract.archive', {
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2" client_case: client_case.uuid,
@click=" uuid: c.uuid,
router.post( }),
route('clientCase.contract.archive', { { reactivate: true },
client_case: client_case.uuid, {
uuid: c.uuid, preserveScroll: true,
}), only: ['contracts', 'activities', 'documents'],
{ reactivate: true }, }
{ )
preserveScroll: true, "
only: ['contracts', 'activities', 'documents'], >
} <FontAwesomeIcon
) :icon="faBoxArchive"
" class="h-4 w-4 text-gray-600"
> />
<FontAwesomeIcon :icon="faBoxArchive" class="h-4 w-4 text-gray-600" /> <span>Ponovno aktiviraj</span>
<span>Ponovno aktiviraj</span> </button>
</button> </template>
<div class="my-1 border-t border-gray-100" /> <template v-if="edit">
<!-- Destruktivno --> <div class="my-1 border-t border-gray-100" />
<button <!-- Destruktivno -->
type="button" <button
class="w-full px-3 py-2 text-left text-sm text-red-700 hover:bg-red-50 flex items-center gap-2" type="button"
@click="onDelete(c)" class="w-full px-3 py-2 text-left text-sm text-red-700 hover:bg-red-50 flex items-center gap-2"
> @click="onDelete(c)"
<FontAwesomeIcon :icon="faTrash" class="h-4 w-4 text-red-600" /> >
<span>Izbriši</span> <FontAwesomeIcon :icon="faTrash" class="h-4 w-4 text-red-600" />
</button> <span>Izbriši</span>
</button>
</template>
</template> </template>
</Dropdown> </Dropdown>
</FwbTableCell> </FwbTableCell>
@ -947,6 +965,7 @@ const closePaymentsDialog = () => {
@close="closeObjectsList" @close="closeObjectsList"
:client_case="client_case" :client_case="client_case"
:contract="selectedContract" :contract="selectedContract"
:edit="edit"
/> />
<PaymentDialog <PaymentDialog
@ -960,6 +979,7 @@ const closePaymentsDialog = () => {
:show="showPaymentsDialog" :show="showPaymentsDialog"
:contract="selectedContract" :contract="selectedContract"
@close="closePaymentsDialog" @close="closePaymentsDialog"
:edit="edit"
/> />
<!-- Generate document dialog --> <!-- Generate document dialog -->

View File

@ -9,6 +9,7 @@ import { router } from "@inertiajs/vue3";
const props = defineProps({ const props = defineProps({
show: { type: Boolean, default: false }, show: { type: Boolean, default: false },
contract: { type: Object, default: null }, contract: { type: Object, default: null },
edit: { type: Boolean, default: true },
}); });
const emit = defineEmits(["close"]); const emit = defineEmits(["close"]);
@ -114,7 +115,7 @@ watch(
<span v-if="p.reference" class="ml-2">Sklic: {{ p.reference }}</span> <span v-if="p.reference" class="ml-2">Sklic: {{ p.reference }}</span>
</div> </div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2" v-if="edit">
<button <button
type="button" type="button"
class="inline-flex items-center gap-1 px-2 py-1 rounded text-red-700 hover:bg-red-50" class="inline-flex items-center gap-1 px-2 py-1 rounded text-red-700 hover:bg-red-50"

View File

@ -7,6 +7,7 @@ import axios from "axios";
const props = defineProps({ const props = defineProps({
show: { type: Boolean, default: false }, show: { type: Boolean, default: false },
contract: { type: Object, default: null }, contract: { type: Object, default: null },
edit: { type: Boolean, default: true },
}); });
const emit = defineEmits(["close"]); const emit = defineEmits(["close"]);
@ -99,20 +100,27 @@ watch(
<div> <div>
<div class="text-sm text-gray-800"> <div class="text-sm text-gray-800">
{{ {{
Intl.NumberFormat('de-DE', { style: 'currency', currency: p.currency || 'EUR' }).format(p.amount ?? 0) Intl.NumberFormat("de-DE", {
style: "currency",
currency: p.currency || "EUR",
}).format(p.amount ?? 0)
}} }}
</div> </div>
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
<span>{{ formatDate(p.paid_at) }}</span> <span>{{ formatDate(p.paid_at) }}</span>
<span v-if="p.reference" class="ml-2">Sklic: {{ p.reference }}</span> <span v-if="p.reference" class="ml-2">Sklic: {{ p.reference }}</span>
<span v-if="p.balance_before !== undefined" class="ml-2"> <span v-if="p.balance_before !== undefined" class="ml-2">
Stanje pred: {{ Stanje pred:
Intl.NumberFormat('de-DE', { style: 'currency', currency: p.currency || 'EUR' }).format(p.balance_before ?? 0) {{
Intl.NumberFormat("de-DE", {
style: "currency",
currency: p.currency || "EUR",
}).format(p.balance_before ?? 0)
}} }}
</span> </span>
</div> </div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2" v-if="edit">
<button <button
type="button" type="button"
class="inline-flex items-center gap-1 px-2 py-1 rounded text-red-700 hover:bg-red-50" class="inline-flex items-center gap-1 px-2 py-1 rounded text-red-700 hover:bg-red-50"
@ -129,10 +137,18 @@ watch(
</template> </template>
<template #footer> <template #footer>
<div class="flex justify-end gap-2 w-full"> <div class="flex justify-end gap-2 w-full">
<button type="button" class="px-3 py-2 rounded bg-gray-200 hover:bg-gray-300" @click="loadPayments"> <button
type="button"
class="px-3 py-2 rounded bg-gray-200 hover:bg-gray-300"
@click="loadPayments"
>
Osveži Osveži
</button> </button>
<button type="button" class="px-3 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700" @click="close"> <button
type="button"
class="px-3 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700"
@click="close"
>
Zapri Zapri
</button> </button>
</div> </div>

View File

@ -324,6 +324,7 @@ const submitAttachSegment = () => {
:segments="segments" :segments="segments"
:templates="contract_doc_templates" :templates="contract_doc_templates"
:edit="hasPerm('contract-edit')" :edit="hasPerm('contract-edit')"
:create-doc="hasPerm('create-docs')"
@edit="openDrawerEditContract" @edit="openDrawerEditContract"
@delete="requestDeleteContract" @delete="requestDeleteContract"
@add-activity="openDrawerAddActivity" @add-activity="openDrawerAddActivity"
@ -371,6 +372,7 @@ const submitAttachSegment = () => {
</div> </div>
<DocumentsTable <DocumentsTable
:documents="documents" :documents="documents"
:edit="hasPerm('doc-edit')"
@view="openViewer" @view="openViewer"
@edit="openDocEdit" @edit="openDocEdit"
:download-url-builder=" :download-url-builder="
@ -408,6 +410,7 @@ const submitAttachSegment = () => {
:contracts="contracts" :contracts="contracts"
@close="closeDocEdit" @close="closeDocEdit"
@saved="onDocSaved" @saved="onDocSaved"
v-if="hasPerm('doc-edit')"
/> />
<DocumentViewerDialog <DocumentViewerDialog
:show="viewer.open" :show="viewer.open"

View File

@ -166,7 +166,7 @@
}); });
// Contract document generation (JSON) - protected by auth+verified; permission enforced inside controller service // Contract document generation (JSON) - protected by auth+verified; permission enforced inside controller service
Route::post('contracts/{contract:uuid}/generate-document', \App\Http\Controllers\ContractDocumentGenerationController::class)->name('contracts.generate-document'); Route::post('contracts/{contract:uuid}/generate-document', \App\Http\Controllers\ContractDocumentGenerationController::class)->name('contracts.generate-document')->middleware("permission:create-docs");
// Phone page // Phone page
Route::get('phone', [PhoneViewController::class, 'index'])->name('phone.index'); Route::get('phone', [PhoneViewController::class, 'index'])->name('phone.index');
@ -337,13 +337,16 @@
Route::post('client-cases/{client_case:uuid}/segments', [ClientCaseContoller::class, 'attachSegment'])->name('clientCase.segments.attach'); Route::post('client-cases/{client_case:uuid}/segments', [ClientCaseContoller::class, 'attachSegment'])->name('clientCase.segments.attach');
// client-case / documents // client-case / documents
Route::post('client-cases/{client_case:uuid}/documents', [ClientCaseContoller::class, 'storeDocument'])->name('clientCase.document.store'); Route::post('client-cases/{client_case:uuid}/documents', [ClientCaseContoller::class, 'storeDocument'])->name('clientCase.document.store');
Route::patch('client-cases/{client_case:uuid}/documents/{document:uuid}', [ClientCaseContoller::class, 'updateDocument'])
->withoutScopedBindings()
->name('clientCase.document.update');
Route::get('client-cases/{client_case:uuid}/documents/{document:uuid}/view', [ClientCaseContoller::class, 'viewDocument'])->name('clientCase.document.view'); Route::get('client-cases/{client_case:uuid}/documents/{document:uuid}/view', [ClientCaseContoller::class, 'viewDocument'])->name('clientCase.document.view');
Route::get('client-cases/{client_case:uuid}/documents/{document:uuid}/download', [ClientCaseContoller::class, 'downloadDocument'])->name('clientCase.document.download'); Route::get('client-cases/{client_case:uuid}/documents/{document:uuid}/download', [ClientCaseContoller::class, 'downloadDocument'])->name('clientCase.document.download');
Route::delete('client-cases/{client_case:uuid}/documents/{document:uuid}', [ClientCaseContoller::class, 'deleteDocument'])->name('clientCase.document.delete'); Route::middleware("permission:doc-edit")->group( function() {
// client-case / person phone - send SMS Route::patch('client-cases/{client_case:uuid}/documents/{document:uuid}', [ClientCaseContoller::class, 'updateDocument'])
->withoutScopedBindings()
->name('clientCase.document.update');
Route::delete('client-cases/{client_case:uuid}/documents/{document:uuid}', [ClientCaseContoller::class, 'deleteDocument'])->name('clientCase.document.delete');
});
// client-case / person phone - send SMS
Route::post('client-cases/{client_case:uuid}/phone/{phone_id}/sms', [ClientCaseContoller::class, 'sendSmsToPhone'])->name('clientCase.phone.sms'); Route::post('client-cases/{client_case:uuid}/phone/{phone_id}/sms', [ClientCaseContoller::class, 'sendSmsToPhone'])->name('clientCase.phone.sms');
// client-case / contracts list for SMS dialog // client-case / contracts list for SMS dialog
Route::get('client-cases/{client_case:uuid}/contracts/list', [ClientCaseContoller::class, 'listContracts'])->name('clientCase.contracts.list'); Route::get('client-cases/{client_case:uuid}/contracts/list', [ClientCaseContoller::class, 'listContracts'])->name('clientCase.contracts.list');