/* MENOUNO — Gestione Recensioni Fetches from FastAPI backend at /api/ */ const API = ""; // same origin const RISULTATO_OPTIONS = [ { value: "", label: "— nessuno" }, { value: "in-attesa", label: "In attesa" }, { value: "lasciata", label: "Recensione lasciata" }, { value: "nessuna", label: "Nessuna risposta" }, { value: "rifiutata", label: "Rifiutata" }, ]; // ── Status computation (mirrors backend logic) ──────────────────────────────── function computeStatus(richiesta, risultato) { const r = (risultato || "").toLowerCase(); const req = (richiesta || "").toLowerCase(); if (r.includes("lasciata")) return "lasciata"; if (r.includes("nessuna")) return "nessuna"; if (r.includes("rifiutata")) return "rifiutata"; if (req === "richiesta inviata") return "in-attesa"; if (req.startsWith("non richiesta")) return "non-richiesta"; return "da-inviare"; } // ── Single table row ────────────────────────────────────────────────────────── function ClientRow({ client: c, onPatch, onSend }) { return (
{/* Cliente */}
{fullName(c)} {c.method === "mail" ? c.email : c.phone}
{/* Contatto */}
{methodLabel(c.method)}
{/* Tatuatore */}
{c.artist || "—"}
{/* Soddisfazione */}
onPatch({ satisfaction: v })} compact />
{/* Richiesta */}
{c.status === "non-richiesta" && c.richiesta_recensione && ( {c.richiesta_recensione} )}
{/* Modalità */}
{/* Risultato */}
{/* Stelle */}
onPatch({ stars: v })}/>
{/* Foto */}
{/* Azione */}
); } // ── Main App ────────────────────────────────────────────────────────────────── function App() { const [clients, setClients] = React.useState([]); const [messages, setMessages] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [selectedMsg, setSelectedMsg] = React.useState(null); const [filter, setFilter] = React.useState("tutti"); const [query, setQuery] = React.useState(""); const [sideOpen, setSideOpen] = React.useState(true); const [editing, setEditing] = React.useState(null); const [refreshing, setRefreshing] = React.useState(false); const [toast, showToast] = useToast(); // ── Fetch ───────────────────────────────────────────────────────────────── // Messages: local JSON, fast — runs independently, never blocks the table skeleton const fetchMessages = React.useCallback(async () => { try { const msgs = await fetch(API + "/api/messages").then(r => r.json()); setMessages(msgs); setSelectedMsg(sel => { const ids = msgs.map(m => m.id); return ids.includes(sel) ? sel : (msgs[0]?.id || null); }); } catch {} }, []); // Clients: Google Sheets — controls table loading/error state const fetchClients = React.useCallback(async (showSpinner = true) => { if (showSpinner) setRefreshing(true); try { const cls = await fetch(API + "/api/clients").then(r => { if (!r.ok) throw new Error("Errore caricamento clienti"); return r.json(); }); setClients(cls); setError(null); } catch (e) { setError(e.message); } finally { setLoading(false); setRefreshing(false); } }, []); React.useEffect(() => { fetchMessages(); // fast, non-blocking fetchClients(false); // controls table skeleton }, []); // ── Client mutations ─────────────────────────────────────────────────────── const patchClient = React.useCallback(async (id, updates) => { // Optimistic UI update setClients(cs => cs.map(c => { if (c.id !== id) return c; const next = { ...c, ...updates }; next.status = computeStatus( updates.richiesta_recensione ?? c.richiesta_recensione, updates.risultato ?? c.risultato ); return next; })); try { const res = await fetch(API + "/api/clients/" + id, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updates), }); if (!res.ok) throw new Error(await res.text()); } catch (e) { showToast("⚠️ Errore salvataggio: " + e.message); fetchClients(false); // roll back optimistic update } }, [fetchClients, showToast]); const handleSend = React.useCallback((client) => { const msg = messages.find(m => m.id === selectedMsg); const url = buildContactURL(client, msg); try { if (client.method === "call") { window.location.href = url; } else { window.open(url, "_blank", "noopener"); } } catch {} showToast("Aperto contatto con " + client.first + " via " + methodLabel(client.method)); }, [messages, selectedMsg, showToast]); // ── Counts & filter ──────────────────────────────────────────────────────── const counts = React.useMemo(() => { const c = { tutti: clients.length, "da-inviare": 0, "in-attesa": 0, "lasciata": 0, "altro": 0, "non-richiesta": 0 }; clients.forEach(cl => { if (cl.status === "da-inviare") c["da-inviare"]++; else if (cl.status === "in-attesa") c["in-attesa"]++; else if (cl.status === "lasciata") c["lasciata"]++; else if (cl.status === "non-richiesta") c["non-richiesta"]++; else c["altro"]++; }); return c; }, [clients]); const filtered = React.useMemo(() => { return clients.filter(c => { if (filter === "da-inviare" && c.status !== "da-inviare") return false; if (filter === "in-attesa" && c.status !== "in-attesa") return false; if (filter === "lasciata" && c.status !== "lasciata") return false; if (filter === "non-richiesta"&& c.status !== "non-richiesta")return false; if (filter === "altro" && !["nessuna","rifiutata"].includes(c.status)) return false; if (query) { const q = query.toLowerCase(); return ( (c.first + " " + c.last).toLowerCase().includes(q) || c.email.toLowerCase().includes(q) || c.phone.includes(q) ); } return true; }); }, [clients, filter, query]); const message = messages.find(m => m.id === selectedMsg); // ── Message CRUD ─────────────────────────────────────────────────────────── const saveMessage = async m => { try { if (m.id) { const res = await fetch(API + "/api/messages/" + m.id, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: m.title, body: m.body }), }); const updated = await res.json(); setMessages(ms => ms.map(x => x.id === m.id ? updated : x)); } else { const res = await fetch(API + "/api/messages", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: m.title, body: m.body }), }); const created = await res.json(); setMessages(ms => [...ms, created]); setSelectedMsg(created.id); } setEditing(null); showToast("Messaggio salvato"); } catch (e) { showToast("⚠️ Errore: " + e.message); } }; const deleteMessage = async id => { await fetch(API + "/api/messages/" + id, { method: "DELETE" }); setMessages(ms => ms.filter(x => x.id !== id)); if (selectedMsg === id) { const rem = messages.filter(x => x.id !== id); setSelectedMsg(rem[0]?.id || null); } showToast("Messaggio eliminato"); }; // ── Render ───────────────────────────────────────────────────────────────── return (
{/* ── Top bar ──────────────────────────────────────────────────────── */}
MENOUNO / FRONT OFFICE — RECENSIONI
setQuery(e.target.value)} /> {query && ( )}
{/* ── Chips ────────────────────────────────────────────────────────── */} {/* ── Body ─────────────────────────────────────────────────────────── */}
{/* TABLE */}
Cliente
Contatto pref.
Tatuatore
Soddisfaz.
Richiesta
Modalità
Risultato
Stelle
Foto
Azione
{loading ? ( Array.from({ length: 6 }).map((_, i) => (
)) ) : error ? (
⚠️ {error}
) : ( <> {filtered.map(c => ( patchClient(c.id, updates)} onSend={() => handleSend(c)} /> ))} {filtered.length === 0 && (
Nessun cliente trovato.
)} )}
{filtered.length} di {clients.length} clienti Seleziona un messaggio → clicca Invia → si apre WhatsApp/Email/Telefono già compilato
{/* SIDEBAR */} {sideOpen && (