feat: implement auto-save for apartment feedback with debounced updates and save state indicators

This commit is contained in:
2026-02-13 19:16:19 +01:00
parent 4853d5c51d
commit 8f5c0cce5e

View File

@@ -1,4 +1,4 @@
import { useState } from "react"; import { useEffect, useRef, useState } from "react";
import { import {
MapPin, MapPin,
Euro, Euro,
@@ -84,24 +84,63 @@ const formatFrDate = (value) => {
}); });
}; };
const normalizeFeedback = (noteP, noteA, commentP, commentA) => ({
NOTE_P: typeof noteP === "number" ? noteP : parseFloat(noteP) || 0,
NOTE_A: typeof noteA === "number" ? noteA : parseFloat(noteA) || 0,
COMMENTAIRE_P: commentP?.trim() ? commentP.trim() : null,
COMMENTAIRE_A: commentA?.trim() ? commentA.trim() : null,
});
export default function AppartCard({ annonce, onDelete, onUpdated }) { export default function AppartCard({ annonce, onDelete, onUpdated }) {
const [noteP, setNoteP] = useState(annonce.NOTE_P || 0); const [noteP, setNoteP] = useState(annonce.NOTE_P || 0);
const [noteA, setNoteA] = useState(annonce.NOTE_A || 0); const [noteA, setNoteA] = useState(annonce.NOTE_A || 0);
const [commentP, setCommentP] = useState(annonce.COMMENTAIRE_P || ""); const [commentP, setCommentP] = useState(annonce.COMMENTAIRE_P || "");
const [commentA, setCommentA] = useState(annonce.COMMENTAIRE_A || ""); const [commentA, setCommentA] = useState(annonce.COMMENTAIRE_A || "");
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [pendingSave, setPendingSave] = useState(false);
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
const hasChanges = const lastSavedRef = useRef(
noteP !== (annonce.NOTE_P || 0) || normalizeFeedback(noteP, noteA, commentP, commentA)
noteA !== (annonce.NOTE_A || 0) || );
commentP !== (annonce.COMMENTAIRE_P || "") ||
commentA !== (annonce.COMMENTAIRE_A || "");
const hasPaulinFeedback = (noteP ?? 0) > 0 || !!commentP?.trim(); const hasPaulinFeedback = (noteP ?? 0) > 0 || !!commentP?.trim();
const hasAgatheFeedback = (noteA ?? 0) > 0 || !!commentA?.trim(); const hasAgatheFeedback = (noteA ?? 0) > 0 || !!commentA?.trim();
useEffect(() => {
const normalized = normalizeFeedback(noteP, noteA, commentP, commentA);
const lastSaved = lastSavedRef.current;
if (
normalized.NOTE_P === lastSaved.NOTE_P &&
normalized.NOTE_A === lastSaved.NOTE_A &&
normalized.COMMENTAIRE_P === lastSaved.COMMENTAIRE_P &&
normalized.COMMENTAIRE_A === lastSaved.COMMENTAIRE_A
) {
return;
}
setPendingSave(true);
const timeout = setTimeout(async () => {
setPendingSave(false);
setSaving(true);
try {
await updateAnnonce(annonce.id, normalized);
lastSavedRef.current = normalized;
onUpdated?.();
} catch (err) {
console.error("Erreur sauvegarde annonce", err);
setPendingSave(true);
} finally {
setSaving(false);
}
}, 700);
return () => clearTimeout(timeout);
}, [annonce.id, commentA, commentP, noteA, noteP, onUpdated]);
const handleSave = async () => { const handleSave = async () => {
setSaving(true); setSaving(true);
await updateAnnonce(annonce.id, { await updateAnnonce(annonce.id, {
@@ -225,6 +264,12 @@ export default function AppartCard({ annonce, onDelete, onUpdated }) {
{expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />} {expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button> </button>
{(pendingSave || saving) && (
<div className="px-5 pb-2 text-xs text-sand-400">
{saving ? "Sauvegarde..." : "Sauvegarde automatique imminente..."}
</div>
)}
{expanded && ( {expanded && (
<div className="px-5 pb-4 space-y-3"> <div className="px-5 pb-4 space-y-3">
{/* Paulin */} {/* Paulin */}
@@ -261,21 +306,6 @@ export default function AppartCard({ annonce, onDelete, onUpdated }) {
/> />
</div> </div>
{/* Save */}
{hasChanges && (
<button
onClick={handleSave}
disabled={saving}
className="flex items-center gap-1.5 px-4 py-1.5 bg-warm-500 text-white text-sm rounded-xl hover:bg-warm-600 transition-colors disabled:opacity-50 font-medium"
>
{saving ? (
<Loader2 size={14} className="animate-spin" />
) : (
<Save size={14} />
)}
{saving ? "Sauvegarde..." : "Sauvegarder"}
</button>
)}
</div> </div>
)} )}
</div> </div>