feat(admin): add document preview functionality to adult verification

- Implemented a preview section for adult verification documents, allowing users to view images and PDFs inline.
- Added localization support for preview titles and messages in German, English, and Spanish.
- Enhanced the component's state management to handle preview visibility and cleanup.
This commit is contained in:
Torsten Schulz (local)
2026-03-27 10:50:28 +01:00
parent 0dd2bce5d1
commit 26eb7b8ce7
4 changed files with 87 additions and 6 deletions

View File

@@ -42,6 +42,9 @@
"reject": "Ablehnen", "reject": "Ablehnen",
"resetPending": "Auf Prüfung setzen", "resetPending": "Auf Prüfung setzen",
"openDocument": "Dokument ansehen", "openDocument": "Dokument ansehen",
"previewTitle": "Nachweis-Vorschau",
"closePreview": "Vorschau schließen",
"previewUnavailable": "Für diesen Dateityp ist hier keine Vorschau verfügbar.",
"empty": "Keine passenden Anfragen gefunden.", "empty": "Keine passenden Anfragen gefunden.",
"loadError": "Die Freigaben konnten nicht geladen werden.", "loadError": "Die Freigaben konnten nicht geladen werden.",
"updateError": "Der Status konnte nicht geändert werden.", "updateError": "Der Status konnte nicht geändert werden.",

View File

@@ -42,6 +42,9 @@
"reject": "Reject", "reject": "Reject",
"resetPending": "Set pending", "resetPending": "Set pending",
"openDocument": "Open document", "openDocument": "Open document",
"previewTitle": "Proof preview",
"closePreview": "Close preview",
"previewUnavailable": "No inline preview is available for this file type.",
"empty": "No matching requests found.", "empty": "No matching requests found.",
"loadError": "Could not load approvals.", "loadError": "Could not load approvals.",
"updateError": "Could not update the status.", "updateError": "Could not update the status.",

View File

@@ -42,6 +42,9 @@
"reject": "Rechazar", "reject": "Rechazar",
"resetPending": "Poner en revisión", "resetPending": "Poner en revisión",
"openDocument": "Abrir documento", "openDocument": "Abrir documento",
"previewTitle": "Vista previa de la prueba",
"closePreview": "Cerrar vista previa",
"previewUnavailable": "No hay vista previa integrada para este tipo de archivo.",
"empty": "No se han encontrado solicitudes.", "empty": "No se han encontrado solicitudes.",
"loadError": "No se pudieron cargar las aprobaciones.", "loadError": "No se pudieron cargar las aprobaciones.",
"updateError": "No se pudo actualizar el estado.", "updateError": "No se pudo actualizar el estado.",

View File

@@ -66,6 +66,24 @@
</tbody> </tbody>
</table> </table>
</section> </section>
<section v-if="previewUrl" class="adult-verification__preview surface-card">
<div class="adult-verification__preview-header">
<div>
<strong>{{ $t('admin.adultVerification.previewTitle') }}</strong>
<span v-if="previewRow">{{ previewRow.username }} · {{ previewRow.adultVerificationRequest?.originalName }}</span>
</div>
<button type="button" class="secondary" @click="clearPreview">
{{ $t('admin.adultVerification.closePreview') }}
</button>
</div>
<img v-if="isImagePreview" :src="previewUrl" class="adult-verification__preview-image" />
<iframe v-else-if="isPdfPreview" :src="previewUrl" class="adult-verification__preview-pdf" />
<div v-else class="adult-verification__state">
{{ $t('admin.adultVerification.previewUnavailable') }}
</div>
</section>
</div> </div>
</template> </template>
@@ -79,7 +97,10 @@ export default {
return { return {
loading: false, loading: false,
statusFilter: 'pending', statusFilter: 'pending',
rows: [] rows: [],
previewUrl: '',
previewMimeType: '',
previewRow: null
}; };
}, },
computed: { computed: {
@@ -90,6 +111,12 @@ export default {
{ value: 'rejected', label: this.$t('admin.adultVerification.filters.rejected') }, { value: 'rejected', label: this.$t('admin.adultVerification.filters.rejected') },
{ value: 'all', label: this.$t('admin.adultVerification.filters.all') } { value: 'all', label: this.$t('admin.adultVerification.filters.all') }
]; ];
},
isImagePreview() {
return this.previewMimeType.startsWith('image/');
},
isPdfPreview() {
return this.previewMimeType === 'application/pdf';
} }
}, },
methods: { methods: {
@@ -109,6 +136,7 @@ export default {
}, },
async changeFilter(filter) { async changeFilter(filter) {
this.statusFilter = filter; this.statusFilter = filter;
this.clearPreview();
await this.load(); await this.load();
}, },
async setStatus(row, status) { async setStatus(row, status) {
@@ -125,16 +153,28 @@ export default {
const response = await apiClient.get(`/api/admin/users/${row.id}/adult-verification/document`, { const response = await apiClient.get(`/api/admin/users/${row.id}/adult-verification/document`, {
responseType: 'blob' responseType: 'blob'
}); });
const url = window.URL.createObjectURL(response.data); this.clearPreview();
window.open(url, '_blank', 'noopener'); this.previewMimeType = response.data.type || '';
window.setTimeout(() => window.URL.revokeObjectURL(url), 10000); this.previewUrl = window.URL.createObjectURL(response.data);
this.previewRow = row;
} catch (error) { } catch (error) {
showApiError(this, error, this.$t('admin.adultVerification.documentError')); showApiError(this, error, this.$t('admin.adultVerification.documentError'));
} }
},
clearPreview() {
if (this.previewUrl) {
window.URL.revokeObjectURL(this.previewUrl);
}
this.previewUrl = '';
this.previewMimeType = '';
this.previewRow = null;
} }
}, },
async mounted() { async mounted() {
await this.load(); await this.load();
},
beforeUnmount() {
this.clearPreview();
} }
}; };
</script> </script>
@@ -147,7 +187,8 @@ export default {
.adult-verification__hero, .adult-verification__hero,
.adult-verification__filters, .adult-verification__filters,
.adult-verification__list { .adult-verification__list,
.adult-verification__preview {
background: linear-gradient(180deg, rgba(255, 252, 247, 0.97), rgba(250, 244, 235, 0.95)); background: linear-gradient(180deg, rgba(255, 252, 247, 0.97), rgba(250, 244, 235, 0.95));
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
@@ -232,10 +273,41 @@ export default {
background: transparent; background: transparent;
} }
.adult-verification__preview-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.adult-verification__preview-header > div {
display: grid;
gap: 4px;
}
.adult-verification__preview-image,
.adult-verification__preview-pdf {
width: 100%;
border-radius: var(--radius-md);
border: 1px solid rgba(93, 64, 55, 0.08);
background: rgba(255, 255, 255, 0.78);
}
.adult-verification__preview-image {
max-height: 70vh;
object-fit: contain;
}
.adult-verification__preview-pdf {
min-height: 70vh;
}
@media (max-width: 760px) { @media (max-width: 760px) {
.adult-verification__hero, .adult-verification__hero,
.adult-verification__filters, .adult-verification__filters,
.adult-verification__list { .adult-verification__list,
.adult-verification__preview {
padding: 18px; padding: 18px;
} }