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 },
// Optional: direct delete URL builder; if absent we emit 'delete'
deleteUrlBuilder: { type: Function, default: null },
edit: { type: Boolean, default: false },
});
// Derive a human-friendly source for a document: Case or Contract reference
const sourceLabel = (doc) => {
@ -322,6 +323,7 @@ function closeActions() {
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="emit('edit', doc)"
v-if="edit"
>
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-600" />
<span>Uredi</span>
@ -338,6 +340,7 @@ function closeActions() {
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"
@click="askDelete(doc)"
v-if="edit"
>
<FontAwesomeIcon :icon="faTrash" class="h-4 w-4" />
<span>Izbriši</span>

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import { router } from "@inertiajs/vue3";
const props = defineProps({
show: { type: Boolean, default: false },
contract: { type: Object, default: null },
edit: { type: Boolean, default: true },
});
const emit = defineEmits(["close"]);
@ -114,7 +115,7 @@ watch(
<span v-if="p.reference" class="ml-2">Sklic: {{ p.reference }}</span>
</div>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2" v-if="edit">
<button
type="button"
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({
show: { type: Boolean, default: false },
contract: { type: Object, default: null },
edit: { type: Boolean, default: true },
});
const emit = defineEmits(["close"]);
@ -99,20 +100,27 @@ watch(
<div>
<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 class="text-xs text-gray-500">
<span>{{ formatDate(p.paid_at) }}</span>
<span v-if="p.reference" class="ml-2">Sklic: {{ p.reference }}</span>
<span v-if="p.balance_before !== undefined" class="ml-2">
Stanje pred: {{
Intl.NumberFormat('de-DE', { style: 'currency', currency: p.currency || 'EUR' }).format(p.balance_before ?? 0)
Stanje pred:
{{
Intl.NumberFormat("de-DE", {
style: "currency",
currency: p.currency || "EUR",
}).format(p.balance_before ?? 0)
}}
</span>
</div>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2" v-if="edit">
<button
type="button"
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 #footer>
<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
</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
</button>
</div>

View File

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

View File

@ -166,7 +166,7 @@
});
// 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
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');
// client-case / documents
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}/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');
// client-case / person phone - send SMS
Route::middleware("permission:doc-edit")->group( function() {
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');
// client-case / contracts list for SMS dialog
Route::get('client-cases/{client_case:uuid}/contracts/list', [ClientCaseContoller::class, 'listContracts'])->name('clientCase.contracts.list');