Dashboard
Panel web para administradores y clientes.
Acceso
Login
El dashboard acepta tanto el admin token como una client API key:
flowchart TD
LOGIN["Pantalla de login"] --> ENTER["Ingresar API key"]
ENTER --> VALIDATE["GET /oauth/validate"]
VALIDATE --> TYPE{type?}
TYPE -->|admin| ADMIN["Dashboard Admin<br/>Todas las tabs"]
TYPE -->|client| CLIENT["Dashboard Cliente<br/>Tabs limitadas"]
Tabs disponibles
| Tab | Admin | Client | Descripción |
|---|---|---|---|
| Overview | ✅ | ✅ | Estado OAuth, MP User ID, expiración del token |
| Sucursales | ✅ | ✅ | Lista de stores con POS |
| Terminales | ✅ | ✅ | Terminales con sucursal, caja y modo |
| Órdenes | ✅ | ✅ | Buscar orden por ordenId |
| Webhooks | ✅ | ✅ | Últimos webhook logs |
| API Keys | ✅ | ❌ | Crear, listar y revocar keys |
| Clientes | ✅ | ❌ | Info completa: OAuth + API key + tokens |
Tecnología
No es React
El dashboard usa hono/jsx/dom, no React. Los imports son diferentes:
| Tecnología | Uso |
|---|---|
hono/jsx/dom |
Rendering |
hono/client (hc) |
Llamadas tipadas a la API |
InferResponseType |
Tipos de respuesta inferidos |
| Tailwind CSS v4 | Estilos |
Estructura de archivos
src/
├── client.tsx # Mount point: render(<App />)
├── app.tsx # Layout + tabs + routing
├── components/
│ ├── StatusBadge.tsx # Badge de estado con colores
│ ├── LoginScreen.tsx # Pantalla de login
│ ├── Overview.tsx # Estado OAuth
│ ├── Stores.tsx # Sucursales + POS
│ ├── Terminals.tsx # Terminales
│ ├── Orders.tsx # Consultar orden
│ ├── ApiKeysPanel.tsx # CRUD API keys (admin)
│ ├── ClientInfo.tsx # Info del cliente (admin)
│ └── Webhooks.tsx # Webhook logs
└── lib/
└── api.ts # createClient(token) con hc<AppType>
Patrón de componente
import type { InferResponseType } from 'hono/client';
import { useEffect, useState } from 'hono/jsx';
import { createClient } from '../lib/api';
export function MiComponente({ token }: { token: string }) {
const client = createClient(token);
// ⚠️ Segundo arg 200 para filtrar solo respuesta exitosa
type Response = InferResponseType<typeof client.mi.ruta.$get, 200>;
const [data, setData] = useState<Response | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const load = async () => {
try {
const res = await client.mi.ruta.$get();
setData(await res.json());
} catch (e) {
console.error('[MiComponente] Failed:', e);
}
setLoading(false);
};
load();
}, [token]);
if (loading) return <div class="text-center py-10 text-slate-500">Cargando...</div>;
// ... render con clases Tailwind
}