// Shared UI primitives for Argo const { useState, useEffect, useRef, useMemo } = React; // Rileva il sistema PayTourist il cui comune corrisponde all'indirizzo della struttura. // Controlla tutte le parti separate da virgola nell'indirizzo (case-insensitive). function _detectPtSystem(address, systems) { if (!address || !systems.length) return null; const parts = address.split(",").map(p => p.trim().toLowerCase()).filter(Boolean); return systems.find(s => { const comune = s.comune.toLowerCase(); return parts.some(p => p === comune || p.startsWith(comune + " ") || comune.startsWith(p)); }) || null; } // --------- ICONS --------- const I = { dashboard: (p) => , home: (p) => , card: (p) => , settings: (p) => , logout: (p) => , plus: (p) => , qr: (p) => , edit: (p) => , download: (p) => , check: (p) => , x: (p) => , upload: (p) => , file: (p) => , trash: (p) => , back: (p) => , chat: (p) => , users: (p) => , building: (p) => , sparkle: (p) => , door: (p) => , pin: (p) => , bell: (p) => , search: (p) => , whatsapp: (p) => , }; // --------- LOGO --------- function Logo({ size = "md" }) { const h = size === "lg" ? 176 : size === "sm" ? 112 : 144; return (
Argo
); } // --------- BADGE --------- function StatusBadge({ status }) { const label = window.STATUS_IT[status] || status; return {label}; } // --------- SIDEBAR --------- function Sidebar({ current, onNavigate, onLogout, host, open }) { const items = [ { id: "dashboard", label: "Dashboard", icon: I.dashboard }, { id: "properties", label: "Le mie strutture", icon: I.home }, { id: "subscription", label: "Abbonamento", icon: I.card }, { id: "settings", label: "Impostazioni", icon: I.settings }, ]; // Iniziali avatar: prime lettere del nome const initials = (host?.full_name || "?") .split(" ").map(p => p[0]).slice(0, 2).join("").toUpperCase(); return ( ); } // --------- TOPBAR --------- function TopBar({ title, breadcrumb, right, onMenuClick }) { return (
{/* Hamburger — visibile ≤1024px tramite CSS */}
{breadcrumb ? (
{breadcrumb.map((b, i) => ( {i > 0 && /} {b.label} ))}
) : (
{title}
)}
{right}
); } // --------- TOAST --------- function useToast() { const [msg, setMsg] = useState(null); useEffect(() => { if (!msg) return; const t = setTimeout(() => setMsg(null), 2400); return () => clearTimeout(t); }, [msg]); const show = (text, type = "success") => setMsg({ text, type }); const node = msg ? (
{msg.type === "success" && } {msg.text}
) : null; return [show, node]; } // --------- MODAL --------- function Modal({ open, onClose, children }) { if (!open) return null; return (
e.stopPropagation()}> {children}
); } // --------- QR CODE (SVG, static pattern) --------- function QRCode({ size = 160, label }) { // Deterministic pseudo-QR pattern (visual only). 25x25 grid. const seed = (label || "argo").split("").reduce((a, c) => a + c.charCodeAt(0), 0); const grid = 25; const cell = size / grid; const rnd = (i) => { let x = (seed * 9301 + i * 49297) % 233280; return (x / 233280); }; const cells = []; for (let y = 0; y < grid; y++) { for (let x = 0; x < grid; x++) { // skip finder pattern squares const inFinder = (x < 8 && y < 8) || (x > 16 && y < 8) || (x < 8 && y > 16); if (inFinder) continue; if (rnd(x * grid + y + 1) > 0.52) { cells.push(); } } } const finder = (fx, fy) => ( ); return ( {cells} {finder(0, 0)} {finder(18, 0)} {finder(0, 18)} ); } // --------- BADGE (generico con varianti) --------- // Affianca StatusBadge (legacy) — usa variant invece di status function Badge({ variant = "plain", children, style }) { return ( {children} ); } // --------- TOGGLE --------- // Toggle on/off accessibile con transizione cubic-bezier // Props: checked, onChange, ariaLabelOn, ariaLabelOff, size ("md" | "sm") function Toggle({ checked, onChange, ariaLabelOn, ariaLabelOff, size = "md" }) { // ariaLabel dinamico: quando off mostra l'azione di abilitazione, viceversa const label = checked ? (ariaLabelOff || "Disabilita") : (ariaLabelOn || "Abilita"); const trackClass = [ "toggle-track", checked ? "on" : "", size === "sm" ? "toggle-track-sm" : "", ].filter(Boolean).join(" "); return ( ); } // --------- PT BANNER (3 stati PayTourist) --------- // state: "uncovered" | "matched-checkin-off" | "matched-no-token" // comune: nome del comune coperto (per stati 2 e 3) // onSettingsClick: callback per aprire impostazioni account (stato 3) function PtBanner({ state, comune, onSettingsClick }) { if (state === "uncovered") { return (
·
Il comune di questa struttura non è ancora coperto da PayTourist.
); } if (state === "matched-checkin-off") { return (
i
{comune} è coperto da PayTourist. Abilita il check-in digitale qui sopra per attivare l'invio automatico delle schedine.
); } if (state === "matched-no-token") { return (
!
{comune} è coperto da PayTourist. {" "}Aggiungi il tuo token nelle{" "} {" "} per attivare l'integrazione.
); } return null; } // --------- PT STRUCTURE PICKER --------- // Card composta per la configurazione della struttura PayTourist // Props: comune, ptEnabled, structures, loading, value, onChange, // onSave, onValidate, onDisable, saving, validating function PtStructurePicker({ comune, ptEnabled, structures, loading, value, onChange, onSave, onValidate, onDisable, saving, validating, error }) { // Se la struttura selezionata (salvata in DB) non compare nell'elenco recuperato // dall'API PayTourist (es. API momentaneamente irraggiungibile, o l'account non // la ritorna più), mostriamo comunque un'opzione sintetica per l'id salvato: // altrimenti il onChange(e.target.value ? parseInt(e.target.value, 10) : null)}> {hasUnknownSelected && ( )} {structures.map(s => ( ))} )} {error && !loading && ( hasUnknownSelected ? (
Impossibile aggiornare l'elenco delle strutture PayTourist in questo momento. La struttura configurata resta attiva e invariata.
) : (
{error}
) )} {!error && structures.length === 0 && !loading && !hasUnknownSelected && (
Nessuna struttura trovata sul tuo account PayTourist.
)}
{ptEnabled && ( )} {ptEnabled && ( )}
); } // --------- DRAWER --------- // Pannello laterale destro con scrim, slide-in, escape, focus trap minimo e scroll-lock. // Props: open, onClose, width (default "480px"), children function Drawer({ open, onClose, width = "480px", children }) { const drawerRef = useRef(null); const lastFocusRef = useRef(null); // Focus trap minimo: focus sul drawer al mount, restore al chiudersi useEffect(() => { if (open) { lastFocusRef.current = document.activeElement; // Piccolo timeout per permettere al CSS di applicarsi prima del focus const t = setTimeout(() => { drawerRef.current?.focus(); }, 50); document.body.classList.add("scroll-lock"); return () => clearTimeout(t); } else { document.body.classList.remove("scroll-lock"); lastFocusRef.current?.focus(); } }, [open]); // Listener Escape useEffect(() => { if (!open) return; const handler = (e) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", handler); return () => document.removeEventListener("keydown", handler); }, [open, onClose]); return ( <>