- améliore l'export/import (format de fichiers en paramètres, amélioration de la robustesse, )

- ajout bouton tout effacer des propositions et participants
This commit is contained in:
Yannick Le Duc
2025-09-16 15:45:28 +02:00
parent 6aead108d7
commit 2a2738f5c0
16 changed files with 1455 additions and 81 deletions

View File

@@ -12,6 +12,7 @@ import Footer from '@/components/Footer';
import SmtpSettingsForm from '@/components/SmtpSettingsForm';
import { Settings, Monitor, Save, CheckCircle, Mail, FileText, Download } from 'lucide-react';
import { ExportAnonymizationSelect, AnonymizationLevel } from '@/components/ExportAnonymizationSelect';
import { ExportFileFormatSelect, ExportFileFormat } from '@/components/ExportFileFormatSelect';
export const dynamic = 'force-dynamic';
@@ -24,6 +25,18 @@ function SettingsPageContent() {
const [proposePageMessage, setProposePageMessage] = useState('');
const [footerMessage, setFooterMessage] = useState('');
const [exportAnonymization, setExportAnonymization] = useState<AnonymizationLevel>('full');
const [exportFileFormat, setExportFileFormat] = useState<ExportFileFormat>('ods');
// États pour la détection des modifications
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [originalValues, setOriginalValues] = useState<{
randomizePropositions: boolean;
proposePageMessage: string;
footerMessage: string;
exportAnonymization: AnonymizationLevel;
exportFileFormat: ExportFileFormat;
} | null>(null);
const [autoSaved, setAutoSaved] = useState(false);
useEffect(() => {
// Vérifier la configuration Supabase
@@ -42,6 +55,34 @@ function SettingsPageContent() {
loadSettings();
}, []);
// Détecter les modifications
useEffect(() => {
if (!originalValues) return;
const hasChanges =
randomizePropositions !== originalValues.randomizePropositions ||
proposePageMessage !== originalValues.proposePageMessage ||
footerMessage !== originalValues.footerMessage ||
exportAnonymization !== originalValues.exportAnonymization ||
exportFileFormat !== originalValues.exportFileFormat;
setHasUnsavedChanges(hasChanges);
}, [randomizePropositions, proposePageMessage, footerMessage, exportAnonymization, exportFileFormat, originalValues]);
// Avertissement avant de quitter la page
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (hasUnsavedChanges) {
e.preventDefault();
e.returnValue = 'Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir quitter ?';
return e.returnValue;
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [hasUnsavedChanges]);
const loadSettings = async () => {
try {
setLoading(true);
@@ -63,6 +104,19 @@ function SettingsPageContent() {
// Charger le niveau d'anonymisation des exports
const anonymizationValue = await settingsService.getStringValue('export_anonymization', 'full') as AnonymizationLevel;
setExportAnonymization(anonymizationValue);
// Charger le format de fichier d'export
const fileFormatValue = await settingsService.getStringValue('export_file_format', 'ods') as ExportFileFormat;
setExportFileFormat(fileFormatValue);
// Stocker les valeurs originales pour la détection des modifications
setOriginalValues({
randomizePropositions: randomizeValue,
proposePageMessage: messageValue,
footerMessage: footerValue,
exportAnonymization: anonymizationValue,
exportFileFormat: fileFormatValue
});
} catch (error) {
console.error('Erreur lors du chargement des paramètres:', error);
} finally {
@@ -72,17 +126,82 @@ function SettingsPageContent() {
const handleRandomizeChange = async (checked: boolean) => {
setRandomizePropositions(checked);
// Sauvegarde automatique pour ce paramètre
try {
await settingsService.setBooleanValue('randomize_propositions', checked);
// Mettre à jour les valeurs originales
if (originalValues) {
setOriginalValues({
...originalValues,
randomizePropositions: checked
});
}
// Afficher la confirmation de sauvegarde automatique
setAutoSaved(true);
setTimeout(() => setAutoSaved(false), 2000);
} catch (error) {
console.error('Erreur lors de la sauvegarde automatique:', error);
}
};
const handleExportAnonymizationChange = async (value: AnonymizationLevel) => {
setExportAnonymization(value);
// Sauvegarde automatique pour ce paramètre
try {
await settingsService.setStringValue('export_anonymization', value);
// Mettre à jour les valeurs originales
if (originalValues) {
setOriginalValues({
...originalValues,
exportAnonymization: value
});
}
// Afficher la confirmation de sauvegarde automatique
setAutoSaved(true);
setTimeout(() => setAutoSaved(false), 2000);
} catch (error) {
console.error('Erreur lors de la sauvegarde automatique:', error);
}
};
const handleExportFileFormatChange = async (value: ExportFileFormat) => {
setExportFileFormat(value);
// Sauvegarde automatique pour ce paramètre
try {
await settingsService.setStringValue('export_file_format', value);
// Mettre à jour les valeurs originales
if (originalValues) {
setOriginalValues({
...originalValues,
exportFileFormat: value
});
}
// Afficher la confirmation de sauvegarde automatique
setAutoSaved(true);
setTimeout(() => setAutoSaved(false), 2000);
} catch (error) {
console.error('Erreur lors de la sauvegarde automatique:', error);
}
};
const handleSave = async () => {
try {
setSaving(true);
await settingsService.setBooleanValue('randomize_propositions', randomizePropositions);
// Sauvegarder seulement les paramètres qui ne sont pas sauvegardés automatiquement
await settingsService.setStringValue('propose_page_message', proposePageMessage);
await settingsService.setStringValue('footer_message', footerMessage);
await settingsService.setStringValue('export_anonymization', exportAnonymization);
// Mettre à jour les valeurs originales
setOriginalValues({
randomizePropositions,
proposePageMessage,
footerMessage,
exportAnonymization,
exportFileFormat
});
setSaved(true);
setTimeout(() => setSaved(false), 2000);
setTimeout(() => setSaved(false), 3000); // Message plus long pour les textes
} catch (error) {
console.error('Erreur lors de la sauvegarde des paramètres:', error);
} finally {
@@ -115,13 +234,36 @@ function SettingsPageContent() {
<div className="mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-slate-100">Paramètres</h1>
<p className="text-slate-600 dark:text-slate-300 mt-2">Configurez les paramètres de l'application</p>
<div className="flex items-center gap-3">
<h1 className="text-3xl font-bold text-slate-900 dark:text-slate-100">Paramètres</h1>
{hasUnsavedChanges && (
<div className="flex items-center gap-2 px-3 py-1 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded-full text-sm font-medium">
<div className="w-2 h-2 bg-orange-500 rounded-full animate-pulse"></div>
Modifications non sauvegardées
</div>
)}
{autoSaved && (
<div className="flex items-center gap-2 px-3 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-full text-sm font-medium">
<CheckCircle className="w-4 h-4" />
Sauvegardé automatiquement
</div>
)}
</div>
<p className="text-slate-600 dark:text-slate-300 mt-2">
{hasUnsavedChanges
? 'Vous avez des modifications non sauvegardées. N\'oubliez pas de cliquer sur "Sauvegarder".'
: 'Configurez les paramètres de l\'application'
}
</p>
</div>
<Button
onClick={handleSave}
disabled={saving}
className="flex items-center gap-2"
disabled={saving || !hasUnsavedChanges}
className={`flex items-center gap-2 ${
hasUnsavedChanges
? 'bg-orange-600 hover:bg-orange-700 text-white'
: ''
}`}
>
{saving ? (
<>
@@ -207,13 +349,25 @@ function SettingsPageContent() {
Ce texte apparaît sous le titre de la campagne pour inviter les utilisateurs à déposer des propositions.
</p>
</div>
<textarea
id="propose-page-message"
value={proposePageMessage}
onChange={(e) => setProposePageMessage(e.target.value)}
className="w-full min-h-[100px] p-3 border border-slate-200 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 resize-y"
placeholder="Entrez votre message d'invitation..."
/>
<div className="relative">
<textarea
id="propose-page-message"
value={proposePageMessage}
onChange={(e) => setProposePageMessage(e.target.value)}
className={`w-full min-h-[100px] p-3 border rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 resize-y ${
originalValues && proposePageMessage !== originalValues.proposePageMessage
? 'border-orange-300 dark:border-orange-600 bg-orange-50 dark:bg-orange-900/20'
: 'border-slate-200 dark:border-slate-700'
}`}
placeholder="Entrez votre message d'invitation..."
/>
{originalValues && proposePageMessage !== originalValues.proposePageMessage && (
<div className="absolute top-2 right-2 flex items-center gap-1 px-2 py-1 bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300 rounded text-xs font-medium">
<div className="w-1.5 h-1.5 bg-orange-500 rounded-full animate-pulse"></div>
Modifié
</div>
)}
</div>
</div>
{/* Footer Message Setting */}
@@ -226,13 +380,25 @@ function SettingsPageContent() {
Ce texte apparaît en bas des pages publiques. Vous pouvez utiliser <code className="bg-slate-100 dark:bg-slate-700 px-1 rounded text-xs">[texte du lien](GITURL)</code> pour insérer un lien vers le repository Git.
</p>
</div>
<textarea
<div className="relative">
<textarea
id="footer-message"
value={footerMessage}
onChange={(e) => setFooterMessage(e.target.value)}
className="w-full min-h-[80px] p-3 border border-slate-200 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 resize-y"
className={`w-full min-h-[80px] p-3 border rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 resize-y ${
originalValues && footerMessage !== originalValues.footerMessage
? 'border-orange-300 dark:border-orange-600 bg-orange-50 dark:bg-orange-900/20'
: 'border-slate-200 dark:border-slate-700'
}`}
placeholder="Entrez votre message de bas de page..."
/>
{originalValues && footerMessage !== originalValues.footerMessage && (
<div className="absolute top-2 right-2 flex items-center gap-1 px-2 py-1 bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300 rounded text-xs font-medium">
<div className="w-1.5 h-1.5 bg-orange-500 rounded-full animate-pulse"></div>
Modifié
</div>
)}
</div>
</div>
</CardContent>
</Card>
@@ -253,10 +419,14 @@ function SettingsPageContent() {
</div>
</CardHeader>
<CardContent className="space-y-6">
<div className="p-4 bg-slate-50 dark:bg-slate-800 rounded-lg">
<div className="p-4 bg-slate-50 dark:bg-slate-800 rounded-lg space-y-6">
<ExportAnonymizationSelect
value={exportAnonymization}
onValueChange={setExportAnonymization}
onValueChange={handleExportAnonymizationChange}
/>
<ExportFileFormatSelect
value={exportFileFormat}
onValueChange={handleExportFileFormatChange}
/>
</div>
</CardContent>