1407 lines
54 KiB
TypeScript
1407 lines
54 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
|
import { browserFetch } from "../lib/api";
|
|
import { logoutByNavigation } from "../lib/logout";
|
|
|
|
export type AdminSection =
|
|
| "overview"
|
|
| "queue"
|
|
| "clients"
|
|
| "makers"
|
|
| "internal-users"
|
|
| "reviews"
|
|
| "incidents"
|
|
| "reports"
|
|
| "fraud"
|
|
| "appeals"
|
|
| "subscriptions"
|
|
| "payments"
|
|
| "billing"
|
|
| "promotions"
|
|
| "founders"
|
|
| "works"
|
|
| "showcases"
|
|
| "services"
|
|
| "reported-media"
|
|
| "catalog-services"
|
|
| "categories"
|
|
| "materials"
|
|
| "technologies"
|
|
| "specialties"
|
|
| "locations"
|
|
| "tickets"
|
|
| "conversations"
|
|
| "help-center"
|
|
| "analytics-product"
|
|
| "analytics-makers"
|
|
| "analytics-clients"
|
|
| "analytics-trust"
|
|
| "analytics-business"
|
|
| "roles"
|
|
| "settings"
|
|
| "integrations"
|
|
| "templates"
|
|
| "audit"
|
|
| "system";
|
|
|
|
type AdminClientProps = {
|
|
section?: AdminSection;
|
|
};
|
|
|
|
type AdminQueueItem = {
|
|
id: string;
|
|
kind: string;
|
|
user: string;
|
|
state: string;
|
|
owner: string;
|
|
wait: string;
|
|
priority: "critical" | "high" | "normal" | "low";
|
|
};
|
|
|
|
type AdminAction = {
|
|
title: string;
|
|
detail: string;
|
|
tone?: "primary" | "success" | "warning" | "danger";
|
|
};
|
|
|
|
function toText(value: unknown, fallback = "") {
|
|
return typeof value === "string" && value ? value : fallback;
|
|
}
|
|
|
|
function toNumber(value: unknown, fallback = 0) {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
}
|
|
|
|
const adminCopy: Record<AdminSection, { eyebrow: string; title: string; description: string }> = {
|
|
overview: {
|
|
eyebrow: "Backoffice",
|
|
title: "Operacion, confianza y riesgo en una sola vista",
|
|
description: "Entrada diaria para administradores generales, moderacion y soporte."
|
|
},
|
|
queue: {
|
|
eyebrow: "Trabajo pendiente",
|
|
title: "Colas operativas priorizadas",
|
|
description: "Todo caso sensible entra por una cola con responsable, prioridad y trazabilidad."
|
|
},
|
|
clients: {
|
|
eyebrow: "Usuarios",
|
|
title: "Clientes y actividad de demanda",
|
|
description: "Supervisa busquedas, consultas, favoritos e historial de confianza del cliente."
|
|
},
|
|
makers: {
|
|
eyebrow: "Usuarios",
|
|
title: "Makers, publicacion y presencia",
|
|
description: "Revisa perfiles, estado comercial, riesgo y acciones administrativas seguras."
|
|
},
|
|
"internal-users": {
|
|
eyebrow: "Usuarios internos",
|
|
title: "Administradores, moderadores y soporte",
|
|
description: "Control de permisos internos sin abrir el backoffice a clientes ni makers."
|
|
},
|
|
reviews: {
|
|
eyebrow: "Confianza",
|
|
title: "Resenas, reputacion y revision manual",
|
|
description: "Las opiniones reportadas se revisan con contexto y evidencia."
|
|
},
|
|
incidents: {
|
|
eyebrow: "Confianza",
|
|
title: "Incidencias entre cliente y maker",
|
|
description: "Mediacion operativa para problemas de calidad, entrega o comunicacion."
|
|
},
|
|
reports: {
|
|
eyebrow: "Confianza",
|
|
title: "Reportes de contenido y comportamiento",
|
|
description: "Entrada rapida para denuncias, contenido inapropiado o riesgo reputacional."
|
|
},
|
|
fraud: {
|
|
eyebrow: "Confianza",
|
|
title: "Prevencion de fraude",
|
|
description: "Senales, bloqueos y acciones preventivas para proteger la plataforma."
|
|
},
|
|
appeals: {
|
|
eyebrow: "Confianza",
|
|
title: "Apelaciones y reconsideraciones",
|
|
description: "Revision secundaria cuando un maker o cliente cuestiona una decision."
|
|
},
|
|
subscriptions: {
|
|
eyebrow: "Negocio",
|
|
title: "Suscripciones del Plan Maker",
|
|
description: "Estado de planes, renovaciones, suspensiones y reactivaciones."
|
|
},
|
|
payments: {
|
|
eyebrow: "Negocio",
|
|
title: "Pagos en riesgo y recuperacion",
|
|
description: "Seguimiento de cobros fallidos, gracia y acciones de retencion."
|
|
},
|
|
billing: {
|
|
eyebrow: "Negocio",
|
|
title: "Facturacion y comprobantes",
|
|
description: "Eventos de facturacion, facturas y datos fiscales de cada maker."
|
|
},
|
|
promotions: {
|
|
eyebrow: "Negocio",
|
|
title: "Promociones y visibilidad",
|
|
description: "Herramientas para impulsar perfiles sin alterar el ranking organico."
|
|
},
|
|
founders: {
|
|
eyebrow: "Negocio",
|
|
title: "Makers fundadores",
|
|
description: "Beneficios especiales para los primeros makers de la plataforma."
|
|
},
|
|
works: {
|
|
eyebrow: "Contenido",
|
|
title: "Trabajos publicados y borradores",
|
|
description: "Revision de portafolios, calidad visual y piezas reportadas."
|
|
},
|
|
showcases: {
|
|
eyebrow: "Contenido",
|
|
title: "Escaparates y colecciones",
|
|
description: "Control de colecciones publicas, orden y destacadas."
|
|
},
|
|
services: {
|
|
eyebrow: "Contenido",
|
|
title: "Servicios y precios",
|
|
description: "Auditoria de capacidades, precios orientativos y condiciones visibles."
|
|
},
|
|
"reported-media": {
|
|
eyebrow: "Contenido",
|
|
title: "Multimedia reportada",
|
|
description: "Fotos, videos o archivos revisados antes de afectar la reputacion."
|
|
},
|
|
"catalog-services": {
|
|
eyebrow: "Catalogos",
|
|
title: "Catalogo de servicios",
|
|
description: "Categorias base para que los makers configuren su oferta."
|
|
},
|
|
categories: {
|
|
eyebrow: "Catalogos",
|
|
title: "Categorias publicas",
|
|
description: "Taxonomia de descubrimiento, filtros y escaparates."
|
|
},
|
|
materials: {
|
|
eyebrow: "Catalogos",
|
|
title: "Materiales",
|
|
description: "Materiales admitidos, aliases y filtros de busqueda."
|
|
},
|
|
technologies: {
|
|
eyebrow: "Catalogos",
|
|
title: "Tecnologias",
|
|
description: "FDM, resina, CAD, escaneo y capacidades futuras."
|
|
},
|
|
specialties: {
|
|
eyebrow: "Catalogos",
|
|
title: "Especialidades",
|
|
description: "Areas de enfoque para ranking, recomendaciones y perfil publico."
|
|
},
|
|
locations: {
|
|
eyebrow: "Catalogos",
|
|
title: "Ubicaciones y cobertura",
|
|
description: "Privacidad, cobertura y geocodificacion usada en los mapas."
|
|
},
|
|
tickets: {
|
|
eyebrow: "Soporte",
|
|
title: "Tickets sin asignar y SLA",
|
|
description: "Soporte operativo separado de conversaciones cliente-maker."
|
|
},
|
|
conversations: {
|
|
eyebrow: "Soporte",
|
|
title: "Conversaciones escaladas",
|
|
description: "Acceso administrativo solo cuando hay incidencia, reporte o auditoria."
|
|
},
|
|
"help-center": {
|
|
eyebrow: "Soporte",
|
|
title: "Centro de ayuda",
|
|
description: "Articulos, plantillas y respuestas sugeridas para reducir tickets."
|
|
},
|
|
"analytics-product": {
|
|
eyebrow: "Analitica",
|
|
title: "Producto y embudo",
|
|
description: "Busqueda, descubrimiento, consulta guiada y conversion."
|
|
},
|
|
"analytics-makers": {
|
|
eyebrow: "Analitica",
|
|
title: "Rendimiento de makers",
|
|
description: "Oferta, respuesta, reputacion y crecimiento de perfiles."
|
|
},
|
|
"analytics-clients": {
|
|
eyebrow: "Analitica",
|
|
title: "Comportamiento de clientes",
|
|
description: "Demanda, filtros, favoritos, contactos y recurrencia."
|
|
},
|
|
"analytics-trust": {
|
|
eyebrow: "Analitica",
|
|
title: "Confianza y seguridad",
|
|
description: "Incidencias, resenas, reportes y tiempos de resolucion."
|
|
},
|
|
"analytics-business": {
|
|
eyebrow: "Analitica",
|
|
title: "Negocio",
|
|
description: "Planes, pagos, cohortes y senales de retencion."
|
|
},
|
|
roles: {
|
|
eyebrow: "Sistema",
|
|
title: "Roles y permisos",
|
|
description: "Acceso exclusivo de admin con modos internos de moderador y soporte."
|
|
},
|
|
settings: {
|
|
eyebrow: "Sistema",
|
|
title: "Configuracion operativa",
|
|
description: "Parametros seguros para operar el MVP sin tocar codigo cada vez."
|
|
},
|
|
integrations: {
|
|
eyebrow: "Sistema",
|
|
title: "Integraciones",
|
|
description: "Servicios externos: mapas, email, imagenes, pagos y notificaciones."
|
|
},
|
|
templates: {
|
|
eyebrow: "Sistema",
|
|
title: "Plantillas",
|
|
description: "Mensajes, decisiones, soporte y respuestas reutilizables."
|
|
},
|
|
audit: {
|
|
eyebrow: "Sistema",
|
|
title: "Auditoria y evidencia",
|
|
description: "Registro de acciones sensibles, decisiones y accesos recientes."
|
|
},
|
|
system: {
|
|
eyebrow: "Sistema",
|
|
title: "Estado operativo",
|
|
description: "Salud de servicios, degradaciones y problemas conocidos."
|
|
}
|
|
};
|
|
|
|
const adminNavGroups: Array<{
|
|
label: string;
|
|
items: Array<{ href: string; label: string; section: AdminSection; badge?: string }>;
|
|
}> = [
|
|
{
|
|
label: "Principal",
|
|
items: [
|
|
{ href: "/app/backoffice", label: "Inicio", section: "overview" },
|
|
{ href: "/app/backoffice/queue", label: "Trabajo pendiente", section: "queue", badge: "12" }
|
|
]
|
|
},
|
|
{
|
|
label: "Usuarios",
|
|
items: [
|
|
{ href: "/app/backoffice/clients", label: "Clientes", section: "clients" },
|
|
{ href: "/app/backoffice/makers", label: "Makers", section: "makers" },
|
|
{ href: "/app/backoffice/internal-users", label: "Usuarios internos", section: "internal-users" }
|
|
]
|
|
},
|
|
{
|
|
label: "Confianza",
|
|
items: [
|
|
{ href: "/app/backoffice/reviews", label: "Resenas", section: "reviews", badge: "8" },
|
|
{ href: "/app/backoffice/incidents", label: "Incidencias", section: "incidents", badge: "3" },
|
|
{ href: "/app/backoffice/reports", label: "Reportes", section: "reports" },
|
|
{ href: "/app/backoffice/fraud", label: "Fraude", section: "fraud" },
|
|
{ href: "/app/backoffice/appeals", label: "Apelaciones", section: "appeals" }
|
|
]
|
|
},
|
|
{
|
|
label: "Negocio",
|
|
items: [
|
|
{ href: "/app/backoffice/subscriptions", label: "Suscripciones", section: "subscriptions", badge: "5" },
|
|
{ href: "/app/backoffice/payments", label: "Pagos", section: "payments" },
|
|
{ href: "/app/backoffice/billing", label: "Facturacion", section: "billing" },
|
|
{ href: "/app/backoffice/promotions", label: "Promociones", section: "promotions" },
|
|
{ href: "/app/backoffice/founders", label: "Makers fundadores", section: "founders" }
|
|
]
|
|
},
|
|
{
|
|
label: "Contenido",
|
|
items: [
|
|
{ href: "/app/backoffice/works", label: "Trabajos", section: "works" },
|
|
{ href: "/app/backoffice/showcases", label: "Escaparates", section: "showcases" },
|
|
{ href: "/app/backoffice/services", label: "Servicios", section: "services" },
|
|
{ href: "/app/backoffice/reported-media", label: "Multimedia reportada", section: "reported-media" }
|
|
]
|
|
},
|
|
{
|
|
label: "Catalogos",
|
|
items: [
|
|
{ href: "/app/backoffice/catalog-services", label: "Servicios", section: "catalog-services" },
|
|
{ href: "/app/backoffice/categories", label: "Categorias", section: "categories" },
|
|
{ href: "/app/backoffice/materials", label: "Materiales", section: "materials" },
|
|
{ href: "/app/backoffice/technologies", label: "Tecnologias", section: "technologies" },
|
|
{ href: "/app/backoffice/specialties", label: "Especialidades", section: "specialties" },
|
|
{ href: "/app/backoffice/locations", label: "Ubicaciones", section: "locations" }
|
|
]
|
|
},
|
|
{
|
|
label: "Soporte",
|
|
items: [
|
|
{ href: "/app/backoffice/tickets", label: "Tickets", section: "tickets", badge: "8" },
|
|
{ href: "/app/backoffice/conversations", label: "Conversaciones", section: "conversations" },
|
|
{ href: "/app/backoffice/help-center", label: "Centro de ayuda", section: "help-center" }
|
|
]
|
|
},
|
|
{
|
|
label: "Analitica",
|
|
items: [
|
|
{ href: "/app/backoffice/analytics-product", label: "Producto", section: "analytics-product" },
|
|
{ href: "/app/backoffice/analytics-makers", label: "Makers", section: "analytics-makers" },
|
|
{ href: "/app/backoffice/analytics-clients", label: "Clientes", section: "analytics-clients" },
|
|
{ href: "/app/backoffice/analytics-trust", label: "Confianza", section: "analytics-trust" },
|
|
{ href: "/app/backoffice/analytics-business", label: "Negocio", section: "analytics-business" }
|
|
]
|
|
},
|
|
{
|
|
label: "Sistema",
|
|
items: [
|
|
{ href: "/app/backoffice/roles", label: "Roles y permisos", section: "roles" },
|
|
{ href: "/app/backoffice/settings", label: "Configuracion", section: "settings" },
|
|
{ href: "/app/backoffice/integrations", label: "Integraciones", section: "integrations" },
|
|
{ href: "/app/backoffice/templates", label: "Plantillas", section: "templates" },
|
|
{ href: "/app/backoffice/audit", label: "Auditoria", section: "audit" },
|
|
{ href: "/app/backoffice/system", label: "Estado operativo", section: "system" }
|
|
]
|
|
}
|
|
];
|
|
|
|
const queueSeed: AdminQueueItem[] = [
|
|
{ id: "#R-832", kind: "Resena", user: "Juan P.", state: "En analisis", owner: "Maria Lopez", wait: "34 min", priority: "critical" },
|
|
{ id: "#I-048", kind: "Incidencia", user: "MakerLab 3D", state: "Nuevo", owner: "Sin asignar", wait: "2 h", priority: "high" },
|
|
{ id: "#R-831", kind: "Resena", user: "Ana G.", state: "Asignado", owner: "Maria Lopez", wait: "3 h", priority: "high" },
|
|
{ id: "#T-184", kind: "Ticket", user: "Juan P.", state: "Esperando usuario", owner: "Pablo Dibene", wait: "1 dia", priority: "normal" },
|
|
{ id: "#T-182", kind: "Ticket", user: "Carlos R.", state: "Nuevo", owner: "Sin asignar", wait: "1 dia", priority: "normal" }
|
|
];
|
|
|
|
const systemHealthRows = [
|
|
["API", "Operativa", "success"],
|
|
["Base de datos", "Operativa", "success"],
|
|
["Imagenes", "Operativa", "success"],
|
|
["Emails", "Degradado", "warning"],
|
|
["Notificaciones", "Operativa", "success"],
|
|
["Pagos", "Operativa", "success"],
|
|
["Mapas / Geocodificacion", "Operativa", "success"],
|
|
["Tareas en cola", "Operativa", "success"]
|
|
];
|
|
|
|
const platformRows = [
|
|
["42", "Nuevas consultas", "+18%", "success"],
|
|
["18", "Nuevos makers", "+12%", "success"],
|
|
["7", "Perfiles publicados", "+8%", "success"],
|
|
["3", "Cancelaciones de suscripcion", "-3%", "danger"],
|
|
["96,4%", "Servicios operativos", "+0,2%", "success"]
|
|
];
|
|
|
|
const workRows = [
|
|
["Ticket #184", "Problema de acceso", "ALTA", "Hace 5 min"],
|
|
["Resena #832", "Posible chantaje", "CRITICA", "Hace 1 h"],
|
|
["Incidencia #48", "Calidad del trabajo", "ALTA", "Hace 2 h"],
|
|
["Reporte #67", "Contenido inapropiado", "NORMAL", "Hace 3 h"]
|
|
];
|
|
|
|
const paymentHistory = [
|
|
["10 jul, 2026", "Renovacion", "$19.990", "Pagado"],
|
|
["8 jul, 2026", "Intento de cobro", "$19.990", "Fallido"],
|
|
["7 jul, 2026", "Intento de cobro", "$19.990", "Fallido"],
|
|
["10 jun, 2026", "Renovacion", "$19.990", "Pagado"],
|
|
["10 may, 2026", "Renovacion", "$19.990", "Pagado"]
|
|
];
|
|
|
|
const sectionActions: Record<AdminSection, AdminAction[]> = {
|
|
overview: [
|
|
{ title: "Priorizar cola", detail: "Ordena pendientes por SLA, riesgo y antiguedad.", tone: "primary" },
|
|
{ title: "Revisar sistema", detail: "Abre servicios degradados y problemas conocidos.", tone: "success" }
|
|
],
|
|
queue: [
|
|
{ title: "Asignar responsable", detail: "Mueve el caso a mi cola y registra auditoria.", tone: "primary" },
|
|
{ title: "Escalar prioridad", detail: "Marca el caso como critico para revision inmediata.", tone: "danger" }
|
|
],
|
|
clients: [
|
|
{ title: "Ver actividad", detail: "Consultas, favoritos, reportes y resenas del cliente.", tone: "primary" },
|
|
{ title: "Bloquear contacto", detail: "Evita nuevas interacciones si hay riesgo.", tone: "warning" }
|
|
],
|
|
makers: [
|
|
{ title: "Ver ficha completa", detail: "Perfil, trabajos, consultas, reputacion y plan.", tone: "primary" },
|
|
{ title: "Suspender perfil", detail: "Accion real: cambia el estado del maker.", tone: "danger" }
|
|
],
|
|
"internal-users": [
|
|
{ title: "Crear moderador", detail: "Permisos limitados a confianza y soporte.", tone: "primary" },
|
|
{ title: "Revisar sesiones", detail: "Audita accesos recientes del equipo.", tone: "warning" }
|
|
],
|
|
reviews: [
|
|
{ title: "Mantener resena", detail: "La opinion queda publica y se notifica al maker.", tone: "success" },
|
|
{ title: "Solicitar edicion", detail: "Pide corregir datos personales o lenguaje.", tone: "warning" },
|
|
{ title: "Invalidar resena", detail: "Oculta la resena por politica.", tone: "danger" }
|
|
],
|
|
incidents: [
|
|
{ title: "Solicitar informacion", detail: "Pide evidencia al cliente o maker.", tone: "primary" },
|
|
{ title: "Resolver incidencia", detail: "Cierra el caso con resolucion registrada.", tone: "success" }
|
|
],
|
|
reports: [
|
|
{ title: "Validar reporte", detail: "Confirma que incumple una politica.", tone: "warning" },
|
|
{ title: "Descartar reporte", detail: "Mantiene el contenido visible.", tone: "success" }
|
|
],
|
|
fraud: [
|
|
{ title: "Congelar cuenta", detail: "Bloqueo preventivo hasta revision manual.", tone: "danger" },
|
|
{ title: "Marcar falso positivo", detail: "Libera la cuenta y aprende del caso.", tone: "success" }
|
|
],
|
|
appeals: [
|
|
{ title: "Aceptar apelacion", detail: "Revierte una decision con motivo.", tone: "success" },
|
|
{ title: "Mantener decision", detail: "Conserva la sancion y deja evidencia.", tone: "warning" }
|
|
],
|
|
subscriptions: [
|
|
{ title: "Extender gracia", detail: "Evita suspender perfil por un pago puntual.", tone: "warning" },
|
|
{ title: "Reactivar perfil", detail: "Publica de nuevo al resolver el pago.", tone: "success" }
|
|
],
|
|
payments: [
|
|
{ title: "Reintentar cobro", detail: "Dispara reintento controlado del plan.", tone: "primary" },
|
|
{ title: "Aplicar credito", detail: "Compensa un problema operativo.", tone: "warning" }
|
|
],
|
|
billing: [
|
|
{ title: "Ver factura", detail: "Consulta comprobantes y estado fiscal.", tone: "primary" },
|
|
{ title: "Actualizar datos", detail: "Deriva al flujo validado de facturacion.", tone: "warning" }
|
|
],
|
|
promotions: [
|
|
{ title: "Crear promocion", detail: "Boost temporal identificado como promocionado.", tone: "primary" },
|
|
{ title: "Pausar campana", detail: "Detiene promocion sin afectar perfil.", tone: "warning" }
|
|
],
|
|
founders: [
|
|
{ title: "Otorgar fundador", detail: "Aplica beneficio especial por tiempo limitado.", tone: "success" },
|
|
{ title: "Revisar vigencia", detail: "Controla fecha y condiciones del beneficio.", tone: "warning" }
|
|
],
|
|
works: [
|
|
{ title: "Revisar trabajo", detail: "Calidad visual, categoria, fotos y reportes.", tone: "primary" },
|
|
{ title: "Ocultar trabajo", detail: "Retira contenido sin borrar evidencia.", tone: "danger" }
|
|
],
|
|
showcases: [
|
|
{ title: "Ver escaparate", detail: "Controla coleccion, portada y orden.", tone: "primary" },
|
|
{ title: "Ocultar escaparate", detail: "No aparece publicamente hasta corregir.", tone: "warning" }
|
|
],
|
|
services: [
|
|
{ title: "Auditar servicio", detail: "Revisa capacidades, precio y condiciones.", tone: "primary" },
|
|
{ title: "Pausar servicio", detail: "Evita consultas mal calificadas.", tone: "warning" }
|
|
],
|
|
"reported-media": [
|
|
{ title: "Aprobar imagen", detail: "Mantiene multimedia visible.", tone: "success" },
|
|
{ title: "Retirar imagen", detail: "Oculta y registra motivo.", tone: "danger" }
|
|
],
|
|
"catalog-services": [
|
|
{ title: "Agregar servicio", detail: "Nueva opcion para configuracion maker.", tone: "primary" },
|
|
{ title: "Fusionar duplicados", detail: "Reduce ruido en filtros.", tone: "warning" }
|
|
],
|
|
categories: [
|
|
{ title: "Crear categoria", detail: "Aparece en busqueda y escaparates.", tone: "primary" },
|
|
{ title: "Reordenar", detail: "Prioriza categorias de mayor demanda.", tone: "warning" }
|
|
],
|
|
materials: [
|
|
{ title: "Agregar material", detail: "Disponible en servicios, trabajos y filtros.", tone: "primary" },
|
|
{ title: "Ocultar material", detail: "Evita seleccion publica sin borrarlo.", tone: "warning" }
|
|
],
|
|
technologies: [
|
|
{ title: "Agregar tecnologia", detail: "Activa una capacidad nueva.", tone: "primary" },
|
|
{ title: "Marcar experimental", detail: "Visible solo para pruebas internas.", tone: "warning" }
|
|
],
|
|
specialties: [
|
|
{ title: "Editar especialidad", detail: "Ajusta etiquetas recomendadas.", tone: "primary" },
|
|
{ title: "Medir demanda", detail: "Muestra busquedas relacionadas.", tone: "success" }
|
|
],
|
|
locations: [
|
|
{ title: "Validar geocodificacion", detail: "Revisa coordenadas y privacidad.", tone: "primary" },
|
|
{ title: "Crear alerta", detail: "Notifica cuando haya oferta compatible.", tone: "success" }
|
|
],
|
|
tickets: [
|
|
{ title: "Asignar ticket", detail: "Pasa a un agente de soporte.", tone: "primary" },
|
|
{ title: "Responder con plantilla", detail: "Usa respuesta sugerida.", tone: "success" }
|
|
],
|
|
conversations: [
|
|
{ title: "Ver contexto", detail: "Muestra consulta, acuerdo y evidencias.", tone: "primary" },
|
|
{ title: "Registrar decision", detail: "Audita intervencion administrativa.", tone: "warning" }
|
|
],
|
|
"help-center": [
|
|
{ title: "Crear articulo", detail: "Reduce tickets repetitivos.", tone: "primary" },
|
|
{ title: "Publicar plantilla", detail: "Respuesta reusable para soporte.", tone: "success" }
|
|
],
|
|
"analytics-product": [
|
|
{ title: "Exportar embudo", detail: "Busqueda a consulta y consulta a acuerdo.", tone: "primary" },
|
|
{ title: "Crear alerta", detail: "Notifica caidas relevantes.", tone: "warning" }
|
|
],
|
|
"analytics-makers": [
|
|
{ title: "Ver ranking", detail: "Oferta, respuesta y conversion.", tone: "primary" },
|
|
{ title: "Detectar baja calidad", detail: "Senales preventivas por perfil.", tone: "warning" }
|
|
],
|
|
"analytics-clients": [
|
|
{ title: "Segmentar demanda", detail: "Necesidades frecuentes por zona.", tone: "primary" },
|
|
{ title: "Revisar retencion", detail: "Clientes que vuelven a consultar.", tone: "success" }
|
|
],
|
|
"analytics-trust": [
|
|
{ title: "Abrir riesgo", detail: "Incidencias por severidad.", tone: "danger" },
|
|
{ title: "Medir resolucion", detail: "Tiempo medio por tipo de caso.", tone: "success" }
|
|
],
|
|
"analytics-business": [
|
|
{ title: "Ver MRR demo", detail: "Planes activos y pagos en riesgo.", tone: "primary" },
|
|
{ title: "Cohortes", detail: "Retencion de makers por mes.", tone: "success" }
|
|
],
|
|
roles: [
|
|
{ title: "Crear rol interno", detail: "Define permisos de soporte o moderacion.", tone: "primary" },
|
|
{ title: "Revocar acceso", detail: "Cierra sesiones y registra auditoria.", tone: "danger" }
|
|
],
|
|
settings: [
|
|
{ title: "Editar parametros", detail: "Cambios seguros sin redeploy.", tone: "primary" },
|
|
{ title: "Restaurar defaults", detail: "Vuelve a configuracion MVP.", tone: "warning" }
|
|
],
|
|
integrations: [
|
|
{ title: "Probar servicio", detail: "Verifica mapas, email, pagos o imagenes.", tone: "primary" },
|
|
{ title: "Marcar degradado", detail: "Activa aviso interno.", tone: "warning" }
|
|
],
|
|
templates: [
|
|
{ title: "Editar plantilla", detail: "Mensajes de soporte y moderacion.", tone: "primary" },
|
|
{ title: "Publicar version", detail: "Disponible para todo el equipo.", tone: "success" }
|
|
],
|
|
audit: [
|
|
{ title: "Filtrar eventos", detail: "Busca por actor, recurso o decision.", tone: "primary" },
|
|
{ title: "Exportar evidencia", detail: "Paquete para revision interna.", tone: "warning" }
|
|
],
|
|
system: [
|
|
{ title: "Ver detalles tecnicos", detail: "Estado por servicio y cola.", tone: "primary" },
|
|
{ title: "Crear incidente interno", detail: "Abre seguimiento de degradacion.", tone: "danger" }
|
|
]
|
|
};
|
|
|
|
export default function AdminClient({ section = "overview" }: AdminClientProps) {
|
|
const [overview, setOverview] = useState<{ stats: Record<string, number> } | null>(null);
|
|
const [makers, setMakers] = useState<Array<Record<string, unknown>>>([]);
|
|
const [cases, setCases] = useState<Array<Record<string, unknown>>>([]);
|
|
const [error, setError] = useState("");
|
|
const [adminSearch, setAdminSearch] = useState("");
|
|
const [queueTab, setQueueTab] = useState<"mine" | "unassigned" | "high" | "all">("mine");
|
|
const [selectedMakerId, setSelectedMakerId] = useState("");
|
|
const [selectedCaseId, setSelectedCaseId] = useState("");
|
|
const [actionLog, setActionLog] = useState<string[]>([]);
|
|
const [caseState, setCaseState] = useState("En mediacion");
|
|
const [caseOwner, setCaseOwner] = useState("Maria Lopez");
|
|
const [reviewDecision, setReviewDecision] = useState("Invalidar resena");
|
|
|
|
async function loadAll() {
|
|
try {
|
|
const [overviewData, makersData, casesData] = await Promise.all([
|
|
browserFetch<{ stats: Record<string, number> }>("/admin/overview"),
|
|
browserFetch<{ makers: Array<Record<string, unknown>> }>("/admin/makers"),
|
|
browserFetch<{ cases: Array<Record<string, unknown>> }>("/admin/cases")
|
|
]);
|
|
setOverview(overviewData);
|
|
setMakers(makersData.makers);
|
|
setCases(casesData.cases);
|
|
setSelectedMakerId((current) => current || String(makersData.makers[0]?.id || ""));
|
|
setSelectedCaseId((current) => current || String(casesData.cases[0]?.id || queueSeed[1].id));
|
|
} catch (loadError) {
|
|
setError(loadError instanceof Error ? loadError.message : "No se pudo cargar admin");
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void loadAll();
|
|
}, []);
|
|
|
|
function recordAction(message: string) {
|
|
setActionLog((current) => [`${new Date().toLocaleTimeString("es-AR", { hour: "2-digit", minute: "2-digit" })} - ${message}`, ...current].slice(0, 8));
|
|
}
|
|
|
|
async function suspendMaker(id: string) {
|
|
try {
|
|
await browserFetch(`/admin/makers/${id}/suspend`, { method: "POST" });
|
|
recordAction("Maker suspendido y registrado en auditoria.");
|
|
await loadAll();
|
|
} catch (actionError) {
|
|
setError(actionError instanceof Error ? actionError.message : "No se pudo suspender");
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
logoutByNavigation();
|
|
}
|
|
|
|
const search = adminSearch.trim().toLowerCase();
|
|
const filteredMakers = useMemo(() => {
|
|
if (!search) {
|
|
return makers;
|
|
}
|
|
return makers.filter((maker) => {
|
|
const haystack = [
|
|
maker.business_name,
|
|
maker.email,
|
|
maker.city,
|
|
maker.province,
|
|
maker.status,
|
|
maker.availability
|
|
].map((value) => String(value || "").toLowerCase()).join(" ");
|
|
return haystack.includes(search);
|
|
});
|
|
}, [makers, search]);
|
|
|
|
const queueRows = useMemo(() => {
|
|
const apiRows = cases.map((moderationCase, index): AdminQueueItem => ({
|
|
id: `#C-${String(moderationCase.id || index).slice(0, 4)}`,
|
|
kind: toText(moderationCase.target_type, "Caso"),
|
|
user: toText(moderationCase.reason, "Revision manual").slice(0, 24),
|
|
state: toText(moderationCase.status, "open") === "resolved" ? "Resuelto" : "Nuevo",
|
|
owner: index % 2 === 0 ? "Sin asignar" : "Maria Lopez",
|
|
wait: index % 2 === 0 ? "45 min" : "2 h",
|
|
priority: index % 3 === 0 ? "critical" : index % 3 === 1 ? "high" : "normal"
|
|
}));
|
|
const rows = [...apiRows, ...queueSeed];
|
|
if (queueTab === "mine") {
|
|
return rows.filter((row) => row.owner !== "Sin asignar").slice(0, 8);
|
|
}
|
|
if (queueTab === "unassigned") {
|
|
return rows.filter((row) => row.owner === "Sin asignar").slice(0, 8);
|
|
}
|
|
if (queueTab === "high") {
|
|
return rows.filter((row) => row.priority === "critical" || row.priority === "high").slice(0, 8);
|
|
}
|
|
return rows.slice(0, 10);
|
|
}, [cases, queueTab]);
|
|
|
|
const selectedMaker = useMemo(
|
|
() => filteredMakers.find((maker) => String(maker.id) === selectedMakerId) || filteredMakers[0] || makers[0],
|
|
[filteredMakers, makers, selectedMakerId]
|
|
);
|
|
|
|
const copy = adminCopy[section];
|
|
const stats = overview?.stats || {};
|
|
const activeActionLog = actionLog.length ? actionLog : [
|
|
"14:32 - Credito aplicado a MakerLab 3D.",
|
|
"13:10 - Resena invalidada #R-832.",
|
|
"12:45 - Sesiones revocadas a Juan P.",
|
|
"11:22 - Rol asignado a Carlos R.",
|
|
"10:15 - Incidencia resuelta #I-037."
|
|
];
|
|
|
|
return (
|
|
<div className="admin-shell">
|
|
<aside className="admin-sidebar">
|
|
<div className="admin-brand">
|
|
<span className="admin-logo">M3D</span>
|
|
<div>
|
|
<strong>Makers 3D</strong>
|
|
<span>Backoffice</span>
|
|
</div>
|
|
</div>
|
|
|
|
<nav className="admin-nav" aria-label="Backoffice">
|
|
{adminNavGroups.map((group) => (
|
|
<div key={group.label} className="admin-nav-group">
|
|
<span>{group.label}</span>
|
|
{group.items.map((item) => (
|
|
<a key={item.href} href={item.href} className={`admin-nav-item ${section === item.section ? "active" : ""}`}>
|
|
<span>{item.label}</span>
|
|
{item.badge ? <b>{item.badge}</b> : null}
|
|
</a>
|
|
))}
|
|
</div>
|
|
))}
|
|
</nav>
|
|
|
|
{error ? <div className="admin-alert danger">{error}</div> : null}
|
|
|
|
<button className="admin-collapse" type="button" onClick={logout}>
|
|
Cerrar sesion
|
|
</button>
|
|
</aside>
|
|
|
|
<section className="admin-main">
|
|
<header className="admin-topbar">
|
|
<div className="admin-search">
|
|
<span aria-hidden="true">/</span>
|
|
<input
|
|
value={adminSearch}
|
|
onChange={(event) => setAdminSearch(event.target.value)}
|
|
placeholder="Buscar usuario, Maker, consulta, encargo, ticket..."
|
|
/>
|
|
<kbd>Ctrl K</kbd>
|
|
</div>
|
|
<div className="admin-status-line">
|
|
<span className="admin-dot success" />
|
|
<strong>Sistema operativo</strong>
|
|
</div>
|
|
<button className="admin-icon-button" type="button" onClick={() => recordAction("Notificaciones revisadas.")}>
|
|
4
|
|
</button>
|
|
<div className="admin-user">
|
|
<span>PD</span>
|
|
<div>
|
|
<strong>Pablo Dibene</strong>
|
|
<small>Superadmin</small>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="admin-mobile-title">
|
|
<button type="button" className="workspace-menu-button" aria-label="Menu">
|
|
<span />
|
|
<span />
|
|
<span />
|
|
</button>
|
|
<div>
|
|
<strong>{copy.title}</strong>
|
|
<span>{copy.eyebrow}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{section === "overview" ? (
|
|
<OverviewDashboard
|
|
stats={stats}
|
|
makers={filteredMakers}
|
|
selectedMaker={selectedMaker}
|
|
queueRows={queueRows}
|
|
activeActionLog={activeActionLog}
|
|
queueTab={queueTab}
|
|
setQueueTab={setQueueTab}
|
|
setSelectedMakerId={setSelectedMakerId}
|
|
selectedCaseId={selectedCaseId}
|
|
setSelectedCaseId={setSelectedCaseId}
|
|
caseState={caseState}
|
|
setCaseState={setCaseState}
|
|
caseOwner={caseOwner}
|
|
setCaseOwner={setCaseOwner}
|
|
reviewDecision={reviewDecision}
|
|
setReviewDecision={setReviewDecision}
|
|
recordAction={recordAction}
|
|
suspendMaker={suspendMaker}
|
|
/>
|
|
) : (
|
|
<SectionWorkspace
|
|
section={section}
|
|
copy={copy}
|
|
makers={filteredMakers}
|
|
selectedMaker={selectedMaker}
|
|
queueRows={queueRows}
|
|
activeActionLog={activeActionLog}
|
|
recordAction={recordAction}
|
|
suspendMaker={suspendMaker}
|
|
/>
|
|
)}
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function OverviewDashboard({
|
|
stats,
|
|
makers,
|
|
selectedMaker,
|
|
queueRows,
|
|
activeActionLog,
|
|
queueTab,
|
|
setQueueTab,
|
|
setSelectedMakerId,
|
|
selectedCaseId,
|
|
setSelectedCaseId,
|
|
caseState,
|
|
setCaseState,
|
|
caseOwner,
|
|
setCaseOwner,
|
|
reviewDecision,
|
|
setReviewDecision,
|
|
recordAction,
|
|
suspendMaker
|
|
}: {
|
|
stats: Record<string, number>;
|
|
makers: Array<Record<string, unknown>>;
|
|
selectedMaker?: Record<string, unknown>;
|
|
queueRows: AdminQueueItem[];
|
|
activeActionLog: string[];
|
|
queueTab: "mine" | "unassigned" | "high" | "all";
|
|
setQueueTab: (tab: "mine" | "unassigned" | "high" | "all") => void;
|
|
setSelectedMakerId: (id: string) => void;
|
|
selectedCaseId: string;
|
|
setSelectedCaseId: (id: string) => void;
|
|
caseState: string;
|
|
setCaseState: (state: string) => void;
|
|
caseOwner: string;
|
|
setCaseOwner: (owner: string) => void;
|
|
reviewDecision: string;
|
|
setReviewDecision: (decision: string) => void;
|
|
recordAction: (message: string) => void;
|
|
suspendMaker: (id: string) => void;
|
|
}) {
|
|
return (
|
|
<>
|
|
<section className="admin-hero-row">
|
|
<div>
|
|
<h1>Buenos dias, Pablo</h1>
|
|
<p>Martes, 28 de julio de 2026</p>
|
|
</div>
|
|
<div className="admin-role-strip">
|
|
<span><b>General</b> vision total</span>
|
|
<span><b>Moderador</b> confianza</span>
|
|
<span><b>Soporte</b> tickets</span>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="admin-grid admin-grid-top">
|
|
<KpiCard number="3" title="Incidencias criticas" detail="Requieren atencion inmediata" tone="danger" />
|
|
<KpiCard number="8" title="Revisiones pendientes" detail="Resenas y contenido" tone="warning" />
|
|
<KpiCard number="12" title="Tickets sin asignar" detail="En cola de soporte" tone="primary" />
|
|
<KpiCard number="5" title="Pagos en riesgo" detail="Proximos a suspension" tone="gold" />
|
|
</section>
|
|
|
|
<section className="admin-board">
|
|
<div className="admin-board-left">
|
|
<article className="admin-panel">
|
|
<PanelHead title="Mi trabajo" meta="Ver toda mi cola (8)" />
|
|
<div className="admin-work-list">
|
|
{workRows.map(([title, detail, badge, time]) => (
|
|
<button key={title} type="button" onClick={() => recordAction(`${title} abierto desde mi trabajo.`)}>
|
|
<span className={`admin-priority ${badge === "CRITICA" ? "danger" : badge === "ALTA" ? "warning" : "normal"}`} />
|
|
<strong>{title}</strong>
|
|
<small>{detail}</small>
|
|
<b>{badge}</b>
|
|
<em>{time}</em>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</article>
|
|
|
|
<article className="admin-panel">
|
|
<PanelHead title="Actividad de la plataforma" meta="Ver analitica" />
|
|
<div className="admin-platform-list">
|
|
{platformRows.map(([value, label, change, tone]) => (
|
|
<div key={label}>
|
|
<strong>{value}</strong>
|
|
<span>{label}</span>
|
|
<b className={`admin-change ${tone}`}>{change}</b>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<article className="admin-panel admin-queue-card">
|
|
<PanelHead title="Trabajo pendiente" meta="Ver todas las colas" />
|
|
<div className="admin-tabs">
|
|
<button type="button" className={queueTab === "mine" ? "active" : ""} onClick={() => setQueueTab("mine")}>Mi cola</button>
|
|
<button type="button" className={queueTab === "unassigned" ? "active" : ""} onClick={() => setQueueTab("unassigned")}>Sin asignar</button>
|
|
<button type="button" className={queueTab === "high" ? "active" : ""} onClick={() => setQueueTab("high")}>Prioridad alta</button>
|
|
<button type="button" className={queueTab === "all" ? "active" : ""} onClick={() => setQueueTab("all")}>Todo</button>
|
|
</div>
|
|
<QueueTable rows={queueRows} selectedCaseId={selectedCaseId} onSelect={setSelectedCaseId} />
|
|
</article>
|
|
|
|
<aside className="admin-side-stack">
|
|
<SystemCard />
|
|
<article className="admin-panel">
|
|
<PanelHead title="Problemas conocidos" meta="Ver todos" />
|
|
<div className="admin-known-problem">
|
|
<strong>Emails de confirmacion retrasados</strong>
|
|
<span>Argentina - Inicio: 14:20</span>
|
|
<b>Investigando</b>
|
|
</div>
|
|
</article>
|
|
</aside>
|
|
</section>
|
|
|
|
<section className="admin-grid admin-grid-detail">
|
|
<MakerCard maker={selectedMaker} makers={makers} setSelectedMakerId={setSelectedMakerId} suspendMaker={suspendMaker} recordAction={recordAction} />
|
|
<ReviewModerationCard decision={reviewDecision} setDecision={setReviewDecision} recordAction={recordAction} />
|
|
<IncidentCard state={caseState} setState={setCaseState} owner={caseOwner} setOwner={setCaseOwner} recordAction={recordAction} />
|
|
<SubscriptionCard recordAction={recordAction} />
|
|
</section>
|
|
|
|
<section className="admin-bottom-grid">
|
|
<MiniQueue title="Tickets sin asignar" rows={["Duda sobre ubicacion", "Error al subir trabajo", "Problema de pago", "Recuperar cuenta"]} />
|
|
<MiniQueue title="Incidencias criticas" rows={["Calidad del trabajo", "No entrega", "Fraude"]} danger />
|
|
<MiniQueue title="Revisiones pendientes" rows={["Posible chantaje", "Contenido ofensivo", "Datos personales", "Spam"]} />
|
|
<MiniQueue title="Pagos en riesgo" rows={["Renovacion fallida", "Periodo de gracia", "Pago rechazado", "Metodo vencido"]} />
|
|
<MiniQueue title="Accesos recientes" rows={activeActionLog} />
|
|
</section>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function SectionWorkspace({
|
|
section,
|
|
copy,
|
|
makers,
|
|
selectedMaker,
|
|
queueRows,
|
|
activeActionLog,
|
|
recordAction,
|
|
suspendMaker
|
|
}: {
|
|
section: AdminSection;
|
|
copy: { eyebrow: string; title: string; description: string };
|
|
makers: Array<Record<string, unknown>>;
|
|
selectedMaker?: Record<string, unknown>;
|
|
queueRows: AdminQueueItem[];
|
|
activeActionLog: string[];
|
|
recordAction: (message: string) => void;
|
|
suspendMaker: (id: string) => void;
|
|
}) {
|
|
const actions = sectionActions[section] || [];
|
|
const catalogRows = getCatalogRows(section);
|
|
|
|
return (
|
|
<>
|
|
<section className="admin-section-hero">
|
|
<div>
|
|
<span className="eyebrow">{copy.eyebrow}</span>
|
|
<h1>{copy.title}</h1>
|
|
<p>{copy.description}</p>
|
|
</div>
|
|
<div className="admin-section-actions">
|
|
{actions.slice(0, 2).map((action) => (
|
|
<button key={action.title} type="button" className={`admin-action-button ${action.tone || "primary"}`} onClick={() => recordAction(`${action.title}: ${action.detail}`)}>
|
|
<strong>{action.title}</strong>
|
|
<span>{action.detail}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="admin-grid admin-section-grid">
|
|
<article className="admin-panel">
|
|
<PanelHead title="Operacion de la seccion" meta="MVP funcional" />
|
|
{section === "makers" ? (
|
|
<div className="admin-list">
|
|
{makers.map((maker) => (
|
|
<div key={String(maker.id)} className="admin-list-row">
|
|
<span className="admin-avatar">{initials(toText(maker.business_name) || toText(maker.email, "MK"))}</span>
|
|
<div>
|
|
<strong>{toText(maker.business_name) || toText(maker.email, "Maker")}</strong>
|
|
<small>{toText(maker.city, "Cordoba")} | {translateStatus(toText(maker.status, "draft"))}</small>
|
|
</div>
|
|
<button type="button" onClick={() => suspendMaker(String(maker.id))}>Suspender</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : section === "queue" || section === "incidents" || section === "tickets" || section === "reviews" ? (
|
|
<QueueTable rows={queueRows} selectedCaseId="" onSelect={(id) => recordAction(`Caso ${id} abierto desde ${copy.eyebrow}.`)} />
|
|
) : (
|
|
<div className="admin-catalog-grid">
|
|
{catalogRows.map((row) => (
|
|
<button key={row.title} type="button" onClick={() => recordAction(`${row.title} revisado.`)}>
|
|
<span className={`admin-catalog-icon ${row.tone}`} />
|
|
<strong>{row.title}</strong>
|
|
<small>{row.detail}</small>
|
|
<b>{row.meta}</b>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</article>
|
|
|
|
<article className="admin-panel">
|
|
<PanelHead title="Acciones disponibles" meta={`${actions.length} acciones`} />
|
|
<div className="admin-action-list">
|
|
{actions.map((action) => (
|
|
<button key={action.title} type="button" className={action.tone || "primary"} onClick={() => recordAction(`${action.title}: ${action.detail}`)}>
|
|
<strong>{action.title}</strong>
|
|
<span>{action.detail}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</article>
|
|
</section>
|
|
|
|
<section className="admin-grid admin-section-grid">
|
|
{section === "makers" ? (
|
|
<MakerCard maker={selectedMaker} makers={makers} setSelectedMakerId={() => undefined} suspendMaker={suspendMaker} recordAction={recordAction} />
|
|
) : section === "reviews" ? (
|
|
<ReviewModerationCard decision="Mantener resena" setDecision={() => undefined} recordAction={recordAction} />
|
|
) : section === "payments" || section === "subscriptions" || section === "billing" ? (
|
|
<SubscriptionCard recordAction={recordAction} />
|
|
) : section === "system" || section === "integrations" ? (
|
|
<SystemCard />
|
|
) : (
|
|
<article className="admin-panel">
|
|
<PanelHead title="Resumen operativo" meta="Control" />
|
|
<div className="admin-insight-grid">
|
|
<Metric value="96,4%" label="Servicio operativo" tone="success" />
|
|
<Metric value="2 h 15" label="Respuesta promedio" tone="primary" />
|
|
<Metric value="38,9%" label="Conversion estimada" tone="success" />
|
|
<Metric value="Bajo" label="Riesgo actual" tone="success" />
|
|
</div>
|
|
</article>
|
|
)}
|
|
|
|
<article className="admin-panel">
|
|
<PanelHead title="Auditoria reciente" meta="Ver auditoria" />
|
|
<div className="admin-audit-list">
|
|
{activeActionLog.map((entry) => (
|
|
<div key={entry}>
|
|
<span className="admin-dot primary" />
|
|
<p>{entry}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</article>
|
|
</section>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function KpiCard({ number, title, detail, tone }: { number: string; title: string; detail: string; tone: string }) {
|
|
return (
|
|
<article className={`admin-kpi ${tone}`}>
|
|
<span className="admin-kpi-icon" />
|
|
<strong>{number}</strong>
|
|
<div>
|
|
<b>{title}</b>
|
|
<small>{detail}</small>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
function PanelHead({ title, meta }: { title: string; meta?: string }) {
|
|
return (
|
|
<div className="admin-panel-head">
|
|
<h2>{title}</h2>
|
|
{meta ? <button type="button">{meta}</button> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function QueueTable({ rows, selectedCaseId, onSelect }: { rows: AdminQueueItem[]; selectedCaseId: string; onSelect: (id: string) => void }) {
|
|
return (
|
|
<div className="admin-table">
|
|
<div className="admin-table-head">
|
|
<span>Prioridad</span>
|
|
<span>Caso</span>
|
|
<span>Tipo</span>
|
|
<span>Usuario</span>
|
|
<span>Estado</span>
|
|
<span>Responsable</span>
|
|
<span>Espera</span>
|
|
</div>
|
|
{rows.map((row) => (
|
|
<button key={`${row.id}-${row.kind}`} type="button" className={selectedCaseId === row.id ? "active" : ""} onClick={() => onSelect(row.id)}>
|
|
<span><i className={`admin-priority ${row.priority}`} /></span>
|
|
<strong>{row.id}</strong>
|
|
<span>{row.kind}</span>
|
|
<span>{row.user}</span>
|
|
<span>{row.state}</span>
|
|
<span>{row.owner}</span>
|
|
<span>{row.wait}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SystemCard() {
|
|
return (
|
|
<article className="admin-panel">
|
|
<PanelHead title="Estado del sistema" meta="Ver detalles tecnicos" />
|
|
<div className="admin-system-list">
|
|
{systemHealthRows.map(([label, state, tone]) => (
|
|
<div key={label}>
|
|
<span>{label}</span>
|
|
<b className={tone}>{state}</b>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
function MakerCard({
|
|
maker,
|
|
makers,
|
|
setSelectedMakerId,
|
|
suspendMaker,
|
|
recordAction
|
|
}: {
|
|
maker?: Record<string, unknown>;
|
|
makers: Array<Record<string, unknown>>;
|
|
setSelectedMakerId: (id: string) => void;
|
|
suspendMaker: (id: string) => void;
|
|
recordAction: (message: string) => void;
|
|
}) {
|
|
const makerId = String(maker?.id || makers[0]?.id || "");
|
|
const name = toText(maker?.business_name, "MakerLab 3D");
|
|
const status = translateStatus(toText(maker?.status, "published"));
|
|
|
|
return (
|
|
<article className="admin-panel admin-maker-card">
|
|
<PanelHead title="Ficha de Maker" meta="Volver" />
|
|
<div className="admin-maker-head">
|
|
<span className="admin-maker-avatar">{initials(name)}</span>
|
|
<div>
|
|
<h3>{name}</h3>
|
|
<p>Plan Maker activo</p>
|
|
<div className="admin-chip-row">
|
|
<span>{status}</span>
|
|
<span>{translateAvailability(toText(maker?.availability, "available"))}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<select value={makerId} onChange={(event) => setSelectedMakerId(event.target.value)} className="admin-select">
|
|
{makers.map((item) => (
|
|
<option key={String(item.id)} value={String(item.id)}>
|
|
{toText(item.business_name) || toText(item.email, "Maker")}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
<div className="admin-tabs compact">
|
|
<button type="button" className="active">Resumen</button>
|
|
<button type="button" onClick={() => recordAction("Pestana Perfil abierta.")}>Perfil</button>
|
|
<button type="button" onClick={() => recordAction("Trabajos del maker revisados.")}>Trabajos</button>
|
|
<button type="button" onClick={() => recordAction("Consultas del maker revisadas.")}>Consultas</button>
|
|
<button type="button" onClick={() => recordAction("Reputacion del maker revisada.")}>Reputacion</button>
|
|
</div>
|
|
|
|
<div className="admin-insight-grid">
|
|
<Metric value="18" label="Consultas recibidas" tone="success" />
|
|
<Metric value="7" label="Encargos acordados" tone="primary" />
|
|
<Metric value="2 h 15" label="Respuesta promedio" tone="warning" />
|
|
<Metric value="38,9%" label="Tasa conversion" tone="success" />
|
|
</div>
|
|
|
|
<div className="admin-maker-status">
|
|
<div>
|
|
<span>Reputacion</span>
|
|
<strong>4,8</strong>
|
|
<small>*****</small>
|
|
</div>
|
|
<div>
|
|
<span>Incidencias abiertas</span>
|
|
<strong>1</strong>
|
|
<small>Revision activa</small>
|
|
</div>
|
|
<div>
|
|
<span>Riesgo</span>
|
|
<strong>Bajo</strong>
|
|
<small>Sin bloqueos</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="admin-inline-actions">
|
|
<button type="button" onClick={() => recordAction(`Perfil publico de ${name} abierto.`)}>Ver perfil publico</button>
|
|
<button type="button" onClick={() => recordAction(`${name} visto como cliente.`)}>Ver como cliente</button>
|
|
<button type="button" className="danger" disabled={!makerId} onClick={() => suspendMaker(makerId)}>Suspender</button>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
function ReviewModerationCard({
|
|
decision,
|
|
setDecision,
|
|
recordAction
|
|
}: {
|
|
decision: string;
|
|
setDecision: (decision: string) => void;
|
|
recordAction: (message: string) => void;
|
|
}) {
|
|
return (
|
|
<article className="admin-panel admin-review-card">
|
|
<PanelHead title="Revision de resena" meta="Resena #R-832" />
|
|
<div className="admin-review-head">
|
|
<span className="admin-avatar">JP</span>
|
|
<div>
|
|
<strong>Juan Perez</strong>
|
|
<small>Cliente verificado</small>
|
|
</div>
|
|
<span className="admin-badge danger">Critica</span>
|
|
</div>
|
|
<p>"El trabajo llego mal impreso, piezas rotas y el Maker no quiere hacerse cargo. Pesima experiencia."</p>
|
|
<div className="admin-evidence-strip">
|
|
<img src="/demo/feed-print-technical.svg" alt="" />
|
|
<img src="/demo/work-dashboard-bracket.svg" alt="" />
|
|
<img src="/demo/feed-print-gear.svg" alt="" />
|
|
</div>
|
|
<div className="admin-tabs compact">
|
|
<button type="button" className="active">Contexto</button>
|
|
<button type="button">Conversacion</button>
|
|
<button type="button">Evidencias</button>
|
|
<button type="button">Historial</button>
|
|
</div>
|
|
<div className="admin-review-decision">
|
|
<strong>Decision</strong>
|
|
{["Mantener resena", "Solicitar edicion", "Ocultar datos personales", "Invalidar resena", "Escalar caso"].map((item) => (
|
|
<label key={item}>
|
|
<input type="radio" checked={decision === item} onChange={() => setDecision(item)} />
|
|
<span>{item}</span>
|
|
</label>
|
|
))}
|
|
<textarea placeholder="Motivo obligatorio para auditoria" defaultValue="La resena contiene afirmaciones fuertes y debe quedar respaldada por evidencia antes de afectar la reputacion." />
|
|
<button type="button" onClick={() => recordAction(`Decision guardada: ${decision}.`)}>Guardar decision</button>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
function IncidentCard({
|
|
state,
|
|
setState,
|
|
owner,
|
|
setOwner,
|
|
recordAction
|
|
}: {
|
|
state: string;
|
|
setState: (state: string) => void;
|
|
owner: string;
|
|
setOwner: (owner: string) => void;
|
|
recordAction: (message: string) => void;
|
|
}) {
|
|
return (
|
|
<article className="admin-panel admin-incident-card">
|
|
<PanelHead title="Incidencia #I-048" meta="Alta" />
|
|
<div className="admin-incident-meta">
|
|
<span>Cliente <b>Juan Perez</b></span>
|
|
<span>Maker <b>MakerLab 3D</b></span>
|
|
<span>Encargo <b>#E-2841</b></span>
|
|
</div>
|
|
<div className="admin-form-row">
|
|
<label>
|
|
Estado
|
|
<select value={state} onChange={(event) => setState(event.target.value)}>
|
|
<option>En mediacion</option>
|
|
<option>Esperando cliente</option>
|
|
<option>Esperando maker</option>
|
|
<option>Resuelta</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Responsable
|
|
<select value={owner} onChange={(event) => setOwner(event.target.value)}>
|
|
<option>Maria Lopez</option>
|
|
<option>Pablo Dibene</option>
|
|
<option>Sin asignar</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
<div className="admin-tabs compact">
|
|
<button type="button" className="active">Resumen</button>
|
|
<button type="button">Conversacion</button>
|
|
<button type="button">Evidencias</button>
|
|
<button type="button">Cronologia</button>
|
|
</div>
|
|
<p>El cliente reclama que las piezas llegaron rotas y con defectos de impresion. Solicita reposicion del trabajo.</p>
|
|
<div className="admin-inline-actions vertical">
|
|
<button type="button" onClick={() => recordAction("Se solicito informacion al cliente.")}>Solicitar informacion al cliente</button>
|
|
<button type="button" onClick={() => recordAction("Se solicito informacion al maker.")}>Solicitar informacion al Maker</button>
|
|
<button type="button" onClick={() => recordAction("Acuerdo registrado entre partes.")}>Registrar acuerdo entre partes</button>
|
|
<button type="button" onClick={() => recordAction("Incidencia resuelta.")}>Resolver incidencia</button>
|
|
<button type="button" className="danger" onClick={() => recordAction("Incidencia escalada.")}>Escalar caso</button>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
function SubscriptionCard({ recordAction }: { recordAction: (message: string) => void }) {
|
|
return (
|
|
<article className="admin-panel admin-subscription-card">
|
|
<PanelHead title="Suscripcion y pagos" meta="Activo" />
|
|
<h3>MakerLab 3D</h3>
|
|
<strong>ARS $19.990 / mes</strong>
|
|
<div className="admin-payment-meta">
|
|
<span>Proxima renovacion <b>10 de agosto de 2026</b></span>
|
|
<span>Metodo de pago <b>Visa terminada en 4242</b></span>
|
|
</div>
|
|
<div className="admin-tabs compact">
|
|
<button type="button" className="active">Historial</button>
|
|
<button type="button">Pagos</button>
|
|
<button type="button">Facturas</button>
|
|
<button type="button">Promociones</button>
|
|
</div>
|
|
<div className="admin-payment-table">
|
|
{paymentHistory.map(([date, event, amount, state]) => (
|
|
<div key={`${date}-${event}`}>
|
|
<span>{date}</span>
|
|
<strong>{event}</strong>
|
|
<span>{amount}</span>
|
|
<b className={state === "Pagado" ? "success" : "danger"}>{state}</b>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="admin-inline-actions">
|
|
<button type="button" onClick={() => recordAction("Reintento de cobro solicitado.")}>Reintentar cobro</button>
|
|
<button type="button" onClick={() => recordAction("Credito aplicado al maker.")}>Aplicar credito</button>
|
|
<button type="button" onClick={() => recordAction("Extension de gracia agregada.")}>Anadir extension</button>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
function MiniQueue({ title, rows, danger = false }: { title: string; rows: string[]; danger?: boolean }) {
|
|
return (
|
|
<article className="admin-panel admin-mini-queue">
|
|
<PanelHead title={title} meta="Ver todos" />
|
|
{rows.map((row, index) => (
|
|
<div key={`${title}-${row}-${index}`}>
|
|
<strong>{index % 2 === 0 ? "#T-" : "#R-"}{191 - index}</strong>
|
|
<span>{row}</span>
|
|
<b className={danger || index === 2 ? "danger" : index === 1 ? "warning" : "normal"}>{danger ? "Critica" : index === 2 ? "Alta" : "Normal"}</b>
|
|
</div>
|
|
))}
|
|
</article>
|
|
);
|
|
}
|
|
|
|
function Metric({ value, label, tone }: { value: string; label: string; tone: string }) {
|
|
return (
|
|
<div className={`admin-metric ${tone}`}>
|
|
<strong>{value}</strong>
|
|
<span>{label}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function getCatalogRows(section: AdminSection) {
|
|
const defaults = [
|
|
{ title: "Impresion 3D FDM", detail: "PLA, PETG, ABS, ASA", meta: "Activo", tone: "success" },
|
|
{ title: "Diseno 3D / CAD", detail: "Modelado y preparacion", meta: "Activo", tone: "primary" },
|
|
{ title: "Escaneo 3D", detail: "Objetos hasta 50 cm", meta: "Pausado", tone: "warning" },
|
|
{ title: "Produccion en serie", detail: "FDM hasta 200 uds", meta: "Incompleto", tone: "danger" }
|
|
];
|
|
|
|
if (section.includes("analytics")) {
|
|
return [
|
|
{ title: "Busqueda a consulta", detail: "128 eventos medidos", meta: "+18%", tone: "success" },
|
|
{ title: "Consulta a acuerdo", detail: "38,9% conversion", meta: "+2,1%", tone: "success" },
|
|
{ title: "Tiempo de respuesta", detail: "2 h 15 promedio", meta: "Bueno", tone: "primary" },
|
|
{ title: "Riesgo operativo", detail: "3 casos criticos", meta: "Atender", tone: "danger" }
|
|
];
|
|
}
|
|
|
|
if (["roles", "settings", "integrations", "templates", "audit", "system"].includes(section)) {
|
|
return [
|
|
{ title: "Permisos internos", detail: "Admin, moderacion y soporte", meta: "Seguro", tone: "success" },
|
|
{ title: "Email transaccional", detail: "Confirmaciones y alertas", meta: "Degradado", tone: "warning" },
|
|
{ title: "Mapas", detail: "Tiles y geocodificacion", meta: "Operativo", tone: "success" },
|
|
{ title: "Auditoria", detail: "Eventos sensibles", meta: "Activa", tone: "primary" }
|
|
];
|
|
}
|
|
|
|
return defaults;
|
|
}
|
|
|
|
function initials(value: string) {
|
|
return value
|
|
.split(/\s+/)
|
|
.filter(Boolean)
|
|
.slice(0, 2)
|
|
.map((part) => part[0]?.toUpperCase())
|
|
.join("") || "M3";
|
|
}
|
|
|
|
function translateStatus(value: string) {
|
|
const map: Record<string, string> = {
|
|
published: "Perfil publicado",
|
|
draft: "Borrador",
|
|
suspended: "Suspendido",
|
|
active: "Activo"
|
|
};
|
|
return map[value] || value || "Activo";
|
|
}
|
|
|
|
function translateAvailability(value: string) {
|
|
const map: Record<string, string> = {
|
|
available: "Acepta trabajos",
|
|
limited: "Disponibilidad limitada",
|
|
unavailable: "No acepta trabajos"
|
|
};
|
|
return map[value] || value || "Acepta trabajos";
|
|
}
|