Compare commits
No commits in common. "93c927ba71ae818dd2cc9c030923069d5ea3454f" and "15d9d80b527a3a55dd7be278ce64aed8beb8be14" have entirely different histories.
93c927ba71
...
15d9d80b52
@ -309,79 +309,25 @@ export default function DashboardPage() {
|
||||
Fichiers
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{(() => {
|
||||
const files = selectedFolder?.attachedFiles;
|
||||
if (!files) return false;
|
||||
|
||||
if (typeof files === 'object') {
|
||||
return Object.keys(files).length > 0;
|
||||
}
|
||||
return false;
|
||||
})() ? (
|
||||
{/* <CardContent>
|
||||
{selectedFolder.files && selectedFolder.files.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{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">
|
||||
<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>
|
||||
{selectedFolder.files.map((file, index) => (
|
||||
<div key={index} 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="min-w-0">
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState, memo } from 'react';
|
||||
import Modal from './Modal';
|
||||
import type { FolderData, AttachedFile } from '@/lib/4nk/models/FolderData';
|
||||
import type { FolderData } from '@/lib/4nk/models/FolderData';
|
||||
import { MemberAutocomplete } from '../ui/member-autocomplete';
|
||||
|
||||
type FolderType = 'contrat' | 'projet' | 'rapport' | 'finance' | 'rh' | 'marketing' | 'autre';
|
||||
@ -30,8 +30,7 @@ const defaultFolder: FolderData = {
|
||||
updated_at: new Date().toISOString(),
|
||||
notes: [],
|
||||
messages: [],
|
||||
messages_owner: [],
|
||||
attachedFiles: []
|
||||
messages_owner: []
|
||||
};
|
||||
|
||||
function capitalize(s?: string) {
|
||||
@ -39,46 +38,6 @@ 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' },
|
||||
@ -105,9 +64,6 @@ 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) {
|
||||
@ -143,74 +99,6 @@ 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);
|
||||
@ -309,110 +197,6 @@ 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>
|
||||
|
||||
@ -1,14 +1,5 @@
|
||||
import type { RoleDefinition } from "./Roles";
|
||||
|
||||
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;
|
||||
@ -18,7 +9,6 @@ export interface FolderData {
|
||||
notes: string[];
|
||||
messages: string[];
|
||||
messages_owner: string[];
|
||||
attachedFiles?: AttachedFile[];
|
||||
}
|
||||
|
||||
export function isFolderData(data: any): data is FolderData {
|
||||
@ -53,7 +43,6 @@ const emptyFolderData: FolderData = {
|
||||
notes: [],
|
||||
messages: [],
|
||||
messages_owner: [],
|
||||
attachedFiles: [],
|
||||
};
|
||||
|
||||
const folderDataFields: string[] = Object.keys(emptyFolderData);
|
||||
|
||||
@ -119,7 +119,6 @@ export function FourNKProvider({ children }: { children: ReactNode }) {
|
||||
notes: privateData.notes || [],
|
||||
messages: privateData.messages || [],
|
||||
messages_owner: privateData.messages_owner || [],
|
||||
attachedFiles: privateData.attachedFiles || [],
|
||||
});
|
||||
// });
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user