refactor(directory) : gate report actions via RBAC permissions + guard report deletion
- replace hardcoded ROLE_ADMIN check with usePermissions().can('directory.{clients,prospects}.manage')
- rename misleading isAdmin prop to canManage in CommercialReportTab and ReportDocumentList
- add busy guard on delete confirmation modal to prevent duplicate DELETE on double-click
This commit is contained in:
@@ -13,12 +13,14 @@
|
|||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
:label="$t('common.cancel')"
|
:label="$t('common.cancel')"
|
||||||
button-class="w-auto px-4"
|
button-class="w-auto px-4"
|
||||||
|
:disabled="busy"
|
||||||
@click="cancel"
|
@click="cancel"
|
||||||
/>
|
/>
|
||||||
<MalioButton
|
<MalioButton
|
||||||
variant="danger"
|
variant="danger"
|
||||||
:label="$t('common.delete')"
|
:label="$t('common.delete')"
|
||||||
button-class="w-auto px-4"
|
button-class="w-auto px-4"
|
||||||
|
:disabled="busy"
|
||||||
@click="$emit('confirm')"
|
@click="$emit('confirm')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,8 +31,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
|
// Suppression en cours : on désactive les actions pour éviter un double envoi.
|
||||||
|
busy?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -39,6 +43,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
|
if (props.busy) return
|
||||||
emit('update:modelValue', false)
|
emit('update:modelValue', false)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<span v-if="reports.length">{{ $t('directory.reports.count', { n: reports.length }, reports.length) }}</span>
|
<span v-if="reports.length">{{ $t('directory.reports.count', { n: reports.length }, reports.length) }}</span>
|
||||||
</p>
|
</p>
|
||||||
<MalioButton
|
<MalioButton
|
||||||
v-if="isAdmin"
|
v-if="canManage"
|
||||||
icon-name="mdi:plus"
|
icon-name="mdi:plus"
|
||||||
icon-position="left"
|
icon-position="left"
|
||||||
button-class="w-auto px-4"
|
button-class="w-auto px-4"
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<p class="font-medium text-neutral-600">{{ $t('directory.reports.empty') }}</p>
|
<p class="font-medium text-neutral-600">{{ $t('directory.reports.empty') }}</p>
|
||||||
<p class="max-w-sm text-sm text-neutral-400">{{ $t('directory.reports.emptyHint') }}</p>
|
<p class="max-w-sm text-sm text-neutral-400">{{ $t('directory.reports.emptyHint') }}</p>
|
||||||
<MalioButton
|
<MalioButton
|
||||||
v-if="isAdmin"
|
v-if="canManage"
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
icon-name="mdi:plus"
|
icon-name="mdi:plus"
|
||||||
icon-position="left"
|
icon-position="left"
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<span v-if="report.author"> · {{ report.author.username }}</span>
|
<span v-if="report.author"> · {{ report.author.username }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isAdmin" class="flex shrink-0 gap-1">
|
<div v-if="canManage" class="flex shrink-0 gap-1">
|
||||||
<MalioButtonIcon
|
<MalioButtonIcon
|
||||||
icon="mdi:pencil-outline"
|
icon="mdi:pencil-outline"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
|
|
||||||
<!-- Documents joints -->
|
<!-- Documents joints -->
|
||||||
<div
|
<div
|
||||||
v-if="(report.documents?.length ?? 0) || isAdmin"
|
v-if="(report.documents?.length ?? 0) || canManage"
|
||||||
class="mt-3 border-t border-neutral-100 pt-3"
|
class="mt-3 border-t border-neutral-100 pt-3"
|
||||||
>
|
>
|
||||||
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-neutral-400">
|
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-neutral-400">
|
||||||
@@ -107,11 +107,11 @@
|
|||||||
<ReportDocumentList
|
<ReportDocumentList
|
||||||
v-if="report.documents?.length"
|
v-if="report.documents?.length"
|
||||||
:documents="report.documents"
|
:documents="report.documents"
|
||||||
:is-admin="isAdmin"
|
:can-manage="canManage"
|
||||||
@delete="(docId) => removeDocument(docId)"
|
@delete="(docId) => removeDocument(docId)"
|
||||||
/>
|
/>
|
||||||
<ReportDocumentUpload
|
<ReportDocumentUpload
|
||||||
v-if="isAdmin"
|
v-if="canManage"
|
||||||
:report-id="report.id"
|
:report-id="report.id"
|
||||||
@uploaded="reload"
|
@uploaded="reload"
|
||||||
/>
|
/>
|
||||||
@@ -129,6 +129,7 @@
|
|||||||
/>
|
/>
|
||||||
<ConfirmDeleteReportModal
|
<ConfirmDeleteReportModal
|
||||||
v-model="confirmOpen"
|
v-model="confirmOpen"
|
||||||
|
:busy="deleting"
|
||||||
@confirm="confirmDelete"
|
@confirm="confirmDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,7 +142,7 @@ import { useReportDocumentService } from '~/modules/directory/services/report-do
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
owner: { client?: string, prospect?: string }
|
owner: { client?: string, prospect?: string }
|
||||||
isAdmin: boolean
|
canManage: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const reportService = useCommercialReportService()
|
const reportService = useCommercialReportService()
|
||||||
@@ -155,6 +156,7 @@ const editing = ref<CommercialReport | null>(null)
|
|||||||
|
|
||||||
const confirmOpen = ref(false)
|
const confirmOpen = ref(false)
|
||||||
const pendingDelete = ref<CommercialReport | null>(null)
|
const pendingDelete = ref<CommercialReport | null>(null)
|
||||||
|
const deleting = ref(false)
|
||||||
|
|
||||||
// Le plus récent en haut (l'API ne garantit pas l'ordre).
|
// Le plus récent en haut (l'API ne garantit pas l'ordre).
|
||||||
const sortedReports = computed(() =>
|
const sortedReports = computed(() =>
|
||||||
@@ -208,11 +210,16 @@ function askDelete(report: CommercialReport): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function confirmDelete(): Promise<void> {
|
async function confirmDelete(): Promise<void> {
|
||||||
if (!pendingDelete.value) return
|
if (!pendingDelete.value || deleting.value) return
|
||||||
await reportService.remove(pendingDelete.value.id)
|
deleting.value = true
|
||||||
confirmOpen.value = false
|
try {
|
||||||
pendingDelete.value = null
|
await reportService.remove(pendingDelete.value.id)
|
||||||
await reload()
|
confirmOpen.value = false
|
||||||
|
pendingDelete.value = null
|
||||||
|
await reload()
|
||||||
|
} finally {
|
||||||
|
deleting.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeDocument(id: number): Promise<void> {
|
async function removeDocument(id: number): Promise<void> {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
{{ doc.originalName }}
|
{{ doc.originalName }}
|
||||||
</a>
|
</a>
|
||||||
<MalioButtonIcon
|
<MalioButtonIcon
|
||||||
v-if="isAdmin"
|
v-if="canManage"
|
||||||
icon="mdi:trash-can-outline"
|
icon="mdi:trash-can-outline"
|
||||||
button-class="!text-red-600"
|
button-class="!text-red-600"
|
||||||
:aria-label="$t('common.delete')"
|
:aria-label="$t('common.delete')"
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
import type { ReportDocument } from '~/modules/directory/services/dto/report-document'
|
import type { ReportDocument } from '~/modules/directory/services/dto/report-document'
|
||||||
import { useReportDocumentService } from '~/modules/directory/services/report-documents'
|
import { useReportDocumentService } from '~/modules/directory/services/report-documents'
|
||||||
|
|
||||||
defineProps<{ documents: ReportDocument[], isAdmin: boolean }>()
|
defineProps<{ documents: ReportDocument[], canManage: boolean }>()
|
||||||
defineEmits<{ delete: [id: number] }>()
|
defineEmits<{ delete: [id: number] }>()
|
||||||
|
|
||||||
const { getDownloadUrl } = useReportDocumentService()
|
const { getDownloadUrl } = useReportDocumentService()
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #report>
|
<template #report>
|
||||||
<CommercialReportTab :owner="owner" :is-admin="isAdmin" />
|
<CommercialReportTab :owner="owner" :can-manage="canManage" />
|
||||||
</template>
|
</template>
|
||||||
</MalioTabList>
|
</MalioTabList>
|
||||||
</template>
|
</template>
|
||||||
@@ -137,8 +137,8 @@ const {
|
|||||||
load,
|
load,
|
||||||
} = useDirectoryDetail(owner)
|
} = useDirectoryDetail(owner)
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const { can } = usePermissions()
|
||||||
const isAdmin = computed(() => authStore.user?.roles?.includes('ROLE_ADMIN') ?? false)
|
const canManage = computed(() => can('directory.clients.manage'))
|
||||||
|
|
||||||
const client = ref<Client | null>(null)
|
const client = ref<Client | null>(null)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #report>
|
<template #report>
|
||||||
<CommercialReportTab :owner="owner" :is-admin="isAdmin" />
|
<CommercialReportTab :owner="owner" :can-manage="canManage" />
|
||||||
</template>
|
</template>
|
||||||
</MalioTabList>
|
</MalioTabList>
|
||||||
</template>
|
</template>
|
||||||
@@ -157,8 +157,8 @@ const {
|
|||||||
load,
|
load,
|
||||||
} = useDirectoryDetail(owner)
|
} = useDirectoryDetail(owner)
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const { can } = usePermissions()
|
||||||
const isAdmin = computed(() => authStore.user?.roles?.includes('ROLE_ADMIN') ?? false)
|
const canManage = computed(() => can('directory.prospects.manage'))
|
||||||
|
|
||||||
const prospect = ref<Prospect | null>(null)
|
const prospect = ref<Prospect | null>(null)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|||||||
Reference in New Issue
Block a user