const { useEffect, useMemo, useState } = React; const API = "./api"; const districts = ["Miraflores", "Surco", "San Isidro", "San Miguel", "La Molina", "Jesus Maria", "Magdalena", "Barranco", "Lince", "Pueblo Libre"]; const urgencyOptions = ["baja", "media", "alta"]; const requestStatuses = ["new", "reviewed", "contacted", "closed"]; const categoryInsights = { Carpinteria: { range: "Desde S/ 80 a S/ 350 segun ajuste, reparacion o instalacion.", includes: ["Ajuste de puertas y bisagras", "Reparacion de muebles", "Instalacion de repisas o accesorios"], tip: "Envia fotos del mueble o puerta para que el tecnico estime materiales y tiempo antes de visitarte.", }, Cerrajeria: { range: "Desde S/ 60 a S/ 220 segun apertura, cambio de chapa o nivel de seguridad.", includes: ["Apertura de puertas", "Cambio de chapas", "Mejora de seguridad domestica"], tip: "Indica si la cerradura es simple, multipunto o digital para evitar cotizaciones incompletas.", }, Electricidad: { range: "Desde S/ 70 a S/ 300 segun punto electrico, luminaria o tablero.", includes: ["Instalacion de tomacorrientes", "Cambio de luminarias", "Revision de llaves y tableros"], tip: "Si hay chispas, olor a quemado o cortes recurrentes, solicita atencion prioritaria.", }, Gasfiteria: { range: "Desde S/ 70 a S/ 280 segun fuga, griferia, desatoro o conexion.", includes: ["Reparacion de fugas", "Cambio de griferias", "Desatoros y conexiones de agua"], tip: "Cierra la llave de paso si hay fuga activa y comparte una foto o video corto del problema.", }, Limpieza: { range: "Desde S/ 90 a S/ 300 segun area, profundidad y horas requeridas.", includes: ["Limpieza profunda", "Mantenimiento por horas", "Apoyo para mudanza o post obra"], tip: "Indica metraje aproximado, cantidad de ambientes y si necesitas insumos incluidos.", }, "Mantenimiento general": { range: "Desde S/ 80 a S/ 320 segun cantidad de tareas y complejidad.", includes: ["Ajustes menores del hogar", "Instalaciones simples", "Revision preventiva"], tip: "Agrupa varias tareas pequenas en una sola visita para aprovechar mejor el costo de traslado.", }, Pintura: { range: "Desde S/ 180 por ambiente pequeno; varia por metraje, resanes y acabado.", includes: ["Pintado de ambientes", "Resanes menores", "Acabados y retoques"], tip: "Define si necesitas solo mano de obra o tambien materiales para comparar cotizaciones de forma justa.", }, "Reparaciones menores": { range: "Desde S/ 60 a S/ 200 segun tiempo, herramientas y materiales.", includes: ["Ajustes rapidos", "Instalaciones pequenas", "Correcciones de uso diario"], tip: "Describe cada tarea por separado para que el tecnico confirme si puede resolverlas en una visita.", }, }; const experienceOptions = [ { label: "0 a 5 anos", value: "0" }, { label: "5 a 10 anos", value: "5" }, { label: "Mas de 10 anos", value: "10" }, ]; const dayOptions = ["Lunes", "Martes", "Miercoles", "Jueves", "Viernes", "Sabado", "Domingo"]; const scheduleOptions = ["Manana", "Tarde", "Noche", "Horario completo", "Emergencias", "Previa coordinacion"]; function apiUrl(path) { return `${API}/${path}`; } async function requestJson(path, options = {}) { const response = await fetch(apiUrl(path), { headers: { "Content-Type": "application/json", ...(options.headers || {}) }, credentials: "include", ...options, }); const data = await response.json(); if (!response.ok || data.success === false) { throw new Error(data.message || "No se pudo completar la accion."); } return data; } function formatExperience(value) { const found = experienceOptions.find((item) => item.value === String(value)); return found ? found.label : `${value} anos`; } function MultiSelect({ label, options, selected, onChange, placeholder = "Buscar y seleccionar" }) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const filteredOptions = options.filter((option) => option.toLowerCase().includes(query.toLowerCase())); function toggle(option) { if (selected.includes(option)) { onChange(selected.filter((item) => item !== option)); return; } onChange([...selected, option]); } return ( ); } function ServiceGlyph({ name }) { const key = (name || "").toLowerCase(); const common = { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }; let paths = ( <> ); if (key.includes("gasf")) { paths = <>; } else if (key.includes("elect")) { paths = <>; } else if (key.includes("pint")) { paths = <>; } else if (key.includes("cerra")) { paths = <>; } else if (key.includes("limp")) { paths = <>; } else if (key.includes("carp")) { paths = <>; } else if (key.includes("mantenimiento")) { paths = <>; } else if (key.includes("repar")) { paths = <>; } return ; } function Header() { return (
Hogar360
); } function SiteFooter() { return ( ); } function Hero({ categories, filters, setFilters, onRequest }) { return (

Servicios para cuidar tu hogar, en un solo lugar

Encuentra tecnicos confiables para tu hogar en minutos

Gasfiteros, electricistas, pintores y especialistas verificados cerca de ti.

Soy tecnico
Buscar tecnicos
); } function CategoryDescriptionModal({ category, onClose, onExplore }) { if (!category) return null; const insight = categoryInsights[category.name] || { range: "El costo depende del alcance, materiales y urgencia del servicio.", includes: ["Revision del problema", "Cotizacion segun alcance", "Coordinacion directa con el tecnico"], tip: "Describe el problema con detalle y adjunta fotos cuando contactes al tecnico.", }; return (
event.stopPropagation()}>

Servicio

{category.name}

{category.description}

Referencia {insight.range}
Suele incluir
    {insight.includes.map((item) =>
  • {item}
  • )}
Consejo rapido

{insight.tip}

); } function CategoryGrid({ categories, onExploreCategory }) { const [selectedCategory, setSelectedCategory] = useState(null); const steps = [ "Describe tu problema.", "Elige servicio, distrito y fecha.", "Te conectamos con un tecnico disponible.", "Recibe el servicio y califica la experiencia.", ]; return (

Categorias

Elige el servicio que necesitas

{categories.map((category) => ( ))}

Pasos

Solicita tu servicio en minutos

{steps.map((step, index) => (
{index + 1}

{step}

))}
H360
setSelectedCategory(null)} onExplore={(categoryName) => { setSelectedCategory(null); onExploreCategory(categoryName); }} />
); } function ProviderDirectory({ providers, onRequest, filters, setFilters, categories }) { const [page, setPage] = useState(0); const [activeProvider, setActiveProvider] = useState(null); const filtered = useMemo(() => providers.filter((provider) => { const byCategory = !filters.category || provider.category === filters.category; const byDistrict = !filters.district || provider.district === filters.district || provider.service_area?.includes(filters.district); return byCategory && byDistrict; }), [providers, filters]); const totalPages = Math.max(1, Math.ceil(filtered.length / 3)); const visibleProviders = filtered.slice(page * 3, page * 3 + 3); useEffect(() => { setPage(0); }, [filters.category, filters.district, providers.length]); function previousPage() { setPage((current) => Math.max(0, current - 1)); } function nextPage() { setPage((current) => Math.min(totalPages - 1, current + 1)); } return (

Directorio verificado

Tecnicos aprobados por Hogar360

{visibleProviders.map((provider) => (
{provider.photo_url ? {provider.full_name} : provider.full_name.slice(0, 1)}
Aprobado {provider.full_name}

{provider.category}

{provider.service_area || provider.district}

{formatExperience(provider.experience_years)} {provider.reference_price || "Tarifa por evaluar"}
))} {!filtered.length &&

Aun no hay tecnicos aprobados con esos filtros.

}
{filtered.length > 3 && (
{page + 1} de {totalPages}
)}
setActiveProvider(null)} onRequest={(provider) => { setActiveProvider(null); onRequest(provider); }} />
); } function ProviderProfileModal({ provider, onClose, onRequest }) { if (!provider) return null; return (
event.stopPropagation()}>
{provider.photo_url ? {provider.full_name} : provider.full_name.slice(0, 1)}
Aprobado

{provider.full_name}

Categoria: {provider.category}

Oficio: {provider.trade}

Distrito base: {provider.district}

Zonas de atencion: {provider.service_area || provider.district}

Experiencia: {formatExperience(provider.experience_years)}

Disponibilidad: {provider.availability || "Por coordinar"}

Tarifa referencial: {provider.reference_price || "Por evaluar"}

Descripcion: {provider.description}

Calificacion: 4.8

); } function RequestModal({ open, categories, providers, selectedProvider, onClose }) { if (!open) return null; return (
event.stopPropagation()}>

Solicitar servicio

Cuentanos que necesitas arreglar

); } function ServiceRequestForm({ categories, providers = [], selectedProvider, onDone }) { const [form, setForm] = useState({ customer_name: "", customer_whatsapp: "", customer_email: "", district: "", category: "", problem_description: "", urgency: "media", preferred_date: "", preferred_start_time: "", preferred_end_time: "", provider_id: selectedProvider?.id || "", }); const [message, setMessage] = useState(""); const [whatsappUrl, setWhatsappUrl] = useState(""); const [recommendedProviders, setRecommendedProviders] = useState([]); const [selectedRecommendedIds, setSelectedRecommendedIds] = useState([]); const [submittedRequest, setSubmittedRequest] = useState(null); const [saving, setSaving] = useState(false); const isProviderRequest = Boolean(selectedProvider); const providerDistricts = useMemo(() => { if (!selectedProvider) return districts; const serviceAreas = String(selectedProvider.service_area || "") .split(",") .map((area) => area.trim()) .filter(Boolean); return [...new Set([selectedProvider.district, ...serviceAreas].filter(Boolean))]; }, [selectedProvider]); const categoryExists = categories.some((category) => category.name === selectedProvider?.category); useEffect(() => { setForm((current) => ({ ...current, provider_id: selectedProvider?.id || "", category: selectedProvider?.category || current.category, district: providerDistricts.includes(current.district) ? current.district : providerDistricts[0] || current.district, })); }, [selectedProvider, providerDistricts]); function buildWhatsappUrl(provider, requestData) { if (!provider?.whatsapp) return ""; let phone = String(provider.whatsapp).replace(/\D/g, ""); if (phone.length === 9) { phone = `51${phone}`; } if (phone.length < 9) return ""; const timeRange = requestData.preferred_start_time || requestData.preferred_end_time ? `${requestData.preferred_start_time || "por definir"} - ${requestData.preferred_end_time || "por definir"}` : "por coordinar"; const text = [ `Hola ${provider.full_name}, te contacto desde Hogar360.`, `Necesito un servicio de ${requestData.category}.`, `Distrito: ${requestData.district}.`, `Fecha tentativa: ${requestData.preferred_date || "por coordinar"}.`, `Horario: ${timeRange}.`, `Urgencia: ${requestData.urgency}.`, `Descripcion: ${requestData.problem_description}.`, `Mi nombre es ${requestData.customer_name} y mi WhatsApp es ${requestData.customer_whatsapp}.`, ].join("\n"); return `https://wa.me/${phone}?text=${encodeURIComponent(text)}`; } function priceValue(provider) { const match = String(provider.reference_price || "").match(/\d+/); return match ? Number(match[0]) : 999999; } function providerScore(provider, requestData) { let score = 0; if (provider.category === requestData.category) score += 100; if (provider.district === requestData.district) score += 40; if (provider.service_area?.includes(requestData.district)) score += 30; if (provider.reference_price) score += 5; return score; } function buildRecommendations(requestData) { return providers .filter((provider) => provider.category === requestData.category) .filter((provider) => provider.district === requestData.district || provider.service_area?.includes(requestData.district)) .sort((a, b) => { const scoreDiff = providerScore(b, requestData) - providerScore(a, requestData); return scoreDiff || priceValue(a) - priceValue(b); }) .slice(0, 6); } function toggleRecommended(id) { if (selectedRecommendedIds.includes(id)) { setSelectedRecommendedIds(selectedRecommendedIds.filter((item) => item !== id)); return; } setSelectedRecommendedIds([...selectedRecommendedIds, id]); } function openSelectedWhatsappChats() { recommendedProviders .filter((provider) => selectedRecommendedIds.includes(provider.id)) .forEach((provider, index) => { const url = buildWhatsappUrl(provider, submittedRequest); if (url) { setTimeout(() => window.open(url, "_blank", "noopener,noreferrer"), index * 250); } }); } async function submit(event) { event.preventDefault(); setSaving(true); setMessage(""); setWhatsappUrl(""); setRecommendedProviders([]); setSelectedRecommendedIds([]); try { await requestJson("create_request.php", { method: "POST", body: JSON.stringify(form) }); const requestSnapshot = { ...form }; setSubmittedRequest(requestSnapshot); const url = buildWhatsappUrl(selectedProvider, requestSnapshot); setWhatsappUrl(url); if (url) { setMessage("Solicitud registrada. Tambien puedes escribirle al tecnico por WhatsApp."); } else { const recommendations = buildRecommendations(requestSnapshot); setRecommendedProviders(recommendations); setSelectedRecommendedIds(recommendations.map((provider) => provider.id)); setMessage(recommendations.length ? "Solicitud registrada. Te recomendamos estos tecnicos disponibles para que puedas contactarlos por WhatsApp." : "Solicitud enviada. No encontramos tecnicos aprobados con esa categoria y distrito por ahora; el administrador revisara tu solicitud."); } if (!url) { if (!buildRecommendations(requestSnapshot).length) { setForm({ customer_name: "", customer_whatsapp: "", customer_email: "", district: "", category: "", problem_description: "", urgency: "media", preferred_date: "", preferred_start_time: "", preferred_end_time: "", provider_id: "" }); } } if (onDone && !url && !buildRecommendations(requestSnapshot).length) { setTimeout(onDone, 1400); } } catch (error) { setMessage(error.message); } finally { setSaving(false); } } return (
{selectedProvider && (
Solicitando cita con {selectedProvider.full_name} Esta solicitud queda limitada a {selectedProvider.category} y a sus zonas de atencion. Si necesitas otro servicio o distrito, regresa y elige otro tecnico.
)}