// New Property Wizard — 2 steps function NewPropertyWizard({ onClose, onCreated, showToast, host }) { const [step, setStep] = React.useState(1); const TOTAL = 2; // Step 1 const [name, setName] = React.useState(""); const [address, setAddress] = React.useState(""); const [comune, setComune] = React.useState(""); const [civico, setCivico] = React.useState(""); const [beds, setBeds] = React.useState(""); const [photoFile, setPhotoFile] = React.useState(null); const [photoName, setPhotoName] = React.useState(null); const [photoDrag, setPhotoDrag] = React.useState(false); const photoInputRef = React.useRef(null); // Address autocomplete (Photon — photon.komoot.io) const [addrSuggestions, setAddrSuggestions] = React.useState([]); const [addrOpen, setAddrOpen] = React.useState(false); const [addrLoading, setAddrLoading] = React.useState(false); const addrDebounce = React.useRef(null); const addrAbort = React.useRef(null); // Step 2 const [checkin, setCheckin] = React.useState(""); const [checkout, setCheckout] = React.useState(""); const [noteGenerali, setNoteGenerali] = React.useState(""); const [checkinEnabled, setCheckinEnabled] = React.useState(false); const [documents, setDocuments] = React.useState([]); const [docDrag, setDocDrag] = React.useState(false); const docInputRef = React.useRef(null); // PayTourist — auto-detect dal comune dell'indirizzo const [ptSystems, setPtSystems] = React.useState([]); const [ptMatchedSystem, setPtMatchedSystem] = React.useState(null); const [ptStructures, setPtStructures] = React.useState([]); const [ptStructureId, setPtStructureId] = React.useState(""); const [ptLoadingStructures, setPtLoadingStructures] = React.useState(false); // Token per-property (inserito inline nel wizard — usato al submit) const [ptTokenInput, setPtTokenInput] = React.useState(""); const [ptShowToken, setPtShowToken] = React.useState(false); const [saving, setSaving] = React.useState(false); const reset = () => { setStep(1); setName(""); setAddress(""); setComune(""); setCivico(""); setBeds(""); setPhotoFile(null); setPhotoName(null); setAddrSuggestions([]); setAddrOpen(false); setCheckin(""); setCheckout(""); setNoteGenerali(""); setCheckinEnabled(false); setDocuments([]); setPtMatchedSystem(null); setPtStructureId(""); setPtTokenInput(""); setPtSystems([]); setPtStructures([]); }; // Fetch sistemi PayTourist alla prima apertura dello step 2 con check-in attivo React.useEffect(() => { if (checkinEnabled && ptSystems.length === 0) { window.API.listPaytouristSystems().then(setPtSystems).catch(() => {}); } if (!checkinEnabled) { setPtMatchedSystem(null); setPtStructures([]); setPtStructureId(""); } }, [checkinEnabled]); // Auto-detect sistema PayTourist quando cambiano indirizzo o lista sistemi React.useEffect(() => { if (!checkinEnabled || !ptSystems.length) return; const matched = _detectPtSystem(buildFullAddress(), ptSystems); setPtMatchedSystem(matched || null); if (!matched) { setPtStructures([]); setPtStructureId(""); } }, [checkinEnabled, ptSystems, address, civico]); // Fetch strutture quando viene rilevato un sistema e il token locale è inserito // Nel wizard il token non è ancora salvato; usiamo ptTokenInput come trigger. // Le strutture vengono caricate solo se c'è un token da usare. React.useEffect(() => { if (!ptMatchedSystem || !ptTokenInput.trim()) { setPtStructures([]); setPtStructureId(""); return; } setPtLoadingStructures(true); // Passiamo un propertyId fittizio (non ancora creata): non possiamo usare // l'endpoint property-scoped. Usiamo un sistema temporaneo di lookup diretto // chiamando l'API checkin-structure (non disponibile nel wizard) — soluzione // semplificata: mostriamo il campo token ma non pre-carichiamo le strutture. // Le strutture verranno disponibili dopo il salvataggio della property con il token. setPtLoadingStructures(false); setPtStructures([]); }, [ptMatchedSystem?.id, ptTokenInput]); const close = () => { reset(); onClose(); }; const step1Valid = name.trim() && address.trim() && beds; // --------------------------------------------------------------------------- // Address autocomplete — Photon (photon.komoot.io) // Progettato per autocomplete: ~50-100ms, gratuito, no API key, basato su OSM // --------------------------------------------------------------------------- const fetchSuggestions = async (query) => { if (query.length < 3) { setAddrSuggestions([]); setAddrOpen(false); return; } if (addrAbort.current) addrAbort.current.abort(); const ctrl = new AbortController(); addrAbort.current = ctrl; setAddrLoading(true); try { const url = `https://photon.komoot.io/api/?q=${encodeURIComponent(query)}&limit=10&lang=en&osm_tag=highway`; const res = await fetch(url, { signal: ctrl.signal }); const data = await res.json(); const features = (data.features || []) .filter(f => f.properties?.osm_key === "highway" && f.properties?.name) .slice(0, 5); setAddrSuggestions(features); setAddrOpen(features.length > 0); } catch (e) { if (e.name !== "AbortError") setAddrSuggestions([]); } finally { setAddrLoading(false); } }; const onAddressChange = (value) => { setAddress(value); setAddrOpen(false); clearTimeout(addrDebounce.current); addrDebounce.current = setTimeout(() => fetchSuggestions(value), 200); }; const selectSuggestion = (s) => { const p = s.properties || {}; const road = p.name || ""; const city = p.city || p.town || p.village || p.locality || ""; const country = p.country || ""; setAddress([road, city, country].filter(Boolean).join(", ")); if (city) setComune(city); setAddrSuggestions([]); setAddrOpen(false); }; // Inserisce il civico dopo la prima parola di indirizzo: "Via Roma, Milano" + "5" → "Via Roma 5, Milano" const buildFullAddress = () => { const base = address.trim(); const num = civico.trim(); if (!num) return base; const idx = base.indexOf(","); return idx === -1 ? `${base} ${num}` : `${base.slice(0, idx)} ${num}${base.slice(idx)}`; }; const closeDropdownDelayed = () => setTimeout(() => setAddrOpen(false), 150); // --------------------------------------------------------------------------- // File handlers // --------------------------------------------------------------------------- const handlePhotoFiles = (files) => { const file = files && files[0]; if (!file) return; setPhotoFile(file); setPhotoName(file.name); }; const handleDocFiles = (files) => { const arr = files ? Array.from(files) : []; setDocuments(d => [...d, ...arr.map(f => ({ id: "wd" + Date.now() + Math.random(), name: f.name, size: f.size < 1024 * 1024 ? `${Math.round(f.size / 1024)} KB` : `${(f.size / 1024 / 1024).toFixed(1)} MB`, file: f, }))]); }; // --------------------------------------------------------------------------- // Submit // --------------------------------------------------------------------------- const finish = async () => { if (saving) return; setSaving(true); try { const prop = await window.API.createProperty({ name: name.trim(), address: buildFullAddress() || null, comune: comune.trim() || null, rooms: beds ? parseInt(beds, 10) : null, checkin_info: checkin.trim() || null, checkout_info: checkout.trim() || null, extra_info: noteGenerali.trim() || null, checkin_enabled: checkinEnabled, }); if (photoFile) { try { await window.API.uploadPhoto(prop.id, photoFile); } catch (_) {} } for (const doc of documents) { try { await window.API.uploadDocument(prop.id, doc.file); } catch (_) {} } // Salva il token PayTourist sulla property se inserito if (ptTokenInput.trim()) { try { await window.API.updatePaytouristConfig(prop.id, { paytourist_token: ptTokenInput.trim(), paytourist_enabled: !!(ptMatchedSystem && ptStructureId), paytourist_system_id: ptMatchedSystem ? ptMatchedSystem.id : null, paytourist_structure_id: ptStructureId ? parseInt(ptStructureId, 10) : null, }); } catch (_) {} } else if (ptMatchedSystem && ptStructureId) { // Token non inserito nel wizard, ma struttura rilevata — salva solo la config try { await window.API.updatePaytouristConfig(prop.id, { paytourist_enabled: false, paytourist_system_id: ptMatchedSystem.id, paytourist_structure_id: parseInt(ptStructureId, 10), }); } catch (_) {} } if (showToast) showToast(`"${prop.name}" creata con successo`); reset(); onCreated(prop.id); } catch (err) { if (showToast) showToast(err.message || "Errore nella creazione", "error"); } finally { setSaving(false); } }; // --------------------------------------------------------------------------- // Render // --------------------------------------------------------------------------- return (
{ handlePhotoFiles(e.target.files); e.target.value = ""; }} /> { handleDocFiles(e.target.files); e.target.value = ""; }} /> {/* Sidebar — nascosta su mobile via CSS */} {(() => { const WIZARD_STEPS = [ { n: 1, label: "Informazioni struttura", sub: "Nome, indirizzo, camere, foto" }, { n: 2, label: "Informazioni per gli ospiti", sub: "Check-in, check-out, documenti" }, ]; return ( <>
Argo
{WIZARD_STEPS.map(s => { const done = step > s.n; const active = step === s.n; return (
{done ? : s.n}
{s.label}
{s.sub}
); })}
Potrai modificare tutte le informazioni in qualsiasi momento dalla scheda della struttura.
{/* Main */}
{/* Top bar */}
Step {step} di {TOTAL}

{step === 1 ? "Informazioni sulla struttura" : "Informazioni per gli ospiti"}

{/* Stepper mobile — visibile solo su ≤768px */}
{WIZARD_STEPS.map((s, idx) => { const done = step > s.n; const active = step === s.n; return (
{done ? : s.n} {s.label}
{idx < WIZARD_STEPS.length - 1 && (
s.n ? "done" : ""}`} /> )} ); })}
{/* Body */}
{/* ===== STEP 1 ===== */} {step === 1 && (
{/* Nome */}
setName(e.target.value)} placeholder="Es. Casa dei Glicini, Trullo del Nonno…" autoFocus />
{/* Indirizzo + civico */}
{/* Campo autocomplete */}
onAddressChange(e.target.value)} onBlur={closeDropdownDelayed} onFocus={() => addrSuggestions.length > 0 && setAddrOpen(true)} placeholder="Via Roma, Milano…" autoComplete="off" /> {addrLoading && (
)}
{/* Dropdown */} {addrOpen && addrSuggestions.length > 0 && (
{addrSuggestions.map((s, i) => { const p = s.properties || {}; const road = p.name || ""; const city = p.city || p.town || p.village || p.locality || ""; const country = p.country || ""; return ( ); })}
)}
{/* Numero civico */}
setCivico(e.target.value)} placeholder="N°" style={{ textAlign: "center" }} />
Cerca la via e aggiungi il numero civico nel campo a destra
{/* Comune */}
setComune(e.target.value)} placeholder="Es. Venezia" style={{ maxWidth: 280 }} />
Compilato automaticamente dalla selezione dell'indirizzo. Verificalo.
{/* Camere */}
setBeds(e.target.value)} placeholder="4" style={{ maxWidth: 120 }} />
{/* Foto */}
{ e.preventDefault(); setPhotoDrag(true); }} onDragLeave={() => setPhotoDrag(false)} onDrop={e => { e.preventDefault(); setPhotoDrag(false); handlePhotoFiles(e.dataTransfer.files); }} onClick={() => photoInputRef.current?.click()}> {photoName ? (
{photoName}
Caricata ·{" "} { e.stopPropagation(); setPhotoFile(null); setPhotoName(null); }}> rimuovi
) : ( <>
Trascina qui la foto
JPG, PNG o WEBP · max 10 MB · sfoglia dal computer
)}
)} {/* ===== STEP 2 ===== */} {step === 2 && (
Queste informazioni vengono usate dal chatbot per rispondere automaticamente agli ospiti. Puoi sempre modificarle in seguito.