diff --git a/apps/web/src/api/gestion.ts b/apps/web/src/api/gestion.ts index 11e619a..9d31465 100644 --- a/apps/web/src/api/gestion.ts +++ b/apps/web/src/api/gestion.ts @@ -217,13 +217,17 @@ export function useDeleteDocument() { }); } -/** Téléchargement authentifié (un lien nu ne porte pas le jeton). */ -export async function telechargerDocument(id: string, fileName: string): Promise { +/** Récupération authentifiée du fichier (un lien nu ne porte pas le jeton). */ +export async function blobDocument(id: string): Promise { const res = await fetch(`/api/documents/${id}/download`, { headers: { Authorization: `Bearer ${getToken()}` }, }); if (!res.ok) throw new Error('Téléchargement impossible'); - const blob = await res.blob(); + return res.blob(); +} + +export async function telechargerDocument(id: string, fileName: string): Promise { + const blob = await blobDocument(id); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -232,6 +236,22 @@ export async function telechargerDocument(id: string, fileName: string): Promise URL.revokeObjectURL(url); } +/** Ouverture dans un nouvel onglet (PDF et images : le navigateur les affiche). */ +export async function ouvrirDocument(id: string): Promise { + // Onglet ouvert dans le geste utilisateur, sinon les bloqueurs de pop-up + // refusent un window.open survenant après le fetch. + const onglet = window.open('', '_blank'); + try { + const blob = await blobDocument(id); + const url = URL.createObjectURL(blob); + if (onglet) onglet.location.href = url; + else window.open(url, '_blank'); + } catch (e) { + onglet?.close(); + throw e; + } +} + // ————— Analytics ————— export function useAnalyticsSummary(enabled = true) { diff --git a/apps/web/src/components/carte-documents.tsx b/apps/web/src/components/carte-documents.tsx index f664f7f..4f0af55 100644 --- a/apps/web/src/components/carte-documents.tsx +++ b/apps/web/src/components/carte-documents.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { DOCUMENT_KIND_LABELS, DOCUMENT_KINDS, @@ -6,7 +6,14 @@ import { type DocumentDto, type DocumentKind, } from '@siop/shared'; -import { telechargerDocument, useDeleteDocument, useDocuments, useUploadDocument } from '@/api/gestion'; +import { + blobDocument, + ouvrirDocument, + telechargerDocument, + useDeleteDocument, + useDocuments, + useUploadDocument, +} from '@/api/gestion'; import { Button } from '@/components/ui/button'; const CLASSE_TYPE: Partial> = { @@ -20,10 +27,46 @@ export function tailleLisible(octets: number): string { return `${Math.max(1, Math.round(octets / 1024))} Ko`; } +/** Aperçu réel des images : le fichier exige le jeton, donc pas de + * direct — on récupère le blob et on affiche une URL objet, révoquée au + * démontage. Les PDF gardent leur pictogramme (le navigateur les ouvre). */ +function ApercuVignette({ doc }: { doc: DocumentDto }) { + const estImage = doc.contentType.startsWith('image/'); + const [url, setUrl] = useState(); + + useEffect(() => { + if (!estImage) return; + let actif = true; + let objet: string | undefined; + blobDocument(doc.id) + .then((blob) => { + if (!actif) return; + objet = URL.createObjectURL(blob); + setUrl(objet); + }) + .catch(() => {}); + return () => { + actif = false; + if (objet) URL.revokeObjectURL(objet); + }; + }, [doc.id, estImage]); + + return ( + + ); +} + export function VignetteDoc({ doc, surSuppression }: { doc: DocumentDto; surSuppression?: (id: string) => void }) { return (
-
{doc.contentType.startsWith('image/') ? '🖼' : '📄'}
+ {DOCUMENT_KIND_LABELS[doc.kind]} {doc.fileName} @@ -34,6 +77,13 @@ export function VignetteDoc({ doc, surSuppression }: { doc: DocumentDto; surSupp {doc.uploadedByName ? ` · ${doc.uploadedByName}` : ''} +