From ff6602dff3e5d8d49a2d82bf9f49e8e18c10f154 Mon Sep 17 00:00:00 2001 From: LathanDevers Date: Sat, 1 Aug 2026 22:24:49 +0200 Subject: [PATCH] add downloadable content in vault --- src/components/ui/Button.tsx | 2 +- src/components/ui/DocumentObject.tsx | 63 ++++++++++++++++++++++ src/components/ui/TicketRow.tsx | 2 +- src/components/widgets/DocumentsWidget.tsx | 49 +++-------------- src/components/widgets/TicketsWidget.tsx | 2 +- src/hooks/useDocumentDownload.ts | 45 ++++++++++++++++ src/pages/DocumentVault.tsx | 14 +---- src/styles/Neumorphism.ts | 38 ++++++------- src/types/Document.ts | 14 +++-- 9 files changed, 148 insertions(+), 81 deletions(-) create mode 100644 src/components/ui/DocumentObject.tsx create mode 100644 src/hooks/useDocumentDownload.ts diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 962e162..75177bf 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -64,7 +64,7 @@ export const Button = forwardRef( {isLoading && ( diff --git a/src/components/ui/DocumentObject.tsx b/src/components/ui/DocumentObject.tsx new file mode 100644 index 0000000..0403c79 --- /dev/null +++ b/src/components/ui/DocumentObject.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Download } from 'lucide-react'; +import { cn } from '@/utils/utils'; +import { NEUMORPHISM } from '@/styles/Neumorphism'; +import type { Document } from '@/types/Document'; +import { Button } from './Button'; +import { useDocumentDownload } from '@/hooks/useDocumentDownload'; + +interface DocumentObjectProps { + doc: Document; +} + +export const DocumentObject: React.FC = ({ doc }) => { + // 2. On initialise le Hook pour ce document spécifique + const { downloadFile, isDownloading } = useDocumentDownload(); + + const getCategoryBadge = (category: string) => { + switch (category) { + case 'Audit': return 'bg-purple-50 text-purple-700 border-purple-200'; + case 'Facture': return 'bg-slate-100 text-slate-700 border-slate-200'; + case 'Rapport de conformité': return 'bg-emerald-50 text-emerald-700 border-emerald-200'; + default: return 'bg-blue-50 text-blue-700 border-blue-200'; + } + }; + + return ( +
+ +
+ + + +
+
{doc.title}
+
{doc.file} • PDF
+
+
+ + + + {doc.category} + + + + {/* Petit conseil d'optimisation : utiliser une fonction date pour éviter les crashs si doc.created est mal formaté */} + {doc.created ? `${doc.created.split(" ")[0]} ${doc.created.split(" ")[1]?.substring(0,5)}` : 'N/A'} + + + {/* 3. On relie le bouton à l'état et à la fonction du Hook */} + + + + ); +}; \ No newline at end of file diff --git a/src/components/ui/TicketRow.tsx b/src/components/ui/TicketRow.tsx index 681f8ad..75c8dc8 100644 --- a/src/components/ui/TicketRow.tsx +++ b/src/components/ui/TicketRow.tsx @@ -11,7 +11,7 @@ export const TicketRow: React.FC = ({ ticket, onClick }) => { return (
onClick(ticket.id)} - className="p-6 flex items-center justify-between hover:bg-slate-50 transition-colors cursor-pointer group" + className="p-6 flex items-center justify-between hover:bg-amber-50 transition-colors" >
diff --git a/src/components/widgets/DocumentsWidget.tsx b/src/components/widgets/DocumentsWidget.tsx index 8230f7a..a684c58 100644 --- a/src/components/widgets/DocumentsWidget.tsx +++ b/src/components/widgets/DocumentsWidget.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { useDocuments } from '@/hooks/useDocuments'; +import {DocumentObject} from '@ui/DocumentObject' import { Button } from '../ui/Button'; +import { Card } from '../ui/Card'; interface DocumentsWidgetProps { onSelectDocument: (id: string) => void; @@ -8,19 +10,10 @@ interface DocumentsWidgetProps { export function DocumentsWidget(){ const { documents } = useDocuments(); - - const getCategoryBadge = (category: string) => { - switch (category) { - case 'Audit': return 'bg-purple-50 text-purple-700 border-purple-200'; - case 'Facture': return 'bg-slate-100 text-slate-700 border-slate-200'; - case 'Rapport de conformité': return 'bg-emerald-50 text-emerald-700 border-emerald-200'; - default: return 'bg-blue-50 text-blue-700 border-blue-200'; - } - }; return ( -
+ - + - + {documents.map((doc) => ( - - - - - - + ))}
Nom du fichier @@ -36,41 +29,13 @@ export function DocumentsWidget(){
-
- - - -
-
{doc.title}
-
{doc.file.size} • PDF
-
-
-
- - {doc.category} - - - {doc.created.split(" ")[0]+" "+doc.created.split(" ")[1].split(":")[0]+":"+doc.created.split(" ")[1].split(":")[1]} - - -
-
+ ); }; diff --git a/src/components/widgets/TicketsWidget.tsx b/src/components/widgets/TicketsWidget.tsx index 7845a72..0f83517 100644 --- a/src/components/widgets/TicketsWidget.tsx +++ b/src/components/widgets/TicketsWidget.tsx @@ -13,7 +13,7 @@ export const TicketsWidget: React.FC = ({ onSelectTicket }) const { tickets, isLoading, error } = useTickets(); return ( - + {/* En-tête du Widget */}
diff --git a/src/hooks/useDocumentDownload.ts b/src/hooks/useDocumentDownload.ts new file mode 100644 index 0000000..9ea383e --- /dev/null +++ b/src/hooks/useDocumentDownload.ts @@ -0,0 +1,45 @@ +import { useState } from 'react'; +// Importez votre instance PocketBase +import {pb} from '@/services/pocketbase'; +import type { Document } from '@/types/Document'; + +export function useDocumentDownload() { + const [isDownloading, setIsDownloading] = useState(false); + const [error, setError] = useState(null); + + const downloadFile = async (doc: Document) => { + try { + setIsDownloading(true); + setError(null); + + // 1. Demander un jeton éphémère de téléchargement (Sécurité Max) + // Ce jeton autorise le navigateur à télécharger un fichier protégé sans exposer votre session + const fileToken = await pb.files.getToken(); + + // 2. Générer l'URL du fichier + // ATTENTION : Pour que getUrl fonctionne, 'doc' DOIT contenir doc.id ET doc.collectionId (ou doc.collectionName) + const fileUrl = pb.files.getUrl(doc, doc.file); + + // 3. Ajouter le jeton sécurisé et forcer le téléchargement (?download=1) + const downloadUrl = `${fileUrl}?token=${fileToken}&download=1`; + + // 4. Créer un lien invisible pour forcer le navigateur à télécharger + const link = document.createElement("a"); + link.href = downloadUrl; + // On s'assure d'avoir un nom de fichier par défaut si doc.title est vide + link.setAttribute("download", doc.title || "document_aegis.pdf"); + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + } catch (err) { + console.error("Erreur lors du téléchargement sécurisé :", err); + setError("Accès refusé au coffre-fort documentaire."); + } finally { + setIsDownloading(false); + } + }; + + return { downloadFile, isDownloading, error }; +} \ No newline at end of file diff --git a/src/pages/DocumentVault.tsx b/src/pages/DocumentVault.tsx index f4576aa..f34cb92 100644 --- a/src/pages/DocumentVault.tsx +++ b/src/pages/DocumentVault.tsx @@ -1,4 +1,5 @@ import { Card } from '@/components/ui/Card'; +import { Input } from '@/components/ui/Input'; import PageHeader from '@/components/ui/PageHeader'; import DocumentsWidget from '@/components/widgets/DocumentsWidget'; import { useState } from 'react'; @@ -13,15 +14,6 @@ const mockDocuments = [ export default function DocumentVault() { const [searchTerm, setSearchTerm] = useState(''); - const getCategoryBadge = (category: string) => { - switch (category) { - case 'Audit': return 'bg-purple-50 text-purple-700 border-purple-200'; - case 'Facture': return 'bg-slate-100 text-slate-700 border-slate-200'; - case 'Rapport de conformité': return 'bg-emerald-50 text-emerald-700 border-emerald-200'; - default: return 'bg-blue-50 text-blue-700 border-blue-200'; - } - }; - return (
@@ -40,19 +32,17 @@ export default function DocumentVault() {
- setSearchTerm(e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-slate-300 rounded-lg leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-blue-900 focus:border-blue-900 sm:text-sm shadow-sm" />
{/* Liste des Documents (La Carte) */} -
); diff --git a/src/styles/Neumorphism.ts b/src/styles/Neumorphism.ts index d660ac8..3ab86a8 100644 --- a/src/styles/Neumorphism.ts +++ b/src/styles/Neumorphism.ts @@ -3,31 +3,31 @@ export const NEUMORPHISM = { // Reliefs au repos - lightShadow: "shadow-[6px_6px_12px_#c5c5c5,-6px_-6px_12px_#ffffff]", - darkShadow: "dark:shadow-[6px_6px_12px_#121926,-6px_-6px_12px_#2a3850]", + lightShadow: "shadow-[6px_6px_12px_#c5c5c5,-6px_-6px_12px_#ffffff]", + darkShadow: "dark:shadow-[6px_6px_12px_#121926,-6px_-6px_12px_#2a3850]", - lightShadowHover: "hover:shadow-[6px_6px_12px_#c5c5c5,-6px_-6px_12px_#ffffff]", - darkShadowHover: "dark:hover:shadow-[6px_6px_12px_#121926,-6px_-6px_12px_#2a3850]", + lightShadowHover: "hover:shadow-[6px_6px_12px_#c5c5c5,-6px_-6px_12px_#ffffff]", + darkShadowHover: "dark:hover:shadow-[6px_6px_12px_#121926,-6px_-6px_12px_#2a3850]", - // Ombres Neumorphic quand on clique (enfoncé) - lightActive: "active:shadow-[4px_4px_12px_#c5c5c5,-4px_-4px_12px_#ffffff]", - darkActive: "dark:active:shadow-[4px_4px_12px_#121926,-4px_-4px_12px_#2a3850]", + // Ombres Neumorphic quand on clique (enfoncé) + lightActive: "active:shadow-[4px_4px_12px_#c5c5c5,-4px_-4px_12px_#ffffff]", + darkActive: "dark:active:shadow-[4px_4px_12px_#121926,-4px_-4px_12px_#2a3850]", - - insetLightActive: "active:shadow-[inset_4px_4px_8px_#c5c5c5,inset_-4px_-4px_8px_#ffffff]", - insetDarkActive: "dark:active:shadow-[inset_4px_4px_8px_#121926,inset_-4px_-4px_8px_#2a3850]", - insetLightHover: "hover:shadow-[inset_4px_4px_8px_#c5c5c5,inset_-4px_-4px_8px_#ffffff]", - insetDarkHover: "dark:hover:shadow-[inset_4px_4px_8px_#121926,inset_-4px_-4px_8px_#2a3850]", + insetLightActive: "active:shadow-[inset_4px_4px_8px_#c5c5c5,inset_-4px_-4px_8px_#ffffff]", + insetDarkActive: "dark:active:shadow-[inset_4px_4px_8px_#121926,inset_-4px_-4px_8px_#2a3850]", - sizeStyles: { - sm: "px-5 py-2 text-sm rounded-md", - md: "px-[1.7em] py-[0.7em] text-[18px] rounded-[0.5em]", + insetLightHover: "hover:shadow-[inset_4px_4px_8px_#c5c5c5,inset_-4px_-4px_8px_#ffffff]", + insetDarkHover: "dark:hover:shadow-[inset_4px_4px_8px_#121926,inset_-4px_-4px_8px_#2a3850]", + + sizeStyles: { + sm: "px-5 py-2 text-sm rounded-md", + md: "px-[1.7em] py-[0.7em] text-[18px] rounded-[0.5em]", lg: "px-10 py-4 text-xl rounded-xl", icon: "p-3 rounded-lg", - }, + }, // Ombres du bouton ACTIF (effet enfoncé/creusé "inset") - insetLight: "shadow-[inset_4px_4px_8px_#c5c5c5,inset_-4px_-4px_8px_#ffffff]", - insetDark: "dark:shadow-[inset_4px_4px_8px_#121926,inset_-4px_-4px_8px_#2a3850]", + insetLight: "shadow-[inset_4px_4px_8px_#c5c5c5,inset_-4px_-4px_8px_#ffffff]", + insetDark: "dark:shadow-[inset_4px_4px_8px_#121926,inset_-4px_-4px_8px_#2a3850]", - } as const; \ No newline at end of file +} as const; \ No newline at end of file diff --git a/src/types/Document.ts b/src/types/Document.ts index a2b7f4e..df3c7ac 100644 --- a/src/types/Document.ts +++ b/src/types/Document.ts @@ -1,7 +1,11 @@ export interface Document { - id: string; - title: string; - category: string; - file: File; - created: string; + id: string; + collectionId: string; // <-- OBLIGATOIRE pour pb.files.getUrl() + collectionName: string; // (ex: 'aegis_documents_vault') + created: string; + updated: string; + title: string; + file: string; // Le nom du fichier généré par PocketBase (ex: 'rapport_7b3a.pdf') + category: string; + company?: string; // L'ID du client (Relation) } \ No newline at end of file