// 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 (
Check-in digitale
L'ospite riceve un link per compilare online il check-in di tutto il nucleo. Genera un form PDF scaricabile.
Inserisci il token PayTourist per questa struttura (opzionale, modificabile in seguito).
Dopo aver creato la struttura, seleziona la struttura PayTourist dal dettaglio per completare la configurazione.