Merge branch 'dev' of https://git.4nkweb.com/4nk/docv into dev

This commit is contained in:
Sadrinho27 2025-11-12 10:58:55 +01:00
commit 7c09b346ec
4 changed files with 294 additions and 12 deletions

View File

@ -309,25 +309,79 @@ export default function DashboardPage() {
Fichiers
</CardTitle>
</CardHeader>
{/* <CardContent>
{selectedFolder.files && selectedFolder.files.length > 0 ? (
<CardContent>
{(() => {
const files = selectedFolder?.attachedFiles;
if (!files) return false;
if (typeof files === 'object') {
return Object.keys(files).length > 0;
}
return false;
})() ? (
<div className="space-y-3">
{selectedFolder.files.map((file, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-700 rounded-lg">
{Object.entries(selectedFolder.attachedFiles || {}).map(([key, file]: [string, any]) => {
return (
<div key={key} className="flex items-center justify-between p-3 bg-gray-700 rounded-lg">
<div className="flex items-center space-x-3 min-w-0">
<FileText className="h-5 w-5 text-gray-400 flex-shrink-0" />
<div className="flex-shrink-0">
{(file instanceof Map ? file.get('type') : file?.type)?.startsWith('image/') ? (
<svg className="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
</svg>
) : (file instanceof Map ? file.get('type') : file?.type) === 'application/pdf' ? (
<svg className="w-5 h-5 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clipRule="evenodd" />
</svg>
) : (
<svg className="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clipRule="evenodd" />
</svg>
)}
</div>
<div className="min-w-0">
<p className="text-sm font-medium text-gray-100 truncate">{file.name || 'Fichier'}</p>
<p className="text-xs text-gray-400">{formatBytes(file.size || 0)}</p>
<p className="text-sm font-medium text-gray-100 truncate">
{(file instanceof Map ? file.get('name') : file?.name) || 'Fichier'}
</p>
<p className="text-xs text-gray-400">
{(() => {
const size = file instanceof Map ? file.get('size') : file?.size;
return size ? formatBytes(size) : 'Taille inconnue';
})()}
</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => {
const name = file instanceof Map ? file.get('name') : file?.name;
const type = file instanceof Map ? file.get('type') : file?.type;
const base64Data = file instanceof Map ? file.get('base64Data') : file?.base64Data;
if (base64Data && type && name) {
const link = document.createElement('a');
link.href = `data:${type};base64,${base64Data}`;
link.download = name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}}
className="text-blue-400 hover:text-blue-300 hover:bg-gray-600"
disabled={!(file instanceof Map ? file.get('base64Data') : file?.base64Data)}
>
<UploadCloud className="h-4 w-4" />
</Button>
</div>
))}
);
})}
</div>
) : (
<p className="text-gray-500">Aucun fichier dans ce dossier.</p>
)}
</CardContent> */}
</CardContent>
</Card>
</div>
</>

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState, memo } from 'react';
import Modal from './Modal';
import type { FolderData } from '@/lib/4nk/models/FolderData';
import type { FolderData, AttachedFile } from '@/lib/4nk/models/FolderData';
import { MemberAutocomplete } from '../ui/member-autocomplete';
type FolderType = 'contrat' | 'projet' | 'rapport' | 'finance' | 'rh' | 'marketing' | 'autre';
@ -30,7 +30,8 @@ const defaultFolder: FolderData = {
updated_at: new Date().toISOString(),
notes: [],
messages: [],
messages_owner: []
messages_owner: [],
attachedFiles: []
};
function capitalize(s?: string) {
@ -38,6 +39,46 @@ function capitalize(s?: string) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
// Types de fichiers autorisés
const ALLOWED_FILE_TYPES = {
'application/pdf': '.pdf',
'image/png': '.png',
'image/jpeg': '.jpg,.jpeg',
'image/gif': '.gif',
'image/webp': '.webp',
'text/plain': '.txt',
'application/msword': '.doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/vnd.ms-excel': '.xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx'
};
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
// Fonction pour convertir un fichier en base64
const fileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const result = reader.result as string;
// Enlever le préfixe "data:type/subtype;base64,"
const base64 = result.split(',')[1];
resolve(base64);
};
reader.onerror = error => reject(error);
});
};
// Fonction pour formater la taille des fichiers
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// Mapping des couleurs
const folderColors: Record<FolderType, { bg: string; border: string; focus: string; button: string }> = {
contrat: { bg: 'bg-blue-50 dark:bg-blue-900', border: 'border-blue-300 dark:border-blue-700', focus: 'focus:ring-blue-400 dark:focus:ring-blue-600', button: 'bg-blue-500 hover:bg-blue-600' },
@ -64,6 +105,9 @@ function FolderModal({
const [currentNote, setCurrentNote] = useState('');
// --- NOUVEAU: État pour les membres sélectionnés ---
const [selectedMembers, setSelectedMembers] = useState<string[]>([]);
// --- NOUVEAU: États pour la gestion des fichiers ---
const [isUploading, setIsUploading] = useState(false);
const [uploadError, setUploadError] = useState<string | null>(null);
useEffect(() => {
if (isOpen) {
@ -99,6 +143,74 @@ function FolderModal({
setFolderData(prev => ({ ...prev, notes: (prev.notes || []).filter(n => n !== note) }));
};
// --- NOUVEAU: Fonctions pour la gestion des fichiers ---
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (!files || files.length === 0) return;
setIsUploading(true);
setUploadError(null);
try {
const newFiles: AttachedFile[] = [];
for (const file of Array.from(files)) {
// Vérifier le type de fichier
if (!Object.keys(ALLOWED_FILE_TYPES).includes(file.type)) {
throw new Error(`Type de fichier non autorisé: ${file.type}`);
}
// Vérifier la taille
if (file.size > MAX_FILE_SIZE) {
throw new Error(`Fichier trop volumineux: ${file.name} (${formatFileSize(file.size)}). Taille maximale: ${formatFileSize(MAX_FILE_SIZE)}`);
}
// Convertir en base64
const base64Data = await fileToBase64(file);
const attachedFile: AttachedFile = {
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
name: file.name,
type: file.type,
size: file.size,
base64Data,
uploadedAt: new Date().toISOString()
};
newFiles.push(attachedFile);
}
// Ajouter les nouveaux fichiers
setFolderData(prev => ({
...prev,
attachedFiles: [...(prev.attachedFiles || []), ...newFiles]
}));
} catch (error) {
setUploadError(error instanceof Error ? error.message : 'Erreur lors du téléchargement');
} finally {
setIsUploading(false);
// Réinitialiser l'input
event.target.value = '';
}
};
const removeFile = (fileId: string) => {
setFolderData(prev => ({
...prev,
attachedFiles: (prev.attachedFiles || []).filter(f => f.id !== fileId)
}));
};
const downloadFile = (file: AttachedFile) => {
const link = document.createElement('a');
link.href = `data:${file.type};base64,${file.base64Data}`;
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave?.({ ...folderData, updated_at: new Date().toISOString() }, selectedMembers);
@ -197,6 +309,110 @@ function FolderModal({
)}
</div>
{/* Fichiers attachés */}
<div className="space-y-4">
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Fichiers attachés</h3>
{/* Liste des fichiers */}
<div className="space-y-2">
{(folderData.attachedFiles || []).map((file) => (
<div key={file.id} className="flex items-center justify-between bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 rounded-md">
<div className="flex items-center space-x-3">
<div className="flex-shrink-0">
{file.type.startsWith('image/') ? (
<svg className="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clipRule="evenodd" />
</svg>
) : file.type === 'application/pdf' ? (
<svg className="w-5 h-5 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clipRule="evenodd" />
</svg>
) : (
<svg className="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clipRule="evenodd" />
</svg>
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{file.name}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{formatFileSize(file.size)}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<button
type="button"
onClick={() => downloadFile(file)}
className="text-blue-500 hover:text-blue-700 text-sm"
>
Télécharger
</button>
{!readOnly && (
<button
type="button"
onClick={() => removeFile(file.id)}
className="text-red-500 hover:text-red-700 ml-2"
>
×
</button>
)}
</div>
</div>
))}
</div>
{/* Zone de téléchargement */}
{!readOnly && (
<div className="space-y-2">
<div className="flex items-center space-x-2">
<input
type="file"
id="file-upload"
multiple
accept={Object.values(ALLOWED_FILE_TYPES).join(',')}
onChange={handleFileUpload}
disabled={isUploading}
className="hidden"
/>
<label
htmlFor="file-upload"
className={`cursor-pointer inline-flex items-center px-4 py-2 text-white rounded-md ${colors.button} disabled:opacity-50 ${isUploading ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{isUploading ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Téléchargement...
</>
) : (
<>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8l-8-8-8 8" />
</svg>
Ajouter des fichiers
</>
)}
</label>
</div>
{/* Message d'erreur */}
{uploadError && (
<div className="text-red-500 text-sm bg-red-50 dark:bg-red-900/20 p-2 rounded-md">
{uploadError}
</div>
)}
{/* Informations sur les types de fichiers autorisés */}
<div className="text-xs text-gray-500 dark:text-gray-400">
Types autorisés: PDF, Images (PNG, JPG, GIF, WebP), Documents (DOC, DOCX, XLS, XLSX), TXT
<br />
Taille maximale: {formatFileSize(MAX_FILE_SIZE)}
</div>
</div>
)}
</div>
{/* Informations système */}
<div className="space-y-4">
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Informations système</h3>

View File

@ -22,6 +22,15 @@ export interface FolderChatData {
data?: FolderChatAttachment[];
}
export interface AttachedFile {
id: string;
name: string;
type: string; // MIME type
size: number; // taille en bytes
base64Data: string; // contenu du fichier en base64
uploadedAt: string; // timestamp ISO
}
export interface FolderData {
folderNumber: string;
name: string;
@ -31,6 +40,7 @@ export interface FolderData {
notes: string[];
messages: FolderChatData[];
messages_owner: FolderChatData[];
attachedFiles?: AttachedFile[];
}
export function isFolderData(data: any): data is FolderData {
@ -65,6 +75,7 @@ const emptyFolderData: FolderData = {
notes: [],
messages: [],
messages_owner: [],
attachedFiles: [],
};
const folderDataFields: string[] = Object.keys(emptyFolderData);

View File

@ -126,6 +126,7 @@ export function FourNKProvider({ children }: { children: ReactNode }) {
notes: basePrivateData.notes || [],
messages: mergedMessages || [],
messages_owner: mergedMessagesOwner || [],
attachedFiles: basePrivateData.attachedFiles || [],
});
});