Added Money System

This commit is contained in:
2026-01-22 19:25:25 +01:00
parent d5d0d5aaf4
commit bb5c3058b1
12 changed files with 703 additions and 266 deletions

View File

@@ -1,13 +1,13 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, memo } from 'react';
import { useRouter } from 'next/navigation';
import { useNotification } from './NotificationProvider';
import { AVAILABLE_PAGES } from '@/lib/pages';
interface AdherentOption {
id: string;
type: 'situation' | 'prescripteur' | 'facturation';
type: 'situation' | 'prescripteur' | 'facturation' | 'forfait';
value: string;
order: number;
}
@@ -16,8 +16,156 @@ interface OptionsByType {
situation: AdherentOption[];
prescripteur: AdherentOption[];
facturation: AdherentOption[];
forfait: AdherentOption[];
}
// Composant OptionCard mémorisé pour éviter les re-renders inutiles
const OptionCard = memo(({
type,
label,
icon,
options,
loading,
editingId,
editingValue,
newValue,
onEdit,
onSaveEdit,
onCancelEdit,
onDelete,
onAdd,
onNewValueChange,
onEditingValueChange,
}: {
type: 'situation' | 'prescripteur' | 'facturation' | 'forfait';
label: string;
icon: React.ReactNode;
options: AdherentOption[];
loading: boolean;
editingId: string | null;
editingValue: string;
newValue: string;
onEdit: (option: AdherentOption) => void;
onSaveEdit: (id: string, type: 'situation' | 'prescripteur' | 'facturation' | 'forfait') => void;
onCancelEdit: () => void;
onDelete: (id: string) => void;
onAdd: (type: 'situation' | 'prescripteur' | 'facturation' | 'forfait') => void;
onNewValueChange: (type: 'situation' | 'prescripteur' | 'facturation' | 'forfait', value: string) => void;
onEditingValueChange: (value: string) => void;
}) => {
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-lg bg-lblue/10 flex items-center justify-center">
{icon}
</div>
<h3 className="text-lg font-bold text-gray-900">{label}</h3>
<span className="ml-auto px-3 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-600">
{options.length} option{options.length > 1 ? 's' : ''}
</span>
</div>
{/* Liste des options */}
<div className="space-y-2 mb-4">
{loading ? (
<div className="text-center py-4 text-gray-500">Chargement...</div>
) : options.length === 0 ? (
<div className="text-center py-4 text-gray-500 text-sm">
Aucune option configurée
</div>
) : (
options.map((option) => (
<div
key={option.id}
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200 hover:border-gray-300 transition-colors"
>
{editingId === option.id ? (
<>
<input
type="text"
value={editingValue}
onChange={(e) => {
onEditingValueChange(e.target.value);
}}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent"
onKeyDown={(e) => {
if (e.key === 'Enter') {
onSaveEdit(option.id, type);
} else if (e.key === 'Escape') {
onCancelEdit();
}
}}
autoFocus
/>
<button
onClick={() => onSaveEdit(option.id, type)}
className="px-3 py-2 text-sm font-medium text-white bg-lgreen rounded-lg hover:bg-dgreen transition-colors"
>
Enregistrer
</button>
<button
onClick={onCancelEdit}
className="px-3 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 transition-colors"
>
Annuler
</button>
</>
) : (
<>
<span className="flex-1 text-sm font-medium text-gray-900">
{option.value}
</span>
<button
onClick={() => onEdit(option)}
className="px-3 py-1.5 text-xs font-medium text-lblue bg-lblue/10 rounded-lg hover:bg-lblue/20 transition-colors"
>
Modifier
</button>
<button
onClick={() => onDelete(option.id)}
className="px-3 py-1.5 text-xs font-medium text-red-600 bg-red-50 rounded-lg hover:bg-red-100 transition-colors"
>
Supprimer
</button>
</>
)}
</div>
))
)}
</div>
{/* Formulaire d'ajout */}
<div className="flex items-center gap-2 pt-4 border-t border-gray-200">
<input
type="text"
value={newValue || ''}
onChange={(e) => {
onNewValueChange(type, e.target.value);
}}
placeholder={`Ajouter une nouvelle ${label.toLowerCase()}`}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent"
onKeyDown={(e) => {
if (e.key === 'Enter') {
onAdd(type);
}
}}
/>
<button
onClick={() => onAdd(type)}
className="px-4 py-2 text-sm font-medium text-white bg-lblue rounded-lg hover:bg-dblue transition-colors flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Ajouter
</button>
</div>
</div>
);
});
OptionCard.displayName = 'OptionCard';
export default function ConfigurationContent() {
const router = useRouter();
const { showNotification } = useNotification();
@@ -26,6 +174,7 @@ export default function ConfigurationContent() {
situation: [],
prescripteur: [],
facturation: [],
forfait: [],
});
const [loading, setLoading] = useState(true);
const [editingId, setEditingId] = useState<string | null>(null);
@@ -34,6 +183,7 @@ export default function ConfigurationContent() {
situation: '',
prescripteur: '',
facturation: '',
forfait: '',
});
const fetchOptions = useCallback(async () => {
@@ -46,6 +196,7 @@ export default function ConfigurationContent() {
situation: data.situation || [],
prescripteur: data.prescripteur || [],
facturation: data.facturation || [],
forfait: data.forfait || [],
});
}
} catch (error) {
@@ -58,54 +209,48 @@ export default function ConfigurationContent() {
useEffect(() => {
fetchOptions();
}, [fetchOptions]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleAdd = useCallback(async (type: 'situation' | 'prescripteur' | 'facturation') => {
setNewValue((current) => {
const value = (current[type] || '').trim();
if (!value) {
showNotification('Veuillez entrer une valeur', 'error');
return current;
const handleAdd = useCallback(async (type: 'situation' | 'prescripteur' | 'facturation' | 'forfait') => {
const currentValue = newValue[type]?.trim() || '';
if (!currentValue) {
showNotification('Veuillez entrer une valeur', 'error');
return;
}
try {
const response = await fetch('/api/settings/adherent-options', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type,
value: currentValue,
}),
});
if (response.ok) {
setNewValue((prev) => ({ ...prev, [type]: '' }));
await fetchOptions();
showNotification('Option ajoutée avec succès', 'success');
} else {
const error = await response.json();
showNotification(error.error || 'Erreur lors de l\'ajout', 'error');
}
} catch (error) {
console.error('Erreur:', error);
showNotification('Erreur lors de l\'ajout', 'error');
}
}, [newValue, showNotification, fetchOptions]);
// Appel API asynchrone
(async () => {
try {
const response = await fetch('/api/settings/adherent-options', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type,
value,
}),
});
if (response.ok) {
setNewValue((prev) => ({ ...prev, [type]: '' }));
await fetchOptions();
showNotification('Option ajoutée avec succès', 'success');
} else {
const error = await response.json();
showNotification(error.error || 'Erreur lors de l\'ajout', 'error');
}
} catch (error) {
console.error('Erreur:', error);
showNotification('Erreur lors de l\'ajout', 'error');
}
})();
return current;
});
}, [showNotification, fetchOptions]);
const handleEdit = (option: AdherentOption) => {
const handleEdit = useCallback((option: AdherentOption) => {
setEditingId(option.id);
setEditingValue(option.value);
};
}, []);
const handleSaveEdit = async (id: string, type: 'situation' | 'prescripteur' | 'facturation') => {
const handleSaveEdit = useCallback(async (id: string, type: 'situation' | 'prescripteur' | 'facturation' | 'forfait') => {
const value = editingValue.trim();
if (!value) {
showNotification('Veuillez entrer une valeur', 'error');
@@ -134,14 +279,14 @@ export default function ConfigurationContent() {
console.error('Erreur:', error);
showNotification('Erreur lors de la modification', 'error');
}
};
}, [editingValue, showNotification, fetchOptions]);
const handleCancelEdit = () => {
const handleCancelEdit = useCallback(() => {
setEditingId(null);
setEditingValue('');
};
}, []);
const handleDelete = async (id: string) => {
const handleDelete = useCallback(async (id: string) => {
if (!confirm('Êtes-vous sûr de vouloir supprimer cette option ?')) {
return;
}
@@ -162,127 +307,15 @@ export default function ConfigurationContent() {
console.error('Erreur:', error);
showNotification('Erreur lors de la suppression', 'error');
}
};
}, [showNotification, fetchOptions]);
const OptionCard = ({
type,
label,
icon,
}: {
type: 'situation' | 'prescripteur' | 'facturation';
label: string;
icon: React.ReactNode;
}) => {
const typeOptions = options[type] || [];
const handleNewValueChange = useCallback((type: 'situation' | 'prescripteur' | 'facturation' | 'forfait', value: string) => {
setNewValue((prev) => ({ ...prev, [type]: value }));
}, []);
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-lg bg-lblue/10 flex items-center justify-center">
{icon}
</div>
<h3 className="text-lg font-bold text-gray-900">{label}</h3>
<span className="ml-auto px-3 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-600">
{typeOptions.length} option{typeOptions.length > 1 ? 's' : ''}
</span>
</div>
{/* Liste des options */}
<div className="space-y-2 mb-4">
{loading ? (
<div className="text-center py-4 text-gray-500">Chargement...</div>
) : typeOptions.length === 0 ? (
<div className="text-center py-4 text-gray-500 text-sm">
Aucune option configurée
</div>
) : (
typeOptions.map((option) => (
<div
key={option.id}
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200 hover:border-gray-300 transition-colors"
>
{editingId === option.id ? (
<>
<input
type="text"
value={editingValue}
onChange={(e) => setEditingValue(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent"
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSaveEdit(option.id, type);
} else if (e.key === 'Escape') {
handleCancelEdit();
}
}}
autoFocus
/>
<button
onClick={() => handleSaveEdit(option.id, type)}
className="px-3 py-2 text-sm font-medium text-white bg-lgreen rounded-lg hover:bg-dgreen transition-colors"
>
Enregistrer
</button>
<button
onClick={handleCancelEdit}
className="px-3 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 transition-colors"
>
Annuler
</button>
</>
) : (
<>
<span className="flex-1 text-sm font-medium text-gray-900">
{option.value}
</span>
<button
onClick={() => handleEdit(option)}
className="px-3 py-1.5 text-xs font-medium text-lblue bg-lblue/10 rounded-lg hover:bg-lblue/20 transition-colors"
>
Modifier
</button>
<button
onClick={() => handleDelete(option.id)}
className="px-3 py-1.5 text-xs font-medium text-red-600 bg-red-50 rounded-lg hover:bg-red-100 transition-colors"
>
Supprimer
</button>
</>
)}
</div>
))
)}
</div>
{/* Formulaire d'ajout */}
<div className="flex items-center gap-2 pt-4 border-t border-gray-200">
<input
type="text"
value={newValue[type] || ''}
onChange={(e) => {
setNewValue((prev) => ({ ...prev, [type]: e.target.value }));
}}
placeholder={`Ajouter une nouvelle ${label.toLowerCase()}`}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent"
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleAdd(type);
}
}}
/>
<button
onClick={() => handleAdd(type)}
className="px-4 py-2 text-sm font-medium text-white bg-lblue rounded-lg hover:bg-dblue transition-colors flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Ajouter
</button>
</div>
</div>
);
};
const handleEditingValueChange = useCallback((value: string) => {
setEditingValue(value);
}, []);
// Composant pour la gestion des comptes
const GestionComptesContent = () => {
@@ -1095,6 +1128,18 @@ export default function ConfigurationContent() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
}
options={options.situation}
loading={loading}
editingId={editingId}
editingValue={editingValue}
newValue={newValue.situation}
onEdit={handleEdit}
onSaveEdit={handleSaveEdit}
onCancelEdit={handleCancelEdit}
onDelete={handleDelete}
onAdd={handleAdd}
onNewValueChange={handleNewValueChange}
onEditingValueChange={handleEditingValueChange}
/>
<OptionCard
@@ -1105,6 +1150,18 @@ export default function ConfigurationContent() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
}
options={options.prescripteur}
loading={loading}
editingId={editingId}
editingValue={editingValue}
newValue={newValue.prescripteur}
onEdit={handleEdit}
onSaveEdit={handleSaveEdit}
onCancelEdit={handleCancelEdit}
onDelete={handleDelete}
onAdd={handleAdd}
onNewValueChange={handleNewValueChange}
onEditingValueChange={handleEditingValueChange}
/>
<OptionCard
@@ -1115,6 +1172,40 @@ export default function ConfigurationContent() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
}
options={options.facturation}
loading={loading}
editingId={editingId}
editingValue={editingValue}
newValue={newValue.facturation}
onEdit={handleEdit}
onSaveEdit={handleSaveEdit}
onCancelEdit={handleCancelEdit}
onDelete={handleDelete}
onAdd={handleAdd}
onNewValueChange={handleNewValueChange}
onEditingValueChange={handleEditingValueChange}
/>
<OptionCard
type="forfait"
label="Forfaits"
icon={
<svg className="w-5 h-5 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
}
options={options.forfait}
loading={loading}
editingId={editingId}
editingValue={editingValue}
newValue={newValue.forfait}
onEdit={handleEdit}
onSaveEdit={handleSaveEdit}
onCancelEdit={handleCancelEdit}
onDelete={handleDelete}
onAdd={handleAdd}
onNewValueChange={handleNewValueChange}
onEditingValueChange={handleEditingValueChange}
/>
</div>
) : activeConfigSection === 'comptes' ? (