Added Money System
This commit is contained in:
@@ -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' ? (
|
||||
|
||||
Reference in New Issue
Block a user