docv/components/FolderModal.tsx

326 lines
10 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 './ui/modal/Modal';
import './FolderModal.css';
import type { FolderData } from '../lib/4nk/models/FolderData';
type FolderType = 'contrat' | 'projet' | 'rapport' | 'finance' | 'rh' | 'marketing' | 'autre';
interface FolderModalProps {
folder?: FolderData;
onSave?: (folderData: FolderData) => void;
onCancel?: () => void;
readOnly?: boolean;
isOpen: boolean;
onClose: () => void;
folderType?: FolderType;
renderExtraFields?: (
folderData: FolderData,
setFolderData: React.Dispatch<React.SetStateAction<FolderData>>
) => React.ReactNode;
}
const defaultFolder: FolderData = {
folderNumber: '',
name: '',
deedType: '',
description: '',
archived_description: '',
status: 'active',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
customers: [],
documents: [],
notes: [],
stakeholders: []
};
function capitalize(s?: string) {
if (!s) return '';
return s.charAt(0).toUpperCase() + s.slice(1);
}
function FolderModal({
folder = defaultFolder,
onSave,
onCancel,
readOnly = false,
isOpen,
onClose,
folderType = 'autre',
renderExtraFields
}: FolderModalProps) {
const [folderData, setFolderData] = useState<FolderData>({ ...defaultFolder, ...folder });
const [currentCustomer, setCurrentCustomer] = useState<string>('');
const [currentStakeholder, setCurrentStakeholder] = useState<string>('');
const [currentNote, setCurrentNote] = useState<string>('');
// Sync when modal opens or when folder prop changes (useful pour Edit)
useEffect(() => {
if (isOpen) {
// Merge with defaultFolder to ensure arrays exist
setFolderData({ ...defaultFolder, ...(folder || {}) });
setCurrentCustomer('');
setCurrentStakeholder('');
setCurrentNote('');
}
}, [isOpen, folder]);
// Generic input change handler
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
// cast to avoid TS complaints when updating dynamic fields
setFolderData(prev => ({ ...(prev as any), [name]: value } as FolderData));
};
/* ---------- Customers ---------- */
const addCustomer = () => {
const v = currentCustomer.trim();
if (!v) return;
if (!Array.isArray(folderData.customers)) folderData.customers = [];
if (!folderData.customers.includes(v)) {
setFolderData(prev => ({ ...prev, customers: [...(prev.customers || []), v] }));
}
setCurrentCustomer('');
};
const removeCustomer = (customer: string) => {
setFolderData(prev => ({ ...prev, customers: (prev.customers || []).filter(c => c !== customer) }));
};
/* ---------- Stakeholders ---------- */
const addStakeholder = () => {
const v = currentStakeholder.trim();
if (!v) return;
if (!Array.isArray(folderData.stakeholders)) folderData.stakeholders = [];
if (!folderData.stakeholders.includes(v)) {
setFolderData(prev => ({ ...prev, stakeholders: [...(prev.stakeholders || []), v] }));
}
setCurrentStakeholder('');
};
const removeStakeholder = (stakeholder: string) => {
setFolderData(prev => ({ ...prev, stakeholders: (prev.stakeholders || []).filter(s => s !== stakeholder) }));
};
/* ---------- Notes ---------- */
const addNote = () => {
const v = currentNote.trim();
if (!v) return;
if (!Array.isArray(folderData.notes)) folderData.notes = [];
if (!folderData.notes.includes(v)) {
setFolderData(prev => ({ ...prev, notes: [...(prev.notes || []), v] }));
}
setCurrentNote('');
};
const removeNote = (note: string) => {
setFolderData(prev => ({ ...prev, notes: (prev.notes || []).filter(n => n !== note) }));
};
/* ---------- Submit / Cancel ---------- */
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (onSave) {
onSave({
...folderData,
updated_at: new Date().toISOString()
});
}
if (onClose) {
onClose(); // ← ça ferme le modal après sauvegarde
}
};
const handleCancel = () => {
if (onCancel) {
onCancel(); // ton callback spécifique
} else if (onClose) {
onClose(); // fallback si pas de onCancel
}
};
// Title text
const title = `Créer un dossier ${capitalize(folderType)}`;
return (
// On ne fait PAS "if (!isOpen) return null" : Modal gère l'animation/visibilité
<Modal isOpen={isOpen} onClose={onClose} title={title} size="lg">
<div className={`folder-container folder-${folderType}`}>
<form className="folder-form" onSubmit={handleSubmit}>
{/* Informations principales */}
<div className="form-section">
<h3 className="section-title">Informations principales</h3>
<div className="form-row">
<div className="form-field">
<label>
Numéro de dossier <span className="required">*</span>
</label>
<input
type="text"
name="folderNumber"
value={folderData.folderNumber || ''}
onChange={handleInputChange}
required
disabled={readOnly}
placeholder="ex: DOC-2025-001"
/>
</div>
<div className="form-field">
<label>
Nom <span className="required">*</span>
</label>
<input
type="text"
name="name"
value={folderData.name || ''}
onChange={handleInputChange}
required
disabled={readOnly}
placeholder="Nom du dossier"
/>
</div>
</div>
<div className="form-field">
<label>Description</label>
<textarea
name="description"
value={folderData.description || ''}
onChange={handleInputChange}
disabled={readOnly}
placeholder="Description du dossier"
rows={3}
/>
</div>
{folderData.status === 'archived' && (
<div className="form-field">
<label>Description d'archivage</label>
<textarea
name="archived_description"
value={folderData.archived_description || ''}
onChange={handleInputChange}
disabled={readOnly}
placeholder="Raison d'archivage"
rows={2}
/>
</div>
)}
</div>
{/* Champs spécifiques injectés */}
{renderExtraFields && (
<div className="form-section">
{renderExtraFields(folderData, setFolderData)}
</div>
)}
{/* Notes */}
<div className="form-section">
<h3 className="section-title">Notes</h3>
<div className="tag-list">
{(folderData.notes || []).map((note, index) => (
<div key={index} className="tag-item">
<span>{note}</span>
{!readOnly && (
<button
type="button"
className="tag-remove"
onClick={() => removeNote(note)}
aria-label="Supprimer cette note"
>
×
</button>
)}
</div>
))}
</div>
{!readOnly && (
<div className="form-field tag-input-container">
<input
type="text"
value={currentNote}
onChange={(e) => setCurrentNote(e.target.value)}
placeholder="Ajouter une note"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addNote();
}
}}
/>
<button
type="button"
className="btn-add-tag"
onClick={addNote}
>
Ajouter
</button>
</div>
)}
</div>
{/* Informations système */}
<div className="form-section">
<h3 className="section-title">Informations système</h3>
<div className="form-row">
<div className="form-field">
<label>Créé le</label>
<input
type="text"
value={new Date(folderData.created_at).toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
disabled
readOnly
/>
</div>
<div className="form-field">
<label>Dernière mise à jour</label>
<input
type="text"
value={new Date(folderData.updated_at).toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
disabled
readOnly
/>
</div>
</div>
</div>
{/* Actions */}
<div className="form-actions">
<button type="button" className="btn-cancel" onClick={handleCancel}>
Annuler
</button>
<button type="submit" className="btn-submit" disabled={readOnly}>
Enregistrer
</button>
</div>
</form>
</div>
</Modal>
);
}
FolderModal.displayName = 'FolderModal';
export default memo(FolderModal);