"use client"; import type { Route } from "next"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { browserFetch } from "../lib/api"; import { logoutByNavigation } from "../lib/logout"; export type AccountSection = | "summary" | "showcases" | "stats" | "locations" | "profile" | "services" | "works" | "inbox" | "reviews" | "subscription" | "settings"; type AccountClientProps = { section?: AccountSection; conversationId?: string; }; type AccountResponse = { maker: { id: string; slug: string; status: string; business_name: string | null; description: string | null; province: string | null; city: string | null; delivery_scope: string | null; availability: string; public_contact_email: string | null; public_whatsapp: string | null; latitude: number | null; longitude: number | null; main_image_url: string | null; website_url: string | null; instagram_url: string | null; tiktok_url: string | null; twitter_url: string | null; facebook_url: string | null; youtube_url: string | null; linkedin_url: string | null; allow_verified_reviews: boolean | null; show_satisfaction_score: boolean | null; show_review_tags: boolean | null; allow_maker_review_reply: boolean | null; show_past_reviews: boolean | null; }; services: Array>; showcases: Array>; locations: Array>; works: Array>; inquiries: Array>; reviews: Array>; checklist: { checklist: Record; ready: boolean; subscriptionStatus: string; }; }; function toText(value: unknown, fallback = "") { return typeof value === "string" && value ? value : fallback; } function toNumber(value: unknown, fallback = 0) { return typeof value === "number" ? value : Number(value) || fallback; } function uniqueList(values: string[]) { return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean))); } function cleanShowcaseDescription(value: string) { return value .split(/\n\s*\n/) .filter((block) => !block.trim().toLowerCase().startsWith("incluye:")) .join("\n\n") .trim(); } function locationPinStyle(location: Record, index: number) { const lat = toNumber(location.latitude, -31.42); const lng = toNumber(location.longitude, -64.18); const left = Math.max(16, Math.min(84, 50 + ((lng + 64.18) * 220) + (index % 3 - 1) * 9)); const top = Math.max(18, Math.min(82, 50 - ((lat + 31.42) * 220) + (Math.floor(index / 3) - 0.5) * 9)); return { left: `${left}%`, top: `${top}%` }; } const sectionCopy: Record = { summary: { eyebrow: "Mi espacio maker", title: "Dashboard operativo y prioridades reales", description: "Resumen, rendimiento y tareas clave antes que metricas vacias." }, showcases: { eyebrow: "Escaparates", title: "Colecciones editoriales de tus trabajos", description: "Agrupan evidencia real para especializar mejor tu perfil y guiar consultas." }, stats: { eyebrow: "Estadisticas", title: "Rendimiento util, no vanidad", description: "Visitas, consultas y conversion resumidas en una lectura accionable." }, locations: { eyebrow: "Ubicaciones", title: "Cobertura, privacidad y alcance", description: "Define desde donde operas y como te descubren clientes locales o con envio." }, profile: { eyebrow: "Perfil publico", title: "Identidad y confianza", description: "Configura como te ven los clientes en el perfil publico y resultados." }, services: { eyebrow: "Servicios y precios", title: "Capacidades visibles y bien acotadas", description: "Cada servicio debe ayudar a filtrar mejor y a recibir consultas de mas calidad." }, works: { eyebrow: "Trabajos", title: "Portafolio publico con contexto", description: "Publica resultados reales para demostrar calidad y disparar consultas parecidas." }, inbox: { eyebrow: "Consultas", title: "Bandeja con contexto persistente", description: "La referencia del trabajo o servicio acompana toda la conversacion." }, reviews: { eyebrow: "Resenas", title: "Reputacion basada en trabajos cerrados", description: "Las opiniones verificadas ayudan a sostener la confianza y detectar riesgos." }, subscription: { eyebrow: "Plan Maker", title: "Suscripcion visible pero no invasiva", description: "La publicacion nunca depende de magia: depende de requisitos y estado permitido." }, settings: { eyebrow: "Cuenta", title: "Acceso, sesion y preferencias", description: "Una sola cuenta sirve para cliente y maker, con control claro de su estado." } }; const primaryLinks: Array<{ href: Route; label: string; section: AccountSection }> = [ { href: "/account", label: "Inicio", section: "summary" }, { href: "/account/inbox", label: "Consultas", section: "inbox" }, { href: "/account/works", label: "Trabajos", section: "works" } ]; const secondaryLinks: Array<{ href: Route; label: string; section: AccountSection }> = [ { href: "/account/showcases", label: "Escaparates", section: "showcases" }, { href: "/account/services", label: "Servicios", section: "services" }, { href: "/account/stats", label: "Estadisticas", section: "stats" }, { href: "/account/locations", label: "Ubicaciones", section: "locations" }, { href: "/account/profile", label: "Perfil publico", section: "profile" }, { href: "/account/reviews", label: "Resenas", section: "reviews" }, { href: "/account/subscription", label: "Suscripcion", section: "subscription" }, { href: "/account/settings", label: "Configuracion", section: "settings" } ]; const publishSteps = [ "Fotos", "Que hiciste", "Problema y uso", "Tecnicos", "Antes / Resultado", "Servicios", "Precio", "Vista previa", "Publicar" ]; const problemOptions = [ "Reproducir algo descatalogado", "Reemplazar una pieza rota", "Crear algo desde cero", "Personalizar un objeto", "Fabricar varias unidades", "Crear un prototipo" ]; const useOptions = [ "Pieza funcional", "Exterior", "Contacto con calor", "Contacto con agua", "Soporta esfuerzo", "Decoracion / hobby" ]; const publishTips = [ "Usa fotos claras y bien iluminadas", "Muestra el antes y el despues", "Describe el problema y la solucion", "Relaciona el trabajo con tus servicios", "Agrega precio orientativo si ayuda a filtrar" ]; const subscriptionChecklistLabels: Record = { hasBusinessName: { label: "Nombre y descripcion", detail: "Identidad publica basica completada." }, hasDescription: { label: "Descripcion clara", detail: "Explica que haces y para quien." }, hasLocation: { label: "Ubicacion configurada", detail: "Cobertura y privacidad definidas." }, hasService: { label: "Servicios configurados", detail: "Capacidades y precios orientativos." }, hasWork: { label: "Trabajos publicados", detail: "Portafolio con evidencia real." }, hasContact: { label: "Contacto publico", detail: "Email, WhatsApp o canal operativo." }, hasSubscription: { label: "Plan activo", detail: "Permite aparecer en mapa y busquedas." } }; const planBenefits = [ "Perfil profesional completo", "Aparicion en mapa y busquedas", "Servicios y precios ilimitados", "Hasta 3 escaparates publicos", "Trabajos publicados ilimitados", "Consultas y mensajeria", "Resenas verificadas", "Multiples ubicaciones", "Soporte por email" ]; const subscriptionAddOns = [ { title: "Escaparates adicionales", detail: "Mas colecciones para verticales o zonas.", state: "Futuro" }, { title: "Analitica avanzada", detail: "Origen de consultas, embudo y conversion.", state: "Futuro" }, { title: "Herramientas de negocio", detail: "Presupuestos, plantillas y seguimiento.", state: "Futuro" } ]; const supportCards = [ { title: "Preguntas frecuentes", detail: "Resuelve dudas comunes del plan.", action: "Ver FAQ" }, { title: "Contactar soporte", detail: "Escribenos si algo bloquea tu publicacion.", action: "Enviar mensaje" }, { title: "Centro de ayuda", detail: "Guias y tutoriales paso a paso.", action: "Ir al centro" }, { title: "Estado del servicio", detail: "Estado tecnico de la plataforma.", action: "Ver estado" } ]; const subscriptionBillingSteps = ["Facturacion", "Pago", "Validacion"]; const accountQuickAccess = [ ["Datos personales", "Nombre, email, telefono y avatar."], ["Seguridad y acceso", "Contrasena, 2FA y sesiones activas."], ["Privacidad y ubicacion", "Permisos, historial y personalizacion."], ["Notificaciones", "Canales, frecuencia y alertas."], ["Preferencias", "Tema, idioma, moneda y accesibilidad."], ["Cuentas vinculadas", "Google, Apple y redes publicas."] ] as const; const linkedAccounts = [ ["Google", "Conectado", "Desconectar"], ["Apple", "No conectado", "Conectar"], ["Instagram", "Perfil maker", "Gestionar"], ["Sitio web", "makerlab3d.com", "Editar"], ["YouTube", "No conectado", "Conectar"], ["TikTok", "No conectado", "Conectar"] ] as const; const privacyCenterItems = [ ["Tu ubicacion", "Gestiona permisos y ubicaciones guardadas."], ["Tus datos", "Descarga o elimina informacion generada."], ["Personalizacion", "Controla recomendaciones y busquedas."], ["Comunicaciones", "Email, mensajes y marketing."], ["Usuarios bloqueados", "Personas que no pueden contactarte."], ["Cookies y seguimiento", "Gestiona tecnologias de medicion."], ["Privacidad del perfil maker", "Que informacion muestras publicamente."] ] as const; const notificationChannelOptions = ["App", "Email", "Push", "WhatsApp"] as const; const serviceSteps = [ "Tipo", "Configuracion", "Capacidades", "Usos", "Precio", "Entrega", "Visibilidad", "Vista previa" ]; const serviceTemplates = [ { title: "Impresion 3D FDM", category: "Impresion FDM", description: "Piezas funcionales, prototipos y repuestos resistentes para uso real.", icon: "FDM" }, { title: "Impresion 3D Resina", category: "Impresion Resina", description: "Alta resolucion para figuras, miniaturas y piezas con mucho detalle.", icon: "RES" }, { title: "Diseno 3D / CAD", category: "Diseno 3D", description: "Modelado, reparacion de archivos y preparacion tecnica para fabricar.", icon: "CAD" }, { title: "Escaneo 3D", category: "Escaneo 3D", description: "Digitalizacion de objetos fisicos para replica, ajuste o rediseño.", icon: "3D" }, { title: "Produccion en serie", category: "Produccion", description: "Fabricacion de lotes chicos y medianos con control de repetibilidad.", icon: "SER" }, { title: "Postprocesado", category: "Acabados", description: "Lijado, pintura, armado y terminaciones para piezas listas para entregar.", icon: "FIN" } ]; const locationSteps = ["Tipo", "Direccion", "Privacidad", "Atencion", "Cobertura", "Horarios", "Vista previa", "Guardar"]; const locationTypeOptions = [ ["public_workshop", "Taller abierto al publico", "Atencion con cita previa y punto de referencia visible."], ["private_workshop", "Taller con cita previa", "Zona aproximada recomendada para cuidar privacidad."], ["home_workshop", "Trabajo desde casa", "No mostramos direccion exacta publicamente."], ["pickup_point", "Punto de recogida", "Solo entregas/retiros coordinados."], ["mobile_base", "Base movil", "Te desplazas o haces entregas desde una zona base."], ["online", "Atencion online", "Consultas remotas y envio sin punto fisico visible."] ]; const privacyOptions = [ ["approximate", "Zona aproximada", "Recomendado: muestra un radio sin revelar la direccion real."], ["city", "Solo localidad", "Muestra ciudad/provincia y cobertura, sin punto en el mapa."], ["exact", "Direccion exacta", "Solo si es un local o punto abierto al publico."] ]; const travelOptions = [ ["none", "No me desplazo"], ["10", "Hasta 10 km"], ["25", "Hasta 25 km"], ["50", "Hasta 50 km"], ["custom", "Zona personalizada"] ]; const shippingOptions = [ ["none", "No realizo envios"], ["province", "Dentro de mi provincia"], ["nationwide", "A toda Argentina"], ["custom", "Zonas seleccionadas"] ]; const serviceUseOptions = [ "Repuestos y piezas funcionales", "Prototipos", "Decoracion", "Herramientas / accesorios", "Educacion / hobby", "Miniaturas" ]; const serviceConditionOptions = [ "Interior", "Exterior", "Contacto con agua", "Calor moderado", "Alto esfuerzo", "Uso alimentario bajo revision" ]; const showcaseSteps = [ "Datos", "Trabajos", "Portada", "Orden", "Vista previa", "Publicar" ]; const showcaseTypeOptions = [ "Repuestos", "Prototipos", "Funcionales", "Decorativos", "Automotor", "Restauraciones" ]; function emptyWorkForm() { return { title: "", summary: "", story: "", technology: "", material: "", imageUrl: "", galleryUrls: "", problem: "", useCase: "", machine: "", color: "", dimensions: "", fabricationTime: "", quantity: "", includeBeforeAfter: false, beforeImageUrl: "", afterImageUrl: "", serviceId: "", placement: "", priceMode: "", priceFrom: "", priceTo: "" }; } function emptyShowcaseForm() { return { title: "", description: "", types: [] as string[], coverMode: "auto", coverImageUrl: "", selectedWorkIds: [] as string[], featuredWorkId: "", visibility: "published" }; } function emptyServiceForm() { return { title: "", category: "", description: "", technologies: [] as string[], materials: [] as string[], maxX: "", maxY: "", maxZ: "", resolution: "", colors: [] as string[], finishes: [] as string[], uses: [] as string[], conditions: [] as string[], urgency: "yes", priceMode: "from", priceFrom: "", smallPrice: "", mediumPrice: "", largePrice: "", leadTimeDays: "4", localPickup: true, nationwideShipping: true, coverageArea: "Todo el pais", visibility: "published", pauseMessage: "", relatedWorks: [] as string[] }; } function emptyLocationForm() { return { label: "Taller principal", type: "private_workshop", address: "", postalCode: "", province: "Cordoba", city: "Cordoba", latitude: "-31.4201", longitude: "-64.1888", privacy: "approximate", localPickup: true, personalDelivery: true, postalShipping: true, nationwideShipping: true, mobileService: false, onsiteService: false, notes: "", travelRadius: "25", shippingArea: "nationwide", hoursMode: "appointment", alertService: "Impresion 3D en resina", alertRadius: "30", alertEmail: "", minimumRating: "4", isPublished: true }; } function emptySubscriptionBillingForm() { return { invoiceType: "person", name: "Juan Perez", taxId: "20-12345678-9", email: "", address: "Av. Colon 1234, Cordoba", paymentMethod: "visa", cardLast4: "4242", validationCode: "", acceptedValidation: false }; } export default function AccountClient({ section = "summary", conversationId }: AccountClientProps) { const [user, setUser] = useState<{ id: string; email: string; role: string } | null>(null); const [authLoading, setAuthLoading] = useState(true); const [account, setAccount] = useState(null); const [conversations, setConversations] = useState>>([]); const [conversationDetail, setConversationDetail] = useState<{ inquiry: Record; messages: Array> } | null>(null); const [status, setStatus] = useState(""); const [workStep, setWorkStep] = useState(0); const [serviceStep, setServiceStep] = useState(0); const [showcaseStep, setShowcaseStep] = useState(0); const [locationStep, setLocationStep] = useState(0); const [menuOpen, setMenuOpen] = useState(false); const [workListFilter, setWorkListFilter] = useState<"published" | "drafts">("published"); const [serviceListFilter, setServiceListFilter] = useState<"all" | "active" | "drafts">("all"); const [showcaseListFilter, setShowcaseListFilter] = useState<"all" | "published" | "drafts">("all"); const [subscriptionCycle, setSubscriptionCycle] = useState<"monthly" | "annual">("monthly"); const [subscriptionScenario, setSubscriptionScenario] = useState<"active" | "grace" | "suspending" | "inactive">("active"); const [subscriptionBillingOpen, setSubscriptionBillingOpen] = useState(false); const [subscriptionBillingStep, setSubscriptionBillingStep] = useState(0); const [editingWorkId, setEditingWorkId] = useState(null); const [editingServiceId, setEditingServiceId] = useState(null); const [editingShowcaseId, setEditingShowcaseId] = useState(null); const [editingLocationId, setEditingLocationId] = useState(null); const [workWizardOpen, setWorkWizardOpen] = useState(false); const [serviceWizardOpen, setServiceWizardOpen] = useState(false); const [showcaseWizardOpen, setShowcaseWizardOpen] = useState(false); const [locationWizardOpen, setLocationWizardOpen] = useState(false); const [toastMessage, setToastMessage] = useState(""); const [profileForm, setProfileForm] = useState({ businessName: "", description: "", province: "CABA", city: "Buenos Aires", latitude: "-34.6037", longitude: "-58.3816", deliveryScope: "nationwide", availability: "available", publicContactEmail: "", publicWhatsapp: "", mainImageUrl: "/demo/maker-hero-custom.svg", websiteUrl: "", instagramUrl: "", tiktokUrl: "", twitterUrl: "", facebookUrl: "", youtubeUrl: "", linkedinUrl: "" }); const [reviewSettings, setReviewSettings] = useState({ allowVerifiedReviews: true, showSatisfactionScore: true, showReviewTags: true, allowMakerReviewReply: true, showPastReviews: true }); const [serviceForm, setServiceForm] = useState(emptyServiceForm()); const [showcaseForm, setShowcaseForm] = useState(emptyShowcaseForm()); const [locationForm, setLocationForm] = useState(emptyLocationForm()); const [subscriptionBillingForm, setSubscriptionBillingForm] = useState(emptySubscriptionBillingForm()); const [personalAccountForm, setPersonalAccountForm] = useState({ firstName: "Juan", lastName: "Perez", publicName: "Juan P.", phone: "+54 9 351 123 4567", avatarUrl: "/demo/maker-hero-custom.svg" }); const [notificationPrefs, setNotificationPrefs] = useState({ newMessage: true, messageReply: true, makerInquiry: true, acceptedInquiry: true, closedInquiry: false, customerWaiting: true, workUpdates: true, discoveryAlerts: true, reviews: true, security: true, promotions: false }); const [privacyPrefs, setPrivacyPrefs] = useState({ useLocation: true, searchHistory: true, personalizedResults: true, profileVisible: true }); const [appearancePrefs, setAppearancePrefs] = useState({ theme: "dark", textSize: "medium", reduceMotion: false, highContrast: false, language: "es-AR", currency: "ARS" }); const [blockedUsers, setBlockedUsers] = useState([ { id: "maria-g", name: "Maria G.", date: "12/05/2026" }, { id: "usuario-123", name: "Usuario123", date: "03/04/2026" }, { id: "carlos-r", name: "Carlos R.", date: "15/03/2026" } ]); const [workForm, setWorkForm] = useState(emptyWorkForm()); useEffect(() => { void loadAll(); }, []); useEffect(() => { if (section !== "inbox" || !conversationId || !user) { return; } void openConversation(conversationId); }, [conversationId, section, user]); async function loadAll() { try { const me = await browserFetch<{ user: { id: string; email: string; role: string } | null }>("/auth/me"); setUser(me.user); if (!me.user) { return; } if (me.user.role === "admin") { window.location.replace("/app/backoffice"); return; } const [accountData, conversationsData] = await Promise.all([ browserFetch("/account/maker"), browserFetch<{ conversations: Array> }>("/account/conversations") ]); setAccount(accountData); setConversations(conversationsData.conversations); setProfileForm({ businessName: accountData.maker.business_name || "", description: accountData.maker.description || "", province: accountData.maker.province || "CABA", city: accountData.maker.city || "Buenos Aires", latitude: String(accountData.maker.latitude || -34.6037), longitude: String(accountData.maker.longitude || -58.3816), deliveryScope: accountData.maker.delivery_scope || "nationwide", availability: accountData.maker.availability || "available", publicContactEmail: accountData.maker.public_contact_email || "", publicWhatsapp: accountData.maker.public_whatsapp || "", mainImageUrl: accountData.maker.main_image_url || "/demo/maker-hero-custom.svg", websiteUrl: accountData.maker.website_url || "", instagramUrl: accountData.maker.instagram_url || "", tiktokUrl: accountData.maker.tiktok_url || "", twitterUrl: accountData.maker.twitter_url || "", facebookUrl: accountData.maker.facebook_url || "", youtubeUrl: accountData.maker.youtube_url || "", linkedinUrl: accountData.maker.linkedin_url || "" }); setReviewSettings({ allowVerifiedReviews: accountData.maker.allow_verified_reviews !== false, showSatisfactionScore: accountData.maker.show_satisfaction_score !== false, showReviewTags: accountData.maker.show_review_tags !== false, allowMakerReviewReply: accountData.maker.allow_maker_review_reply !== false, showPastReviews: accountData.maker.show_past_reviews !== false }); setLocationForm((current) => ({ ...current, label: accountData.maker.business_name ? `Taller ${accountData.maker.business_name}` : current.label, province: accountData.maker.province || current.province, city: accountData.maker.city || current.city, latitude: String(accountData.maker.latitude || current.latitude), longitude: String(accountData.maker.longitude || current.longitude), nationwideShipping: (accountData.maker.delivery_scope || "nationwide") === "nationwide", postalShipping: (accountData.maker.delivery_scope || "nationwide") === "nationwide", shippingArea: (accountData.maker.delivery_scope || "nationwide") === "nationwide" ? "nationwide" : "province", alertEmail: accountData.maker.public_contact_email || current.alertEmail })); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo cargar la cuenta"); } finally { setAuthLoading(false); } } const checklistItems = useMemo(() => { const entries = account?.checklist?.checklist ? Object.entries(account.checklist.checklist) : []; return entries; }, [account]); const reviewAverage = useMemo(() => { if (!account?.reviews?.length) { return 0; } const total = account.reviews.reduce((sum, review) => sum + toNumber(review.rating_overall, 0), 0); return total / account.reviews.length; }, [account?.reviews]); const reviewRows = account?.reviews || []; const publishedReviews = reviewRows.filter((review) => toText(review.status, "published") === "published"); const reportedReviews = reviewRows.filter((review) => toText(review.status) === "reported"); const completedConversations = conversations.filter((item) => toText(item.status) === "completed").length; const reviewDistribution = [5, 4, 3, 2, 1].map((rating) => { const count = reviewRows.filter((review) => Math.round(toNumber(review.rating_overall, 0)) === rating).length; return { rating, count, percent: reviewRows.length ? Math.round((count / reviewRows.length) * 100) : 0 }; }); const aspectScores = [ ["Calidad", reviewRows.length ? reviewRows.reduce((sum, review) => sum + toNumber(review.rating_quality, 0), 0) / reviewRows.length : 0], ["Comunicacion", reviewRows.length ? reviewRows.reduce((sum, review) => sum + toNumber(review.rating_communication, 0), 0) / reviewRows.length : 0], ["Calidad/precio", reviewRows.length ? reviewRows.reduce((sum, review) => sum + toNumber(review.rating_value, 0), 0) / reviewRows.length : 0], ["Cumplimiento", reviewAverage || 0] ] as const; const reputationBadges = [ ["Trabajo verificado", "Basado en consultas finalizadas dentro de la plataforma."], ["Problema resuelto", "Las incidencias pueden cerrarse con evidencia y respuesta."], ["Cliente recurrente", "Se destaca cuando hay confianza repetida."], ["Respuesta rapida", "Mejora cuando el maker responde en menos de 1 hora."] ] as const; const planStatus = account?.checklist.subscriptionStatus || "inactive"; const isSubscriptionActive = planStatus === "active"; const checklistDoneCount = checklistItems.filter(([, value]) => Boolean(value)).length; const checklistTotal = Math.max(checklistItems.length, 1); const readinessPercent = Math.round((checklistDoneCount / checklistTotal) * 100); const subscriptionPrice = subscriptionCycle === "monthly" ? "19.990" : "199.900"; const subscriptionPeriod = subscriptionCycle === "monthly" ? "mes" : "ano"; const publicProfileHref = `/makers/${account?.maker.slug || "makerlab-3d"}` as Route; const billingEvents = [ { date: "10 jul, 2026", event: "Renovacion", amount: "$19.990", status: "Pagado" }, { date: "9 jul, 2026", event: "Intento de cobro", amount: "$19.990", status: "Fallido" }, { date: "10 jun, 2026", event: "Renovacion", amount: "$19.990", status: "Pagado" } ]; const accountActivityRows = [ ["Consultas", String(conversations.length)], ["Favoritos", "12"], ["Alertas activas", "2"], ["Resenas publicadas", String(account?.reviews.length || 0)], ["Mensajes", String(conversations.length)] ] as const; const notificationRows = [ ["newMessage", "Nuevo mensaje", "Mensajes y consultas"], ["messageReply", "Respuesta a mi mensaje", "Conversaciones"], ["makerInquiry", "Nueva consulta recibida", "Perfil maker"], ["acceptedInquiry", "Consulta aceptada", "Trabajos"], ["closedInquiry", "Consulta cerrada", "Historial"], ["customerWaiting", "Cliente espera respuesta", "Prioridad"] ] as const; const notificationSummaryRows = [ ["Mensajes y consultas", notificationPrefs.newMessage || notificationPrefs.messageReply], ["Trabajos", notificationPrefs.workUpdates], ["Alertas y descubrimiento", notificationPrefs.discoveryAlerts], ["Resenas y reputacion", notificationPrefs.reviews], ["Cuenta y seguridad", notificationPrefs.security], ["Novedades y promociones", notificationPrefs.promotions] ] as const; const savedAlertRows = [ { title: "Resina cerca de casa", detail: "25 km | Cordoba Capital", status: "Activa" }, { title: "Repuestos con envio a Mendoza", detail: "Envio incluido | Mendoza", status: "Activa" }, { title: "Impresion SLA profesional", detail: "30 km | Villa Allende", status: "Pausada" } ]; const activeSessionRows = [ { device: "Chrome - Windows", place: "Cordoba, Argentina", status: "Actual" }, { device: "Safari - iPhone", place: "Cordoba, Argentina", status: "Hace 2 dias" }, { device: "Chrome - Android", place: "Villa Allende, Argentina", status: "Hace 5 dias" } ]; const profileDiagnosticRows = [ ["Logo", Boolean(profileForm.mainImageUrl)], ["Descripcion", Boolean(profileForm.description.trim())], ["Servicios", Boolean(account?.services?.some((service) => Boolean(service.is_published)))], ["Ubicacion", Boolean(account?.locations?.some((location) => Boolean(location.is_published)))], ["Trabajos", Boolean(account?.works?.some((work) => Boolean(work.is_published)))], ["Escaparates", Boolean(account?.showcases?.some((showcase) => Boolean(showcase.is_published)))], ["Redes sociales", Boolean(profileForm.websiteUrl || profileForm.instagramUrl || profileForm.tiktokUrl)] ] as const; const profileDiagnosticPercent = Math.round((profileDiagnosticRows.filter(([, value]) => value).length / profileDiagnosticRows.length) * 100); const accountStateRows = [ ["Cuenta", "Activa"], ["Email", "Verificado"], ["Seguridad", appearancePrefs.highContrast ? "Alta" : "Media"], ["Notificaciones", "Personalizadas"], ["Mi espacio Maker", account?.maker.status === "published" ? "Activo" : "Borrador"], ["Plan Maker", planStatus === "active" ? "Activo" : "Pendiente"] ] as const; async function saveProfile(event: React.FormEvent) { event.preventDefault(); setStatus("Guardando perfil..."); try { await browserFetch("/account/maker/profile", { method: "PUT", body: JSON.stringify({ ...profileForm, latitude: Number(profileForm.latitude), longitude: Number(profileForm.longitude) }) }); setStatus("Perfil guardado."); await loadAll(); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo guardar el perfil"); } } async function toggleReviewSetting(key: keyof typeof reviewSettings) { const nextSettings = { ...reviewSettings, [key]: !reviewSettings[key] }; const previousSettings = reviewSettings; setReviewSettings(nextSettings); setStatus("Guardando configuracion de resenas..."); try { await browserFetch("/account/maker/review-settings", { method: "PATCH", body: JSON.stringify(nextSettings) }); setStatus("Configuracion de resenas guardada."); await loadAll(); } catch (error) { setReviewSettings(previousSettings); setStatus(error instanceof Error ? error.message : "No se pudo guardar la configuracion."); } } function updateLocationForm(field: keyof typeof locationForm, value: string | boolean) { setLocationForm((current) => ({ ...current, [field]: value })); } function locationFormFromProfile() { return { ...emptyLocationForm(), label: profileForm.businessName ? `Taller ${profileForm.businessName}` : "Taller principal", province: profileForm.province || "Cordoba", city: profileForm.city || "Cordoba", latitude: profileForm.latitude || "-31.4201", longitude: profileForm.longitude || "-64.1888", nationwideShipping: profileForm.deliveryScope === "nationwide", postalShipping: profileForm.deliveryScope === "nationwide", shippingArea: profileForm.deliveryScope === "nationwide" ? "nationwide" : "province", alertEmail: profileForm.publicContactEmail }; } function locationFormFromLocation(location: Record) { const draftData = location.draft_data && typeof location.draft_data === "object" && !Array.isArray(location.draft_data) ? location.draft_data as Partial> : {}; return { ...emptyLocationForm(), ...draftData, label: toText(location.label, toText(draftData.label, "Taller principal")), type: toText(location.location_type, toText(draftData.type, "private_workshop")), address: toText(location.address, toText(draftData.address)), postalCode: toText(location.postal_code, toText(draftData.postalCode)), province: toText(location.province, toText(draftData.province, "Cordoba")), city: toText(location.city, toText(draftData.city, "Cordoba")), latitude: String(location.latitude ?? draftData.latitude ?? "-31.4201"), longitude: String(location.longitude ?? draftData.longitude ?? "-64.1888"), privacy: toText(location.privacy, toText(draftData.privacy, "approximate")), localPickup: Boolean(location.local_pickup ?? draftData.localPickup ?? true), personalDelivery: Boolean(location.personal_delivery ?? draftData.personalDelivery ?? true), postalShipping: Boolean(location.postal_shipping ?? draftData.postalShipping ?? true), nationwideShipping: Boolean(location.nationwide_shipping ?? draftData.nationwideShipping ?? false), mobileService: Boolean(location.mobile_service ?? draftData.mobileService ?? false), onsiteService: Boolean(location.onsite_service ?? draftData.onsiteService ?? false), notes: toText(location.notes, toText(draftData.notes)), travelRadius: toText(location.travel_radius, toText(draftData.travelRadius, "25")), shippingArea: toText(location.shipping_area, toText(draftData.shippingArea, "nationwide")), hoursMode: toText(location.hours_mode, toText(draftData.hoursMode, "appointment")), alertService: toText(location.alert_service, toText(draftData.alertService)), alertRadius: toText(location.alert_radius, toText(draftData.alertRadius, "30")), minimumRating: toText(location.minimum_rating, toText(draftData.minimumRating, "4")), isPublished: Boolean(location.is_published) }; } function locationPayload(isPublished: boolean) { const deliveryScope = locationForm.nationwideShipping || locationForm.shippingArea === "nationwide" ? "nationwide" : "local"; const draftData = { ...locationForm, isPublished }; return { ...locationForm, latitude: Number(locationForm.latitude || profileForm.latitude || -31.4201), longitude: Number(locationForm.longitude || profileForm.longitude || -64.1888), nationwideShipping: deliveryScope === "nationwide", postalShipping: locationForm.postalShipping || deliveryScope === "nationwide", draftData, isPublished }; } function startNewLocation() { setEditingLocationId(null); setLocationForm(locationFormFromProfile()); setLocationStep(0); setLocationWizardOpen(true); setStatus(""); } function editLocation(location: Record) { setEditingLocationId(String(location.id)); setLocationForm(locationFormFromLocation(location)); setLocationStep(0); setLocationWizardOpen(true); setStatus("Ubicacion cargada para editar."); } function closeLocationWizard() { setLocationWizardOpen(false); } function goToNextLocationStep() { if (locationStep === 0 && !locationForm.type) { showToast("Elige el tipo de ubicacion."); return; } if (locationStep === 1 && (!locationForm.city.trim() || !locationForm.province.trim())) { showToast("Completa ciudad y provincia."); return; } setLocationStep((current) => Math.min(current + 1, locationSteps.length - 1)); } async function saveLocation(shouldClose = false) { setStatus(shouldClose ? "Guardando ubicacion..." : "Guardando borrador de ubicacion..."); const isPublished = shouldClose; const payload = locationPayload(isPublished); try { const response = await browserFetch<{ location: Record }>( editingLocationId ? `/account/maker/locations/${editingLocationId}` : "/account/maker/locations", { method: editingLocationId ? "PATCH" : "POST", body: JSON.stringify(payload) } ); setEditingLocationId(String(response.location.id)); if (isPublished) { const nextProfileForm = { ...profileForm, businessName: profileForm.businessName || account?.maker.business_name || "Maker 3D", description: profileForm.description || account?.maker.description || "Maker con cobertura configurada para recibir consultas y coordinar trabajos de impresion 3D.", province: locationForm.province || profileForm.province, city: locationForm.city || profileForm.city, latitude: String(Number(locationForm.latitude || profileForm.latitude || -31.4201)), longitude: String(Number(locationForm.longitude || profileForm.longitude || -64.1888)), deliveryScope: payload.nationwideShipping ? "nationwide" : "local", publicContactEmail: profileForm.publicContactEmail || locationForm.alertEmail }; await browserFetch("/account/maker/profile", { method: "PUT", body: JSON.stringify({ ...nextProfileForm, latitude: Number(nextProfileForm.latitude), longitude: Number(nextProfileForm.longitude) }) }); setProfileForm(nextProfileForm); } setStatus(isPublished ? "Ubicacion guardada." : "Borrador de ubicacion guardado."); showToast(isPublished ? "Ubicacion guardada." : "Borrador guardado."); await loadAll(); if (isPublished) { setEditingLocationId(null); setLocationWizardOpen(false); } } catch (error) { const message = error instanceof Error ? error.message : "No se pudo guardar la ubicacion"; setStatus(message); showToast(message); } } async function deleteLocation(location: Record) { setStatus("Eliminando ubicacion..."); try { await browserFetch(`/account/maker/locations/${String(location.id)}`, { method: "DELETE" }); setStatus("Ubicacion eliminada."); await loadAll(); } catch (error) { const message = error instanceof Error ? error.message : "No se pudo eliminar la ubicacion"; setStatus(message); showToast(message); } } async function createService(event: React.FormEvent) { event.preventDefault(); await saveService(true); } async function createWork(event: React.FormEvent) { event.preventDefault(); await saveWork(true); } function showToast(message: string) { setToastMessage(message); window.setTimeout(() => { setToastMessage((current) => (current === message ? "" : current)); }, 3200); } function updateServiceForm(field: keyof typeof serviceForm, value: string | boolean | string[]) { setServiceForm((current) => ({ ...current, [field]: value })); } function toggleServiceListField(field: "technologies" | "materials" | "colors" | "finishes" | "uses" | "conditions" | "relatedWorks", value: string) { setServiceForm((current) => { const values = current[field]; return { ...current, [field]: values.includes(value) ? values.filter((item) => item !== value) : [...values, value] }; }); } function applyServiceTemplate(template: (typeof serviceTemplates)[number]) { setServiceForm((current) => ({ ...current, title: template.title, category: template.category, description: template.description, technologies: template.category.includes("Resina") ? ["Resina"] : template.category.includes("Diseno") ? ["CAD"] : ["FDM"], materials: template.category.includes("Resina") ? ["Resina standard"] : ["PLA", "PETG"], uses: template.category.includes("Diseno") ? ["Prototipos"] : ["Repuestos y piezas funcionales", "Prototipos"] })); setServiceStep(1); } function servicePayload(isPublished: boolean) { const price = serviceForm.priceMode === "hidden" ? null : Number(serviceForm.priceFrom || serviceForm.smallPrice || 0) * 100; return { title: serviceForm.title, category: serviceForm.category, description: [ serviceForm.description, serviceForm.maxX || serviceForm.maxY || serviceForm.maxZ ? `Volumen maximo: X ${serviceForm.maxX || "-"} mm, Y ${serviceForm.maxY || "-"} mm, Z ${serviceForm.maxZ || "-"} mm.` : "", serviceForm.uses.length ? `Ideal para: ${serviceForm.uses.join(", ")}.` : "", serviceForm.conditions.length ? `Condiciones: ${serviceForm.conditions.join(", ")}.` : "", serviceForm.priceMode === "range" ? `Rangos: chico $${serviceForm.smallPrice || "-"}, mediano $${serviceForm.mediumPrice || "-"}, grande $${serviceForm.largePrice || "-"}.` : "", serviceForm.pauseMessage && serviceForm.visibility === "paused" ? `Mensaje de pausa: ${serviceForm.pauseMessage}` : "" ].filter(Boolean).join("\n\n"), technologies: serviceForm.technologies, materials: serviceForm.materials, localPickup: serviceForm.localPickup, nationwideShipping: serviceForm.nationwideShipping, leadTimeDays: Number(serviceForm.leadTimeDays || 4), priceFromCents: price, draftData: serviceForm, isPublished }; } async function saveService(isPublished: boolean) { const wantsPublish = isPublished && serviceForm.visibility !== "draft"; setStatus(wantsPublish ? "Publicando servicio..." : "Guardando servicio..."); try { const response = await browserFetch<{ service: Record }>( editingServiceId ? `/account/maker/services/${editingServiceId}` : "/account/maker/services", { method: editingServiceId ? "PATCH" : "POST", body: JSON.stringify(servicePayload(wantsPublish && serviceForm.visibility !== "paused")) } ); setEditingServiceId(String(response.service.id)); if (wantsPublish && serviceForm.visibility !== "paused") { setServiceForm(emptyServiceForm()); setEditingServiceId(null); setServiceWizardOpen(false); setStatus("Servicio publicado."); } else { setStatus(serviceForm.visibility === "paused" ? "Servicio pausado." : "Borrador de servicio guardado."); } await loadAll(); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo guardar el servicio"); } } function startNewService() { setEditingServiceId(null); setServiceForm(emptyServiceForm()); setServiceStep(0); setServiceWizardOpen(true); setStatus(""); } function closeServiceWizard() { setServiceWizardOpen(false); } function serviceFormFromService(service: Record) { const draftData = service.draft_data && typeof service.draft_data === "object" && !Array.isArray(service.draft_data) ? service.draft_data as Partial> : {}; return { ...emptyServiceForm(), ...draftData, title: !Boolean(service.is_published) && toText(service.title) === "Servicio en borrador" ? "" : toText(service.title), category: !Boolean(service.is_published) && toText(service.category) === "Pendiente" ? "" : toText(service.category), description: !Boolean(service.is_published) && toText(service.description) === "Servicio pendiente de completar." ? "" : toText(service.description), technologies: Array.isArray(service.technologies) ? service.technologies.map(String) : [], materials: Array.isArray(service.materials) ? service.materials.map(String) : [], leadTimeDays: String(service.lead_time_days || 4), localPickup: Boolean(service.local_pickup), nationwideShipping: Boolean(service.nationwide_shipping), priceFrom: service.price_from_cents ? String(Math.round(toNumber(service.price_from_cents) / 100)) : "", visibility: Boolean(service.is_published) ? "published" : "draft" }; } function editService(service: Record) { setEditingServiceId(String(service.id)); setServiceForm(serviceFormFromService(service)); setServiceStep(1); setServiceWizardOpen(true); setStatus("Servicio cargado para editar."); } async function pauseService(service: Record) { const restoredForm = { ...serviceFormFromService(service), visibility: "paused" }; setStatus("Pausando servicio..."); try { await browserFetch(`/account/maker/services/${String(service.id)}`, { method: "PATCH", body: JSON.stringify({ ...servicePayload(false), ...{ title: restoredForm.title, category: restoredForm.category, description: restoredForm.description, technologies: restoredForm.technologies, materials: restoredForm.materials, localPickup: restoredForm.localPickup, nationwideShipping: restoredForm.nationwideShipping, leadTimeDays: Number(restoredForm.leadTimeDays || 4), priceFromCents: restoredForm.priceFrom ? Number(restoredForm.priceFrom) * 100 : null, draftData: restoredForm, isPublished: false } }) }); setStatus("Servicio pausado."); await loadAll(); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo pausar el servicio"); } } async function deleteService(service: Record) { if (typeof window !== "undefined" && !window.confirm("Esto eliminara el servicio. Quieres continuar?")) { return; } setStatus("Eliminando servicio..."); try { await browserFetch(`/account/maker/services/${String(service.id)}`, { method: "DELETE" }); setStatus("Servicio eliminado."); await loadAll(); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo eliminar el servicio"); } } function updateShowcaseForm(field: keyof typeof showcaseForm, value: string | string[]) { setShowcaseForm((current) => ({ ...current, [field]: value })); } function toggleShowcaseType(value: string) { setShowcaseForm((current) => ({ ...current, types: current.types.includes(value) ? current.types.filter((item) => item !== value) : [...current.types, value] })); } function toggleShowcaseWork(workId: string) { setShowcaseForm((current) => { const selectedWorkIds = current.selectedWorkIds.includes(workId) ? current.selectedWorkIds.filter((id) => id !== workId) : [...current.selectedWorkIds, workId]; return { ...current, selectedWorkIds, featuredWorkId: selectedWorkIds.includes(current.featuredWorkId) ? current.featuredWorkId : selectedWorkIds[0] || "", coverImageUrl: selectedWorkIds.length ? current.coverImageUrl : "" }; }); } function moveShowcaseWork(workId: string, direction: -1 | 1) { setShowcaseForm((current) => { const index = current.selectedWorkIds.indexOf(workId); const nextIndex = index + direction; if (index < 0 || nextIndex < 0 || nextIndex >= current.selectedWorkIds.length) { return current; } const selectedWorkIds = [...current.selectedWorkIds]; const [item] = selectedWorkIds.splice(index, 1); selectedWorkIds.splice(nextIndex, 0, item); return { ...current, selectedWorkIds }; }); } function chooseShowcaseCover(work: Record) { setShowcaseForm((current) => ({ ...current, coverMode: "existing", featuredWorkId: String(work.id), coverImageUrl: toText(work.image_url, "") })); } function getShowcaseSelectedWorks() { const worksById = new Map((account?.works || []).map((work) => [String(work.id), work])); return showcaseForm.selectedWorkIds.map((id) => worksById.get(id)).filter(Boolean) as Array>; } function getShowcaseCoverImage() { if (showcaseForm.coverMode === "existing" && showcaseForm.coverImageUrl) { return showcaseForm.coverImageUrl; } const featured = (account?.works || []).find((work) => String(work.id) === showcaseForm.featuredWorkId); return toText(featured?.image_url, toText(getShowcaseSelectedWorks()[0]?.image_url, "/demo/work-custom.svg")); } function showcasePayload(isPublished: boolean) { const cleanedDescription = cleanShowcaseDescription(showcaseForm.description); const selectedWorksCount = showcaseForm.selectedWorkIds.length; const publishedDescription = cleanedDescription || `${showcaseForm.title || "Escaparate"} agrupa ${selectedWorksCount} trabajos seleccionados para mostrar experiencia real del maker.`; const cleanedDraftData = { ...showcaseForm, description: cleanedDescription }; return { title: showcaseForm.title, description: isPublished ? publishedDescription : cleanedDescription, coverImageUrl: getShowcaseCoverImage(), selectedWorkIds: showcaseForm.selectedWorkIds, featuredWorkId: showcaseForm.featuredWorkId || showcaseForm.selectedWorkIds[0] || null, draftData: cleanedDraftData, isPublished }; } async function saveShowcase(isPublished: boolean) { if (isPublished && !showcaseForm.selectedWorkIds.length) { setShowcaseStep(1); showToast("Selecciona al menos un trabajo para publicar el escaparate."); return; } setStatus(isPublished ? "Publicando escaparate..." : "Guardando escaparate..."); try { const response = await browserFetch<{ showcase: Record }>( editingShowcaseId ? `/account/maker/showcases/${editingShowcaseId}` : "/account/maker/showcases", { method: editingShowcaseId ? "PATCH" : "POST", body: JSON.stringify(showcasePayload(isPublished)) } ); setEditingShowcaseId(String(response.showcase.id)); if (isPublished) { setShowcaseForm(emptyShowcaseForm()); setEditingShowcaseId(null); setShowcaseWizardOpen(false); setStatus("Escaparate publicado."); } else { setStatus("Borrador de escaparate guardado."); } await loadAll(); } catch (error) { const message = error instanceof Error ? error.message : "No se pudo guardar el escaparate"; setStatus(message); showToast(message); } } function startNewShowcase() { setEditingShowcaseId(null); setShowcaseForm(emptyShowcaseForm()); setShowcaseStep(0); setShowcaseWizardOpen(true); setStatus(""); } function closeShowcaseWizard() { setShowcaseWizardOpen(false); } function showcaseFormFromShowcase(showcase: Record) { const draftData = showcase.draft_data && typeof showcase.draft_data === "object" && !Array.isArray(showcase.draft_data) ? showcase.draft_data as Partial> : {}; const storedDescription = !Boolean(showcase.is_published) && toText(showcase.description) === "Escaparate pendiente de completar." ? "" : toText(showcase.description); return { ...emptyShowcaseForm(), ...draftData, title: !Boolean(showcase.is_published) && toText(showcase.title) === "Escaparate en borrador" ? "" : toText(showcase.title), description: cleanShowcaseDescription(storedDescription), coverImageUrl: toText(showcase.cover_image_url), selectedWorkIds: Array.isArray(showcase.selected_work_ids) ? showcase.selected_work_ids.map(String) : [], featuredWorkId: toText(showcase.featured_work_id), visibility: Boolean(showcase.is_published) ? "published" : "draft" }; } function editShowcase(showcase: Record) { setEditingShowcaseId(String(showcase.id)); setShowcaseForm(showcaseFormFromShowcase(showcase)); setShowcaseStep(0); setShowcaseWizardOpen(true); setStatus("Escaparate cargado para editar."); } async function unpublishShowcase(showcase: Record) { const restoredForm = { ...showcaseFormFromShowcase(showcase), visibility: "draft" }; setStatus("Pasando escaparate a borrador..."); try { await browserFetch(`/account/maker/showcases/${String(showcase.id)}`, { method: "PATCH", body: JSON.stringify({ title: restoredForm.title, description: restoredForm.description, coverImageUrl: restoredForm.coverImageUrl, selectedWorkIds: restoredForm.selectedWorkIds, featuredWorkId: restoredForm.featuredWorkId || restoredForm.selectedWorkIds[0] || null, draftData: restoredForm, isPublished: false }) }); setStatus("Escaparate guardado como borrador."); setShowcaseListFilter("drafts"); await loadAll(); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo despublicar el escaparate"); } } async function deleteShowcase(showcase: Record) { if (typeof window !== "undefined" && !window.confirm("Esto eliminara el escaparate. Quieres continuar?")) { return; } setStatus("Eliminando escaparate..."); try { await browserFetch(`/account/maker/showcases/${String(showcase.id)}`, { method: "DELETE" }); setStatus("Escaparate eliminado."); await loadAll(); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo eliminar el escaparate"); } } async function saveWork(isPublished: boolean) { setStatus(isPublished ? "Publicando trabajo..." : "Guardando borrador..."); try { const galleryUrls = uniqueList([ workForm.imageUrl, ...workForm.galleryUrls.split("\n").map((value) => value.trim()).filter(Boolean), ...(workForm.includeBeforeAfter ? [workForm.beforeImageUrl, workForm.afterImageUrl] : []) ]); if (!galleryUrls.length) { setWorkStep(0); const message = isPublished ? "Agrega al menos una foto para publicar." : "Agrega al menos una foto para guardar el borrador."; setStatus(""); showToast(message); return; } if (isPublished && (!workForm.title.trim() || !workForm.summary.trim() || !workForm.technology.trim() || !workForm.material.trim())) { setWorkStep(1); setStatus("Completa titulo, resumen, tecnologia y material antes de publicar."); return; } const story = [ workForm.story, workForm.problem || workForm.useCase ? `Necesidad: ${workForm.problem || "Sin especificar"}. Uso: ${workForm.useCase || "Sin especificar"}.` : "", workForm.technology || workForm.material || workForm.machine || workForm.dimensions || workForm.fabricationTime || workForm.quantity ? `Detalle tecnico: ${[workForm.technology, workForm.material, workForm.machine, workForm.dimensions, workForm.fabricationTime, workForm.quantity].filter(Boolean).join(", ")}.` : "", workForm.priceMode ? (workForm.priceMode === "hidden" ? "Precio: no visible publicamente." : `Precio orientativo: $${workForm.priceFrom || "0"} a $${workForm.priceTo || workForm.priceFrom || "0"}.`) : "" ].filter(Boolean).join("\n\n"); const response = await browserFetch<{ work: Record }>(editingWorkId ? `/account/maker/works/${editingWorkId}` : "/account/maker/works", { method: editingWorkId ? "PATCH" : "POST", body: JSON.stringify({ ...workForm, galleryUrls, story: story || workForm.summary, draftData: workForm, isPublished, serviceId: workForm.serviceId || account?.services?.[0]?.id || null }) }); if (!isPublished) { setEditingWorkId(String(response.work.id)); setStatus("Borrador guardado. Puedes seguir editandolo."); await loadAll(); return; } setEditingWorkId(null); setWorkForm(emptyWorkForm()); setWorkStep(publishSteps.length - 1); setWorkWizardOpen(false); setStatus(isPublished ? "Trabajo publicado." : "Borrador guardado."); await loadAll(); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo guardar el trabajo"); } } async function activateDemoSubscription() { setStatus("Activando plan demo..."); try { await browserFetch("/account/maker/subscription/demo-activate", { method: "POST" }); setStatus("Plan demo activado."); await loadAll(); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo activar el plan"); } } function updateSubscriptionBillingForm(field: keyof ReturnType, value: string | boolean) { setSubscriptionBillingForm((current) => ({ ...current, [field]: value })); } function openSubscriptionBillingWizard(step = 0) { setSubscriptionBillingForm((current) => ({ ...current, email: current.email || profileForm.publicContactEmail || user?.email || "" })); setSubscriptionBillingStep(step); setSubscriptionBillingOpen(true); setStatus(""); } function closeSubscriptionBillingWizard() { setSubscriptionBillingOpen(false); } function validateSubscriptionBillingStep(step: number) { if (step === 0) { if (!subscriptionBillingForm.name.trim() || !subscriptionBillingForm.taxId.trim() || !subscriptionBillingForm.email.includes("@")) { showToast("Completa nombre, documento fiscal y un email valido."); return false; } return true; } if (step === 1) { if (!/^\d{4}$/.test(subscriptionBillingForm.cardLast4.trim())) { showToast("Ingresa los ultimos 4 digitos del metodo de pago."); return false; } return true; } if (!subscriptionBillingForm.acceptedValidation || subscriptionBillingForm.validationCode.trim() !== "1234") { showToast("Acepta la validacion y usa el codigo demo 1234."); return false; } return true; } function goToNextSubscriptionBillingStep() { if (!validateSubscriptionBillingStep(subscriptionBillingStep)) { return; } setSubscriptionBillingStep((current) => Math.min(current + 1, subscriptionBillingSteps.length - 1)); } function saveSubscriptionBilling() { if (![0, 1, 2].every((step) => validateSubscriptionBillingStep(step))) { return; } setSubscriptionBillingOpen(false); setStatus("Datos de facturacion y pago validados."); } function updatePersonalAccountField(field: keyof typeof personalAccountForm, value: string) { setPersonalAccountForm((current) => ({ ...current, [field]: value })); } function toggleNotificationPref(key: keyof typeof notificationPrefs) { setNotificationPrefs((current) => ({ ...current, [key]: !current[key] })); } function togglePrivacyPref(key: keyof typeof privacyPrefs) { setPrivacyPrefs((current) => ({ ...current, [key]: !current[key] })); } function updateAppearancePref(field: keyof typeof appearancePrefs, value: string | boolean) { setAppearancePrefs((current) => ({ ...current, [field]: value })); } function saveAccountSettings() { setStatus("Configuracion de cuenta guardada."); showToast("Configuracion guardada."); } function exportAccountData() { setStatus("Exportacion demo preparada. En produccion se generara un archivo descargable."); showToast("Exportacion demo preparada."); } function requestAccountDeletion() { setStatus("Solicitud demo registrada. La eliminacion real requerira confirmacion adicional."); showToast("Solicitud de eliminacion registrada como demo."); } function unblockUser(id: string) { setBlockedUsers((current) => current.filter((blockedUser) => blockedUser.id !== id)); setStatus("Usuario desbloqueado."); } async function requestPublish() { setStatus("Revisando requisitos..."); try { await browserFetch("/account/maker/publish", { method: "POST" }); setStatus("Perfil publicado."); await loadAll(); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo publicar"); } } async function openConversation(id: string) { try { const detail = await browserFetch<{ inquiry: Record; messages: Array> }>(`/inquiries/${id}`); setConversationDetail(detail); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo abrir la conversacion"); } } async function sendReply(event: React.FormEvent) { event.preventDefault(); if (!conversationDetail) { return; } const form = new FormData(event.currentTarget); const body = String(form.get("body") || ""); if (!body.trim()) { return; } try { await browserFetch(`/inquiries/${conversationDetail.inquiry.id as string}/messages`, { method: "POST", body: JSON.stringify({ body }) }); await openConversation(conversationDetail.inquiry.id as string); event.currentTarget.reset(); setStatus("Respuesta enviada."); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo responder"); } } async function markCompleted() { if (!conversationDetail) { return; } try { await browserFetch(`/inquiries/${conversationDetail.inquiry.id as string}/complete`, { method: "POST" }); setStatus("Trabajo marcado como finalizado."); await Promise.all([loadAll(), openConversation(conversationDetail.inquiry.id as string)]); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo marcar como finalizado"); } } async function rejectInquiry() { if (!conversationDetail) { return; } try { await browserFetch(`/inquiries/${conversationDetail.inquiry.id as string}/reject`, { method: "POST", body: JSON.stringify({ reason: "No puedo tomar este trabajo en este momento." }) }); setStatus("Consulta rechazada."); await Promise.all([loadAll(), openConversation(conversationDetail.inquiry.id as string)]); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo rechazar la consulta"); } } function logout() { logoutByNavigation(); } function updateWorkForm(field: keyof typeof workForm, value: string | boolean) { setWorkForm((current) => ({ ...current, [field]: value })); } function startNewWork() { setEditingWorkId(null); setWorkForm(emptyWorkForm()); setWorkStep(0); setWorkWizardOpen(true); setStatus(""); } function closeWorkWizard() { setWorkWizardOpen(false); } function workFormFromWork(work: Record) { const isPublished = Boolean(work.is_published); const imageUrl = toText(work.image_url); const galleryUrls = Array.isArray(work.gallery_urls) ? work.gallery_urls.map((item) => String(item)) : []; const isFallbackDraftImage = !isPublished && imageUrl === "/demo/work-custom.svg"; const draftData = work.draft_data && typeof work.draft_data === "object" && !Array.isArray(work.draft_data) ? work.draft_data as Partial> : {}; return { ...emptyWorkForm(), ...draftData, title: !isPublished && toText(work.title) === "Borrador sin titulo" ? "" : toText(work.title), summary: !isPublished && toText(work.summary) === "Borrador pendiente de completar." ? "" : toText(work.summary), story: !isPublished && toText(work.story).startsWith("Borrador pendiente") ? "" : toText(work.story), technology: !isPublished && toText(work.technology) === "Pendiente" ? "" : toText(work.technology), material: !isPublished && toText(work.material) === "Pendiente" ? "" : toText(work.material), imageUrl: isFallbackDraftImage ? "" : imageUrl, galleryUrls: uniqueList(galleryUrls.filter((item) => item !== "/demo/work-custom.svg")).join("\n") }; } function resumeDraft(work: Record) { setEditingWorkId(String(work.id)); setWorkForm(workFormFromWork(work)); setWorkStep(0); setWorkWizardOpen(true); setStatus("Borrador cargado. Continua desde el paso que necesites."); } async function unpublishWork(work: Record) { setStatus("Despublicando trabajo..."); try { const restoredForm = workFormFromWork(work); await browserFetch(`/account/maker/works/${String(work.id)}`, { method: "PATCH", body: JSON.stringify({ ...restoredForm, galleryUrls: uniqueList([restoredForm.imageUrl, ...restoredForm.galleryUrls.split("\n")]), draftData: restoredForm, isPublished: false, serviceId: toText(work.service_id) || null }) }); setStatus("Trabajo despublicado y guardado como borrador."); setWorkListFilter("drafts"); await loadAll(); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo despublicar el trabajo"); } } async function deleteWork(work: Record) { if (typeof window !== "undefined" && !window.confirm("Esto eliminara el trabajo de forma permanente. Quieres continuar?")) { return; } setStatus("Eliminando trabajo..."); try { await browserFetch(`/account/maker/works/${String(work.id)}`, { method: "DELETE" }); if (editingWorkId === String(work.id)) { setEditingWorkId(null); setWorkForm(emptyWorkForm()); setWorkStep(0); setWorkWizardOpen(false); } setStatus("Trabajo eliminado."); await loadAll(); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo eliminar el trabajo"); } } function getWorkGalleryImages() { return uniqueList([workForm.imageUrl, ...workForm.galleryUrls.split("\n")]); } function setCoverImage(image: string) { setWorkForm((current) => ({ ...current, imageUrl: image, galleryUrls: uniqueList([image, ...current.galleryUrls.split("\n")]).join("\n") })); } function removeWorkImage(image: string) { setWorkForm((current) => { const galleryUrls = uniqueList(current.galleryUrls.split("\n").filter((item) => item !== image)); const nextCover = current.imageUrl === image ? (galleryUrls[0] || "") : current.imageUrl; return { ...current, imageUrl: nextCover, galleryUrls: galleryUrls.join("\n"), beforeImageUrl: current.beforeImageUrl === image ? "" : current.beforeImageUrl, afterImageUrl: current.afterImageUrl === image ? "" : current.afterImageUrl }; }); } async function compressImage(file: File) { if (!file.type.startsWith("image/")) { return file; } try { const bitmap = await createImageBitmap(file); const maxSide = 1400; const scale = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height)); const width = Math.max(1, Math.round(bitmap.width * scale)); const height = Math.max(1, Math.round(bitmap.height * scale)); const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const context = canvas.getContext("2d"); if (!context) { return file; } context.drawImage(bitmap, 0, 0, width, height); const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.82)); bitmap.close(); if (!blob) { return file; } return new File([blob], file.name.replace(/\.[^.]+$/, ".jpg"), { type: "image/jpeg" }); } catch { return file; } } async function uploadWorkImage(file: File) { const compactFile = await compressImage(file); const formData = new FormData(); formData.append("file", compactFile); const response = await browserFetch<{ url: string }>("/uploads/public-image", { method: "POST", body: formData }); return response.url; } async function addWorkFiles(files: FileList | null) { if (!files?.length) { return; } const selectedFiles = Array.from(files).filter((file) => file.type.startsWith("image/")).slice(0, 8); if (!selectedFiles.length) { setStatus("Selecciona imagenes validas."); return; } try { setStatus("Subiendo imagenes..."); const images = await Promise.all(selectedFiles.map((file) => uploadWorkImage(file))); setWorkForm((current) => { const galleryUrls = uniqueList([...images, ...current.galleryUrls.split("\n")]); return { ...current, imageUrl: current.imageUrl || images[0], galleryUrls: galleryUrls.join("\n") }; }); setStatus(`${images.length} imagen(es) agregada(s).`); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudieron subir las imagenes."); } } async function updateProfileLogo(files: FileList | null) { const file = Array.from(files || []).find((item) => item.type.startsWith("image/")); if (!file) { return; } try { setStatus("Subiendo logo..."); const imageUrl = await uploadWorkImage(file); setProfileForm((current) => ({ ...current, mainImageUrl: imageUrl })); setStatus("Logo cargado. Guarda el perfil para publicarlo."); } catch (error) { setStatus(error instanceof Error ? error.message : "No se pudo subir el logo."); } } function goToNextWorkStep() { if (workStep === 0 && !getWorkGalleryImages().length) { setStatus("Agrega al menos una foto para continuar."); return; } setWorkStep((current) => Math.min(current + 1, publishSteps.length - 1)); } function goToWorkStep(step: number) { if (step > 0 && !getWorkGalleryImages().length) { setStatus("Agrega al menos una foto para continuar."); setWorkStep(0); return; } setWorkStep(step); } function renderChoiceGrid(field: keyof typeof workForm, options: string[]) { return (
{options.map((option) => ( ))}
); } function goToNextServiceStep() { if (serviceStep === 0 && !serviceForm.title.trim()) { showToast("Elige un tipo de servicio para continuar."); return; } setServiceStep((current) => Math.min(current + 1, serviceSteps.length - 1)); } function renderServiceMultiChoice(field: "technologies" | "materials" | "colors" | "finishes" | "uses" | "conditions" | "relatedWorks", options: string[]) { const selected = serviceForm[field]; return (
{options.map((option) => ( ))}
); } function renderServiceStep() { if (serviceStep === 0) { return (
Nuevo servicio

Que servicio quieres ofrecer?

{serviceTemplates.map((template) => ( ))}
); } if (serviceStep === 1) { return (
Datos basicos

Configuracion del servicio

updateServiceForm("title", event.target.value)} placeholder="Nombre del servicio" /> updateServiceForm("category", event.target.value)} placeholder="Categoria" />