WS-3 session 1b-ii Task 4 (audit Buckets A, C, D — 26 items resolved
this commit; 24 indent items in useTimeSlotDropdown.ts remain — see
deviations).
Bucket A — Trivial fixes (12 items resolved):
- A.1: second-pass eslint --fix on App.vue resolved 4 multi-attribute
warnings. AppKpiCard / PortalLayout / PublicLayout
lines-around-comment items were attempted via blank-line addition,
but that introduced an equal number of vue/block-tag-newline
errors (the rules conflict at the SFC <script>-tag boundary). The
blank-line additions were reverted; net-zero, the 3 items remain
for a 1b-iii .eslintrc.cjs override decision.
- A.3: 6 unused-imports / unused-vars manual deletes:
* OrganisationSwitcher.vue: removed orphan toggleMenu() function
* CreateShiftDialog.vue: removed unused 'scenario' from destructure
* pages/events/[id]/time-slots/index.vue: removed unused 'event'
slot scope binding (template <#default="{ event }"> → <#default>)
* pages/organisation/companies.vue: removed unused authStore
declaration + import
* pages/platform/activity-log/index.vue: removed unused
search/searchDebounced pair
* PersonDetailPanel.vue:77: removed redundant single-statement
if-braces (curly autofix that the original pass didn't reach)
Bucket C — Style preference (8 items resolved):
- DismissFailureDialog.vue:43: collapsed two consecutive `if cond return false`
branches into `return !(cond)`
- FormFailureDetail.vue:44: replaced `void clipboard.writeText(...)` with
`clipboard.writeText(...).catch(() => {})` — fire-and-forget with
silent rejection (the no-void rule wants the void operator gone;
.catch() handles it semantically).
- AssignShiftDialog.vue:40-46: hasOverlapWarning collapsed from
always-false branching to `computed(() => false)` (the early-return
was dead code; backend enforces the constraint).
- SectionsShiftsPanel.vue:333 + registration-fields.vue:335: rewrote
`:delay-on-touch-only="true"` to attribute-shorthand `delay-on-touch-only`.
- AssignPersonDialog.vue:120-128: collapsed two `if outer { if inner ... }`
pairs into single `if (outer && inner)` form (sonarjs/no-collapsible-if).
- useImpersonationStore.ts:99-104: collapsed the same nested-if pattern
into `if (!data.data.active && state.value)`.
Bucket D — Vuetify utility class rename (5 items, 3 files):
- ml-1 → ms-1 (PersonDetailPanel:271, SectionsShiftsPanel:357,
AssignPersonDialog:496)
- pl-4 → ps-4 (AssignPersonDialog:457)
- ml-auto → ms-auto (AssignPersonDialog:471)
LTR/RTL-aware Vuetify utilities, matching the Vuexy reference idiom.
Tests + typecheck verified green.
Lint baseline: 62 → 36.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
428 lines
13 KiB
Vue
428 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { useFormFailure } from '@/composables/api/useFormFailures'
|
|
import type { FormFailureScope } from '@/composables/api/useFormFailures'
|
|
import { listenerShortName, shortId } from '@/types/form-failures'
|
|
import RetryFailureDialog from '@/components/form-failures/RetryFailureDialog.vue'
|
|
import ResolveFailureDialog from '@/components/form-failures/ResolveFailureDialog.vue'
|
|
import DismissFailureDialog from '@/components/form-failures/DismissFailureDialog.vue'
|
|
|
|
const props = defineProps<{
|
|
failureId: string
|
|
scope: FormFailureScope
|
|
orgId?: string
|
|
}>()
|
|
|
|
const failureIdRef = computed(() => props.failureId)
|
|
const orgIdRef = computed(() => props.orgId)
|
|
const { data: failure, isLoading, isError, refetch } = useFormFailure(failureIdRef, props.scope, orgIdRef)
|
|
|
|
const stateLabel = { open: 'Open', resolved: 'Opgelost', dismissed: 'Dismissed' } as const
|
|
const stateColor = { open: 'error', resolved: 'success', dismissed: 'warning' } as const
|
|
|
|
function formatDateTime(iso: string | null): string {
|
|
if (!iso)
|
|
return '—'
|
|
|
|
return new Date(iso).toLocaleDateString('nl-NL', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
}
|
|
|
|
const isOpen = computed(() => failure.value?.state === 'open')
|
|
|
|
const retryDialogOpen = ref(false)
|
|
const resolveDialogOpen = ref(false)
|
|
const dismissDialogOpen = ref(false)
|
|
|
|
function copyText(text: string): void {
|
|
if (navigator?.clipboard)
|
|
navigator.clipboard.writeText(text).catch(() => {})
|
|
}
|
|
|
|
const formattedContext = computed(() => {
|
|
if (!failure.value?.context)
|
|
return null
|
|
try {
|
|
return JSON.stringify(failure.value.context, null, 2)
|
|
}
|
|
catch {
|
|
return null
|
|
}
|
|
})
|
|
|
|
const traceExpanded = ref(false)
|
|
|
|
function formatAttemptOutcome(outcome: 'succeeded' | 'failed'): string {
|
|
return outcome === 'succeeded' ? 'Geslaagd' : 'Mislukt'
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<!-- Loading -->
|
|
<div
|
|
v-if="isLoading"
|
|
class="text-center pa-8"
|
|
>
|
|
<VProgressCircular
|
|
indeterminate
|
|
color="primary"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Error -->
|
|
<VAlert
|
|
v-else-if="isError"
|
|
type="error"
|
|
class="mb-4"
|
|
>
|
|
Kon failure niet laden.
|
|
<template #append>
|
|
<VBtn
|
|
variant="text"
|
|
@click="refetch"
|
|
>
|
|
Opnieuw proberen
|
|
</VBtn>
|
|
</template>
|
|
</VAlert>
|
|
|
|
<!-- Detail body -->
|
|
<div v-else-if="failure">
|
|
<!-- Header -->
|
|
<div class="d-flex align-start justify-space-between mb-6 flex-wrap ga-3">
|
|
<div>
|
|
<div class="d-flex align-center mb-2 ga-2">
|
|
<VChip
|
|
:color="stateColor[failure.state]"
|
|
size="large"
|
|
>
|
|
{{ stateLabel[failure.state] }}
|
|
</VChip>
|
|
<h4 class="text-h4">
|
|
Form failure
|
|
<code class="text-h5">{{ shortId(failure.id) }}</code>
|
|
</h4>
|
|
</div>
|
|
<p class="text-body-2 text-disabled mb-0">
|
|
Failed {{ formatDateTime(failure.failed_at) }} · Listener: {{ listenerShortName(failure.listener_class) }}
|
|
</p>
|
|
<p
|
|
v-if="failure.organisation_name || failure.form_schema_label"
|
|
class="text-body-2 text-disabled mb-0"
|
|
>
|
|
<span v-if="failure.organisation_name">{{ failure.organisation_name }}</span>
|
|
<span v-if="failure.organisation_name && failure.form_schema_label"> · </span>
|
|
<span v-if="failure.form_schema_label">{{ failure.form_schema_label }}</span>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="d-flex ga-2">
|
|
<VBtn
|
|
color="error"
|
|
prepend-icon="tabler-refresh"
|
|
:disabled="!isOpen"
|
|
@click="retryDialogOpen = true"
|
|
>
|
|
Retry
|
|
</VBtn>
|
|
<VBtn
|
|
color="success"
|
|
prepend-icon="tabler-check"
|
|
:disabled="!isOpen"
|
|
@click="resolveDialogOpen = true"
|
|
>
|
|
Markeren als opgelost
|
|
</VBtn>
|
|
<VBtn
|
|
color="warning"
|
|
prepend-icon="tabler-x"
|
|
:disabled="!isOpen"
|
|
@click="dismissDialogOpen = true"
|
|
>
|
|
Dismiss
|
|
</VBtn>
|
|
</div>
|
|
</div>
|
|
|
|
<VRow>
|
|
<!-- Left column 60% -->
|
|
<VCol
|
|
cols="12"
|
|
md="7"
|
|
>
|
|
<!-- Exception card -->
|
|
<VCard
|
|
title="Exception"
|
|
class="mb-4"
|
|
>
|
|
<VCardText>
|
|
<div class="mb-3">
|
|
<span class="text-caption text-disabled d-block mb-1">Class</span>
|
|
<code class="text-body-2 d-block">{{ failure.exception_class }}</code>
|
|
</div>
|
|
<div>
|
|
<span class="text-caption text-disabled d-block mb-1">Message</span>
|
|
<code class="text-body-2 d-block">{{ failure.exception_message }}</code>
|
|
</div>
|
|
<VBtn
|
|
variant="text"
|
|
size="small"
|
|
prepend-icon="tabler-copy"
|
|
class="mt-2"
|
|
@click="copyText(failure.exception_message)"
|
|
>
|
|
Bericht kopiëren
|
|
</VBtn>
|
|
|
|
<div
|
|
v-if="failure.exception_trace"
|
|
class="mt-4"
|
|
>
|
|
<VBtn
|
|
variant="text"
|
|
size="small"
|
|
:prepend-icon="traceExpanded ? 'tabler-chevron-down' : 'tabler-chevron-right'"
|
|
@click="traceExpanded = !traceExpanded"
|
|
>
|
|
Stack trace
|
|
</VBtn>
|
|
<pre
|
|
v-if="traceExpanded"
|
|
class="text-caption mt-2"
|
|
style="white-space: pre-wrap; max-height: 360px; overflow: auto; background: rgb(var(--v-theme-surface)); padding: 0.75rem; border-radius: 4px;"
|
|
>{{ failure.exception_trace }}</pre>
|
|
</div>
|
|
</VCardText>
|
|
</VCard>
|
|
|
|
<!-- Context card -->
|
|
<VCard
|
|
v-if="formattedContext"
|
|
title="Context"
|
|
class="mb-4"
|
|
>
|
|
<VCardText>
|
|
<pre
|
|
class="text-body-2"
|
|
style="white-space: pre-wrap; max-height: 300px; overflow: auto;"
|
|
>{{ formattedContext }}</pre>
|
|
<VBtn
|
|
variant="text"
|
|
size="small"
|
|
prepend-icon="tabler-copy"
|
|
class="mt-2"
|
|
@click="copyText(formattedContext)"
|
|
>
|
|
Kopieer als JSON
|
|
</VBtn>
|
|
</VCardText>
|
|
</VCard>
|
|
|
|
<!-- Resolution timeline -->
|
|
<VCard
|
|
title="Tijdlijn"
|
|
class="mb-4"
|
|
>
|
|
<VCardText>
|
|
<VTimeline
|
|
density="compact"
|
|
side="end"
|
|
>
|
|
<VTimelineItem
|
|
dot-color="error"
|
|
size="small"
|
|
>
|
|
<div class="text-body-2">
|
|
<strong>Failed</strong>
|
|
<span class="text-caption text-disabled ms-2">{{ formatDateTime(failure.failed_at) }}</span>
|
|
</div>
|
|
</VTimelineItem>
|
|
|
|
<VTimelineItem
|
|
v-if="failure.retry_count > 0"
|
|
dot-color="warning"
|
|
size="small"
|
|
>
|
|
<div class="text-body-2">
|
|
<strong>{{ failure.retry_count }} retry-poging{{ failure.retry_count === 1 ? '' : 'en' }}</strong>
|
|
</div>
|
|
</VTimelineItem>
|
|
|
|
<VTimelineItem
|
|
v-if="failure.resolved_at"
|
|
dot-color="success"
|
|
size="small"
|
|
>
|
|
<div class="text-body-2">
|
|
<strong>Opgelost</strong>
|
|
<span class="text-caption text-disabled ms-2">{{ formatDateTime(failure.resolved_at) }}</span>
|
|
<p
|
|
v-if="failure.resolved_by_user_name"
|
|
class="text-caption mb-0"
|
|
>
|
|
door {{ failure.resolved_by_user_name }}
|
|
</p>
|
|
<p
|
|
v-if="failure.resolved_note"
|
|
class="text-caption mt-1 mb-0"
|
|
>
|
|
"{{ failure.resolved_note }}"
|
|
</p>
|
|
</div>
|
|
</VTimelineItem>
|
|
|
|
<VTimelineItem
|
|
v-if="failure.dismissed_at"
|
|
dot-color="warning"
|
|
size="small"
|
|
>
|
|
<div class="text-body-2">
|
|
<strong>Dismissed</strong> ({{ failure.dismissed_reason_type }})
|
|
<span class="text-caption text-disabled ms-2">{{ formatDateTime(failure.dismissed_at) }}</span>
|
|
<p
|
|
v-if="failure.dismissed_by_user_name"
|
|
class="text-caption mb-0"
|
|
>
|
|
door {{ failure.dismissed_by_user_name }}
|
|
</p>
|
|
<p
|
|
v-if="failure.dismissed_reason_note"
|
|
class="text-caption mt-1 mb-0"
|
|
>
|
|
"{{ failure.dismissed_reason_note }}"
|
|
</p>
|
|
</div>
|
|
</VTimelineItem>
|
|
|
|
<VTimelineItem
|
|
v-if="isOpen && failure.retry_count === 0"
|
|
dot-color="info"
|
|
size="small"
|
|
>
|
|
<div class="text-body-2 text-disabled">
|
|
In afwachting van actie...
|
|
</div>
|
|
</VTimelineItem>
|
|
</VTimeline>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
|
|
<!-- Right column 40% -->
|
|
<VCol
|
|
cols="12"
|
|
md="5"
|
|
>
|
|
<!-- Submission card -->
|
|
<VCard
|
|
title="Inzending"
|
|
class="mb-4"
|
|
>
|
|
<VCardText>
|
|
<div class="d-flex justify-space-between align-center mb-2">
|
|
<span class="text-caption text-disabled">Submission ID</span>
|
|
<code class="text-body-2">{{ shortId(failure.form_submission_id) }}</code>
|
|
</div>
|
|
<VBtn
|
|
variant="text"
|
|
size="small"
|
|
prepend-icon="tabler-copy"
|
|
@click="copyText(failure.form_submission_id)"
|
|
>
|
|
Volledige ID kopiëren
|
|
</VBtn>
|
|
<p class="text-caption text-disabled mt-2 mb-0">
|
|
Submission detail-pagina is nog niet beschikbaar in v1.
|
|
</p>
|
|
</VCardText>
|
|
</VCard>
|
|
|
|
<!-- Listener card -->
|
|
<VCard
|
|
title="Listener"
|
|
class="mb-4"
|
|
>
|
|
<VCardText>
|
|
<code class="text-body-2 d-block">{{ failure.listener_class }}</code>
|
|
</VCardText>
|
|
</VCard>
|
|
|
|
<!-- Retry history card — per-attempt detail (sessie 3c) -->
|
|
<VCard
|
|
title="Retry-geschiedenis"
|
|
class="mb-4"
|
|
>
|
|
<VCardText>
|
|
<p
|
|
v-if="!failure.retry_history?.length"
|
|
class="text-body-2 text-disabled mb-0"
|
|
>
|
|
Nog niet opnieuw geprobeerd.
|
|
</p>
|
|
<VTimeline
|
|
v-else
|
|
density="compact"
|
|
side="end"
|
|
>
|
|
<VTimelineItem
|
|
v-for="attempt in failure.retry_history"
|
|
:key="attempt.id"
|
|
:dot-color="attempt.outcome === 'succeeded' ? 'success' : 'error'"
|
|
size="small"
|
|
>
|
|
<div class="text-body-2">
|
|
<strong>{{ formatAttemptOutcome(attempt.outcome) }}</strong>
|
|
<span class="text-caption text-disabled ms-2">{{ formatDateTime(attempt.attempted_at) }}</span>
|
|
<p
|
|
v-if="attempt.attempted_by_user_name"
|
|
class="text-caption mb-0"
|
|
>
|
|
door {{ attempt.attempted_by_user_name }}
|
|
</p>
|
|
<p
|
|
v-if="attempt.exception_message"
|
|
class="text-caption mt-1 mb-0"
|
|
>
|
|
<code>{{ attempt.exception_class }}</code>: {{ attempt.exception_message }}
|
|
</p>
|
|
</div>
|
|
</VTimelineItem>
|
|
</VTimeline>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
</VRow>
|
|
|
|
<!-- Action dialogs -->
|
|
<RetryFailureDialog
|
|
v-model="retryDialogOpen"
|
|
:failure="failure"
|
|
:scope="scope"
|
|
:org-id="orgId"
|
|
@success="refetch"
|
|
/>
|
|
<ResolveFailureDialog
|
|
v-model="resolveDialogOpen"
|
|
:failure="failure"
|
|
:scope="scope"
|
|
:org-id="orgId"
|
|
@success="refetch"
|
|
/>
|
|
<DismissFailureDialog
|
|
v-model="dismissDialogOpen"
|
|
:failure="failure"
|
|
:scope="scope"
|
|
:org-id="orgId"
|
|
@success="refetch"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|