// Simple placeholder pages for Properties list, Subscription, Settings function PropertyCard({ p, onOpenProperty }) { const activeCount = p.sessions.filter(s => s.status === "active").length; const pendingCount = p.sessions.filter(s => s.status === "pending").length; return (
onOpenProperty(p.id)}>
{p.photo_url ? {p.name} : p.name}
{p.name}
{p.address || p.city}
{p.rooms != null ? `${p.rooms} ${p.rooms === 1 ? "camera" : "camere"}` : ""}
{activeCount > 0 && ( {activeCount} chat attiv{activeCount === 1 ? "a" : "e"} )} {pendingCount > 0 && ( {pendingCount} in attesa )} {activeCount === 0 && pendingCount === 0 && ( Nessuna sessione )}
); } function PropertiesListPage({ state, onOpenProperty, onNewProperty }) { return (

Le mie strutture

{state.properties.length} strutture · {state.properties.reduce((a, p) => a + p.sessions.length, 0)} sessioni totali

{state.properties.map(p => ( ))} {/* Add new card */}
Aggiungi struttura
); } window.PropertyCard = PropertyCard; // Sconto annuale — deve corrispondere a ANNUAL_DISCOUNT in stripe_service.py const SUB_ANNUAL_DISCOUNT = 0.90; const SUB_PLANS_META = [ { id: "starter", name: "Starter", monthlyPrice: 19, properties: "4 strutture", features: ["Chatbot WhatsApp/Telegram", "30 messaggi/giorno per camera", "Documenti PDF", "QR Code", "Supporto email"] }, { id: "pro", name: "Pro", monthlyPrice: 39, properties: "fino a 10 strutture", features: ["Tutto di Starter", "100 messaggi/giorno per camera", "Check-in digitale OCR", "Supporto prioritario"] }, { id: "business", name: "Business", monthlyPrice: 79, properties: "fino a 20 strutture", features: ["Tutto di Pro", "150 messaggi/giorno per camera", "API access", "Account manager dedicato"] }, ]; function SubscriptionPage({ host, showToast }) { const [tab, setTab] = React.useState("plan"); const [invoices, setInvoices] = React.useState([]); const [loadingInv, setLoadingInv] = React.useState(false); const [downloadingId, setDownloadingId] = React.useState(null); const [billingStatus, setBillingStatus] = React.useState(null); const [portalLoading, setPortalLoading] = React.useState(false); const [interval, setInterval] = React.useState("monthly"); const [checkoutPlan, setCheckoutPlan] = React.useState(null); React.useEffect(() => { window.API.getBillingStatus().then(setBillingStatus).catch(() => {}); }, []); const handlePortal = async () => { setPortalLoading(true); try { const { url } = await window.API.createPortal(); window.location.href = url; } catch (err) { if (showToast) showToast(err.message || "Errore", "error"); setPortalLoading(false); } }; const isCustomPlan = billingStatus?.plan_type === "custom"; const billingLoaded = billingStatus !== null; const currentPlan = (host?.plan || "starter").toLowerCase(); const currentMeta = SUB_PLANS_META.find(p => p.id === currentPlan) || SUB_PLANS_META[0]; const statusLabel = { active: { text: "Attivo", cls: "badge-green" }, trialing: { text: "Trial", cls: "badge-blue" }, past_due: { text: "Pagamento in sospeso", cls: "badge-amber" }, canceled: { text: "Cancellato", cls: "badge-red" }, incomplete: { text: "Incompleto", cls: "badge-gray" }, }; const subStatus = billingStatus?.subscription_status; const statusInfo = statusLabel[subStatus] || null; const loadInvoices = async () => { setLoadingInv(true); try { const data = await window.API.listInvoices(); setInvoices(data); } catch (err) { if (showToast) showToast(err.message || "Errore caricamento fatture", "error"); } finally { setLoadingInv(false); } }; React.useEffect(() => { if (tab === "invoices" || isCustomPlan) loadInvoices(); }, [tab, isCustomPlan]); const handleDownload = async (inv) => { setDownloadingId(inv.id); try { await window.API.downloadInvoice(inv.id, inv.filename); } catch (err) { if (showToast) showToast(err.message || "Errore download", "error"); } finally { setDownloadingId(null); } }; const fmtPeriod = (p) => { if (!p) return "n.d."; const [y, m] = p.split("-"); const months = ["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"]; return `${months[parseInt(m, 10) - 1]} ${y}`; }; const fmtAmount = (a) => a != null ? `€${parseFloat(a).toFixed(2).replace(".", ",")}` : "n.d."; const fmtMonthly = (mp) => interval === "monthly" ? `€${mp}` : `€${(mp * SUB_ANNUAL_DISCOUNT).toFixed(2).replace(".00", "")}`; const annualSaving = (mp) => Math.round(mp * 12 * (1 - SUB_ANNUAL_DISCOUNT)); if (!billingLoaded) { return (

Abbonamento

Caricamento…
); } if (isCustomPlan) { const priceMonthly = billingStatus?.price_monthly_eur; const priceYearly = billingStatus?.price_yearly_eur; const propertyLimit = billingStatus?.property_limit; const dailyMsgLimit = billingStatus?.daily_msg_limit; const subStatus = billingStatus?.subscription_status; const hasSubscription = billingStatus?.has_subscription; const customStatusLabel = { active: { text: "Attivo", cls: "badge-green" }, trialing: { text: "Trial", cls: "badge-blue" }, past_due: { text: "Pagamento in sospeso", cls: "badge-amber" }, canceled: { text: "Cancellato", cls: "badge-red" }, incomplete: { text: "Incompleto", cls: "badge-gray" }, }; const customStatusInfo = customStatusLabel[subStatus] || null; return (

Abbonamento

Piano personalizzato gestito dall'assistenza.

{billingStatus?.plan_display_name || "Piano custom"} {customStatusInfo && hasSubscription && ( {customStatusInfo.text} )}
{priceMonthly != null ? `€${priceMonthly.toFixed(2).replace(".00", "")}/mese` : "Fatturazione personalizzata"}

Fatture

{loadingInv ? (
Caricamento fatture…
) : invoices.length === 0 ? (
Nessuna fattura disponibile
Le fatture appariranno qui non appena emesse
) : (
{invoices.map(inv => ( ))}
Fattura Periodo Importo Data emissione
{inv.filename} {fmtPeriod(inv.period)} {fmtAmount(inv.amount)} {inv.issued_at ? new Date(inv.issued_at).toLocaleDateString("it-IT", { day: "2-digit", month: "short", year: "numeric" }) : "n.d."}
)}

Per modifiche al piano o al metodo di pagamento, contatta l'assistenza.

); } return (

Abbonamento

Gestisci il tuo piano e le fatture.

{/* Banner pagamento in sospeso */} {subStatus === "past_due" && (
⚠️ Il pagamento dell'abbonamento non è andato a buon fine. Aggiorna il metodo di pagamento per evitare interruzioni.
)} {/* Card stato abbonamento */}
Piano {currentMeta.name} {statusInfo ? {statusInfo.text} : billingStatus && !billingStatus.has_subscription ? Nessun abbonamento : null}
€{currentMeta.monthlyPrice}/mese
{billingStatus?.has_subscription && ( )}
{/* Tab switcher */}
{[["plan", "Piano"], ["invoices", "Fatture"]].map(([id, label]) => ( ))}
{tab === "plan" && (

Cambia piano

{/* Toggle mensile / annuale */}
{["monthly", "yearly"].map(iv => ( ))}
{/* Layout: plan cards + checkout panel — responsive via classe CSS */}
{SUB_PLANS_META.map(p => { const isCurrent = p.id === currentPlan; const isSelected = checkoutPlan === p.id; return (
{ if (!isCurrent) setCheckoutPlan(p.id); }}> {isCurrent && (
Attuale
)}
{p.name}
{fmtMonthly(p.monthlyPrice)} /mese
{interval === "yearly" && (
Risparmi €{annualSaving(p.monthlyPrice)}/anno
)}
{p.properties}
    {p.features.map((f, i) => (
  • {f}
  • ))}
{!isCurrent && ( )} {isCurrent && ( )}
); })}
{/* Pannello checkout embedded */}
{checkoutPlan && ( <>
Piano {SUB_PLANS_META.find(p => p.id === checkoutPlan)?.name} · {interval === "monthly" ? "mensile" : "annuale"}
)}
)} {tab === "invoices" && (
{loadingInv ? (
Caricamento fatture…
) : invoices.length === 0 ? (
Nessuna fattura disponibile
Le fatture appariranno qui non appena emesse
) : (
{invoices.map(inv => ( ))}
Fattura Periodo Importo Data emissione
{inv.filename} {fmtPeriod(inv.period)} {fmtAmount(inv.amount)} {inv.issued_at ? new Date(inv.issued_at).toLocaleDateString("it-IT", { day: "2-digit", month: "short", year: "numeric" }) : "n.d."}
)}
)}
); } function SettingsPage({ host, showToast, onHostUpdated }) { const [fullName, setFullName] = React.useState(host?.full_name || ""); const [phone, setPhone] = React.useState(host?.phone_number || ""); const [saving, setSaving] = React.useState(false); // Impostazione invio automatico schedine PayTourist const [autoSubmit, setAutoSubmit] = React.useState(host?.paytourist_auto_submit ?? false); React.useEffect(() => { setAutoSubmit(host?.paytourist_auto_submit ?? false); }, [host?.paytourist_auto_submit]); // Approvazione automatica degli ospiti (piani chatbot) const [autoApprove, setAutoApprove] = React.useState(host?.auto_approve_sessions ?? false); React.useEffect(() => { setAutoApprove(host?.auto_approve_sessions ?? false); }, [host?.auto_approve_sessions]); React.useEffect(() => { setFullName(host?.full_name || ""); setPhone(host?.phone_number || ""); }, [host?.full_name, host?.phone_number]); const [currentPw, setCurrentPw] = React.useState(""); const [newPw, setNewPw] = React.useState(""); const [confirmPw, setConfirmPw] = React.useState(""); const [pwError, setPwError] = React.useState(""); const [savingPw, setSavingPw] = React.useState(false); const save = async () => { setSaving(true); try { const updated = await window.API.updateHost({ full_name: fullName.trim() || null, phone_number: phone.trim() || null }); if (updated && onHostUpdated) onHostUpdated(updated); if (showToast) showToast("Profilo aggiornato"); } catch (err) { if (showToast) showToast(err.message || "Errore", "error"); } finally { setSaving(false); } }; const changePassword = async () => { setPwError(""); if (newPw !== confirmPw) { setPwError("Le password non coincidono."); return; } if (newPw.length < 8) { setPwError("La nuova password deve essere di almeno 8 caratteri."); return; } setSavingPw(true); try { await window.API.changePassword(currentPw, newPw); setCurrentPw(""); setNewPw(""); setConfirmPw(""); if (showToast) showToast("Password aggiornata"); } catch (err) { setPwError(err.message || "Errore"); } finally { setSavingPw(false); } }; const toggleAutoApprove = async () => { const newValue = !autoApprove; setAutoApprove(newValue); // ottimistico try { const updated = await window.API.updateHost({ auto_approve_sessions: newValue }); if (updated && onHostUpdated) onHostUpdated(updated); } catch (err) { setAutoApprove(!newValue); // ripristina in caso di errore if (showToast) showToast(err.message || "Errore aggiornamento impostazione", "error"); } }; const toggleAutoSubmit = async () => { const newValue = !autoSubmit; setAutoSubmit(newValue); // ottimistico try { const updated = await window.API.updateHost({ paytourist_auto_submit: newValue }); if (updated && onHostUpdated) onHostUpdated(updated); } catch (err) { setAutoSubmit(!newValue); // ripristina in caso di errore if (showToast) showToast(err.message || "Errore aggiornamento impostazione", "error"); } }; return (

Impostazioni

Profilo, notifiche e preferenze.

Profilo

setFullName(e.target.value)} />
setPhone(e.target.value)} placeholder="+39 333 1234567" />

Modifica password

setCurrentPw(e.target.value)} autoComplete="current-password" />
setNewPw(e.target.value)} autoComplete="new-password" />
setConfirmPw(e.target.value)} autoComplete="new-password" />
{pwError &&
{pwError}
}

Notifiche

{[ { label: "Email per nuove richieste di accesso", on: true }, { label: "Email per recensioni degli ospiti", on: true }, { label: "Riepilogo settimanale", on: false }, ].map((n, i) => (
{n.label}
))}
{["starter", "pro", "business"].includes(host?.plan) && (

Approvazione ospiti

Approvazione automatica degli ospiti
{autoApprove ? "Attiva — gli ospiti che scansionano il QR accedono subito al chatbot senza la tua approvazione e non ricevi l'email di richiesta accesso." : "Disattivata — ogni ospite che scansiona il QR resta in attesa finché non approvi la richiesta dal link ricevuto via email."}
)} {["pro", "business"].includes(host?.plan) && (

Invio schedine PayTourist

{autoSubmit ? "Invio automatico attivo" : "Revisione prima dell'invio"}
{autoSubmit ? "Invia automaticamente — le schedine vengono inviate a PayTourist appena l'ospite completa il check-in (se i dati sono completi, altrimenti ricevi comunque l'email di revisione)." : "Rivedi prima di inviare — le schedine restano in bozza e ricevi un'email per controllarle e inviarle."}
)}
); } window.PropertiesListPage = PropertiesListPage; window.SubscriptionPage = SubscriptionPage; window.SettingsPage = SettingsPage;