docv/components/4nk/FolderModal.tsx

243 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState, memo } from 'react';
import Modal from './Modal';
import type { FolderData } from '@/lib/4nk/models/FolderData';
import { MemberAutocomplete } from '../ui/member-autocomplete';
type FolderType = 'contrat' | 'projet' | 'rapport' | 'finance' | 'rh' | 'marketing' | 'autre';
interface FolderModalProps {
folder?: FolderData;
// --- MODIFIÉ ---
onSave?: (folderData: FolderData, selectedMembers: string[]) => void;
onCancel?: () => void;
readOnly?: boolean;
isOpen: boolean;
onClose: () => void;
folderType?: FolderType;
// --- NOUVEAU ---
members?: string[]; // Liste des membres disponibles
renderExtraFields?: (
folderData: FolderData,
setFolderData: React.Dispatch<React.SetStateAction<FolderData>>
) => React.ReactNode;
}
const defaultFolder: FolderData = {
folderNumber: '',
name: '',
description: '',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
notes: [],
messages: [],
messages_owner: []
};
function capitalize(s?: string) {
if (!s) return '';
return s.charAt(0).toUpperCase() + s.slice(1);
}
// 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' },
projet: { bg: 'bg-green-50 dark:bg-green-900', border: 'border-green-300 dark:border-green-700', focus: 'focus:ring-green-400 dark:focus:ring-green-600', button: 'bg-green-500 hover:bg-green-600' },
rapport: { bg: 'bg-yellow-50 dark:bg-yellow-900', border: 'border-yellow-300 dark:border-yellow-700', focus: 'focus:ring-yellow-400 dark:focus:ring-yellow-600', button: 'bg-yellow-500 hover:bg-yellow-600' },
finance: { bg: 'bg-indigo-50 dark:bg-indigo-900', border: 'border-indigo-300 dark:border-indigo-700', focus: 'focus:ring-indigo-400 dark:focus:ring-indigo-600', button: 'bg-indigo-500 hover:bg-indigo-600' },
rh: { bg: 'bg-pink-50 dark:bg-pink-900', border: 'border-pink-300 dark:border-pink-700', focus: 'focus:ring-pink-400 dark:focus:ring-pink-600', button: 'bg-pink-500 hover:bg-pink-600' },
marketing: { bg: 'bg-purple-50 dark:bg-purple-900', border: 'border-purple-300 dark:border-purple-700', focus: 'focus:ring-purple-400 dark:focus:ring-purple-600', button: 'bg-purple-500 hover:bg-purple-600' },
autre: { bg: 'bg-gray-50 dark:bg-gray-800', border: 'border-gray-300 dark:border-gray-600', focus: 'focus:ring-blue-400 dark:focus:ring-blue-600', button: 'bg-blue-500 hover:bg-blue-600' },
};
function FolderModal({
folder = defaultFolder,
onSave,
onCancel,
readOnly = false,
isOpen,
onClose,
folderType = 'autre',
members = [],
renderExtraFields
}: FolderModalProps) {
const [folderData, setFolderData] = useState<FolderData>({ ...defaultFolder, ...folder });
const [currentNote, setCurrentNote] = useState('');
// --- NOUVEAU: État pour les membres sélectionnés ---
const [selectedMembers, setSelectedMembers] = useState<string[]>([]);
useEffect(() => {
if (isOpen) {
setFolderData({ ...defaultFolder, ...(folder || {}) });
setCurrentNote('');
setSelectedMembers([]); // <-- MODIFIÉ: Réinitialise les membres
}
}, [isOpen, folder]);
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setFolderData(prev => ({ ...(prev as any), [name]: value } as FolderData));
};
const handleMemberToggle = (memberId: string) => {
setSelectedMembers(prev =>
prev.includes(memberId)
? prev.filter(id => id !== memberId)
: [...prev, memberId]
);
};
const addNote = () => {
const v = currentNote.trim();
if (!v) return;
setFolderData(prev => ({ ...prev, notes: [...(prev.notes || []), v] }));
setCurrentNote('');
};
const removeNote = (note: string) => {
setFolderData(prev => ({ ...prev, notes: (prev.notes || []).filter(n => n !== note) }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave?.({ ...folderData, updated_at: new Date().toISOString() }, selectedMembers);
onClose();
};
const handleCancel = () => {
if (onCancel) onCancel();
else onClose();
};
const colors = folderColors[folderType];
const title = `Créer un dossier ${capitalize(folderType)}`;
return (
<Modal isOpen={isOpen} onClose={onClose} title={title} size="lg">
<div className={`p-6 rounded-lg space-y-8 ${colors.bg} text-gray-900 dark:text-gray-100`}>
<form className="space-y-8" onSubmit={handleSubmit}>
{/* Informations principales */}
<div className="space-y-6">
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Informations principales</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{['folderNumber', 'name'].map((field) => (
<div className="relative" key={field}>
<input
type="text"
name={field}
value={folderData[field as keyof FolderData] || ''}
onChange={handleInputChange}
required
disabled={readOnly}
placeholder=" "
className={`peer block w-full px-3 pt-5 pb-2 rounded-md ${colors.bg} text-gray-900 dark:text-gray-100 placeholder-transparent border ${colors.border} focus:outline-none ${colors.focus}`}
/>
<label className="absolute left-3 top-2.5 text-gray-500 dark:text-gray-400 text-sm transition-all peer-placeholder-shown:top-5 peer-placeholder-shown:text-base peer-focus:top-2.5 peer-focus:text-sm">
{field === 'folderNumber' ? 'Numéro de dossier *' : 'Nom *'}
</label>
</div>
))}
</div>
{/* Description */}
<div className="relative">
<textarea
name="description"
value={folderData.description || ''}
onChange={handleInputChange}
disabled={readOnly}
placeholder=" "
rows={3}
className={`peer block w-full px-3 pt-5 pb-2 rounded-md ${colors.bg} text-gray-900 dark:text-gray-100 placeholder-transparent border ${colors.border} focus:outline-none ${colors.focus} resize-none`}
/>
<label className="absolute left-3 top-2.5 text-gray-500 dark:text-gray-400 text-sm transition-all peer-placeholder-shown:top-5 peer-placeholder-shown:text-base peer-focus:top-2.5 peer-focus:text-sm">
Description
</label>
</div>
</div>
{/* Membres */}
{!readOnly && (
<div className="space-y-4">
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Membres</h3>
<MemberAutocomplete
allMembers={members}
selectedMembers={selectedMembers}
onChange={setSelectedMembers} // On passe le setter de l'état
/>
</div>
)}
{/* Notes */}
<div className="space-y-4">
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Notes</h3>
<div className="space-y-2">
{(folderData.notes || []).map((note, index) => (
<div key={index} className="flex items-center justify-between bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-1 rounded-md">
<span>{note}</span>
{!readOnly && (
<button type="button" className="text-red-500 hover:text-red-700 ml-2" onClick={() => removeNote(note)}>×</button>
)}
</div>
))}
</div>
{!readOnly && (
<div className="flex space-x-2">
<input
type="text"
value={currentNote}
onChange={(e) => setCurrentNote(e.target.value)}
placeholder="Ajouter une note"
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addNote(); } }}
className={`flex-1 border rounded-md px-3 py-2 ${colors.bg} text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-400 border ${colors.border} focus:outline-none ${colors.focus}`}
/>
<button type="button" className={`px-4 py-2 text-white rounded-md ${colors.button}`} onClick={addNote}>Ajouter</button>
</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>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{['created_at', 'updated_at'].map((field) => {
const value = new Date(folderData[field as keyof FolderData] as string).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
const label = field === 'created_at' ? 'Créé le' : 'Dernière mise à jour';
return (
<div className="relative" key={field}>
<input
type="text"
value={value}
disabled
readOnly
placeholder=" "
className={`peer block w-full px-3 pt-5 pb-2 rounded-md ${colors.bg} text-gray-900 dark:text-gray-100 placeholder-transparent border ${colors.border} focus:outline-none ${colors.focus}`}
/>
<label className="absolute left-3 top-2.5 text-gray-500 dark:text-gray-400 text-sm transition-all peer-placeholder-shown:top-5 peer-placeholder-shown:text-base peer-focus:top-2.5 peer-focus:text-sm">
{label}
</label>
</div>
);
})}
</div>
</div>
{/* Actions */}
<div className="flex justify-end space-x-3">
<button type="button" className={`px-4 py-2 rounded-md ${colors.bg} text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:hover:bg-gray-700`} onClick={handleCancel}>Annuler</button>
<button type="submit" className={`px-4 py-2 text-white rounded-md ${colors.button} disabled:opacity-50`} disabled={readOnly}>Enregistrer</button>
</div>
{/* Champs spécifiques injectés */}
{renderExtraFields && renderExtraFields(folderData, setFolderData)}
</form>
</div>
</Modal>
);
}
FolderModal.displayName = 'FolderModal';
export default memo(FolderModal);