Merge branch 'dev' of https://git.4nkweb.com/4nk/docv into dev
This commit is contained in:
commit
7c09b346ec
@ -309,25 +309,79 @@ export default function DashboardPage() {
|
|||||||
Fichiers
|
Fichiers
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{/* <CardContent>
|
<CardContent>
|
||||||
{selectedFolder.files && selectedFolder.files.length > 0 ? (
|
{(() => {
|
||||||
|
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">
|
<div className="space-y-3">
|
||||||
{selectedFolder.files.map((file, index) => (
|
{Object.entries(selectedFolder.attachedFiles || {}).map(([key, file]: [string, any]) => {
|
||||||
<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">
|
return (
|
||||||
<FileText className="h-5 w-5 text-gray-400 flex-shrink-0" />
|
<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>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-sm font-medium text-gray-100 truncate">{file.name || 'Fichier'}</p>
|
<p className="text-sm font-medium text-gray-100 truncate">
|
||||||
<p className="text-xs text-gray-400">{formatBytes(file.size || 0)}</p>
|
{(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>
|
||||||
</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>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-gray-500">Aucun fichier dans ce dossier.</p>
|
<p className="text-gray-500">Aucun fichier dans ce dossier.</p>
|
||||||
)}
|
)}
|
||||||
</CardContent> */}
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState, memo } from 'react';
|
import React, { useEffect, useState, memo } from 'react';
|
||||||
import Modal from './Modal';
|
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';
|
import { MemberAutocomplete } from '../ui/member-autocomplete';
|
||||||
|
|
||||||
type FolderType = 'contrat' | 'projet' | 'rapport' | 'finance' | 'rh' | 'marketing' | 'autre';
|
type FolderType = 'contrat' | 'projet' | 'rapport' | 'finance' | 'rh' | 'marketing' | 'autre';
|
||||||
@ -30,7 +30,8 @@ const defaultFolder: FolderData = {
|
|||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
notes: [],
|
notes: [],
|
||||||
messages: [],
|
messages: [],
|
||||||
messages_owner: []
|
messages_owner: [],
|
||||||
|
attachedFiles: []
|
||||||
};
|
};
|
||||||
|
|
||||||
function capitalize(s?: string) {
|
function capitalize(s?: string) {
|
||||||
@ -38,6 +39,46 @@ function capitalize(s?: string) {
|
|||||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
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
|
// Mapping des couleurs
|
||||||
const folderColors: Record<FolderType, { bg: string; border: string; focus: string; button: string }> = {
|
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' },
|
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('');
|
const [currentNote, setCurrentNote] = useState('');
|
||||||
// --- NOUVEAU: État pour les membres sélectionnés ---
|
// --- NOUVEAU: État pour les membres sélectionnés ---
|
||||||
const [selectedMembers, setSelectedMembers] = useState<string[]>([]);
|
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(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@ -99,6 +143,74 @@ function FolderModal({
|
|||||||
setFolderData(prev => ({ ...prev, notes: (prev.notes || []).filter(n => n !== note) }));
|
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) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onSave?.({ ...folderData, updated_at: new Date().toISOString() }, selectedMembers);
|
onSave?.({ ...folderData, updated_at: new Date().toISOString() }, selectedMembers);
|
||||||
@ -197,6 +309,110 @@ function FolderModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 */}
|
{/* Informations système */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Informations système</h3>
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Informations système</h3>
|
||||||
|
|||||||
@ -22,6 +22,15 @@ export interface FolderChatData {
|
|||||||
data?: FolderChatAttachment[];
|
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 {
|
export interface FolderData {
|
||||||
folderNumber: string;
|
folderNumber: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -31,6 +40,7 @@ export interface FolderData {
|
|||||||
notes: string[];
|
notes: string[];
|
||||||
messages: FolderChatData[];
|
messages: FolderChatData[];
|
||||||
messages_owner: FolderChatData[];
|
messages_owner: FolderChatData[];
|
||||||
|
attachedFiles?: AttachedFile[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFolderData(data: any): data is FolderData {
|
export function isFolderData(data: any): data is FolderData {
|
||||||
@ -65,6 +75,7 @@ const emptyFolderData: FolderData = {
|
|||||||
notes: [],
|
notes: [],
|
||||||
messages: [],
|
messages: [],
|
||||||
messages_owner: [],
|
messages_owner: [],
|
||||||
|
attachedFiles: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const folderDataFields: string[] = Object.keys(emptyFolderData);
|
const folderDataFields: string[] = Object.keys(emptyFolderData);
|
||||||
|
|||||||
@ -126,6 +126,7 @@ export function FourNKProvider({ children }: { children: ReactNode }) {
|
|||||||
notes: basePrivateData.notes || [],
|
notes: basePrivateData.notes || [],
|
||||||
messages: mergedMessages || [],
|
messages: mergedMessages || [],
|
||||||
messages_owner: mergedMessagesOwner || [],
|
messages_owner: mergedMessagesOwner || [],
|
||||||
|
attachedFiles: basePrivateData.attachedFiles || [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user