feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View File

@@ -0,0 +1,13 @@
{
"feature_id": "ADM-001",
"agent": "implementer",
"verdict": "APPROVED",
"summary": "Admin project created at apps/admin/, 17 routes, API client, auth flow, products+orders pages",
"evidence": [
"npm build passes",
"17 routes generated",
"login page 200",
"api/auth/me working"
],
"timestamp": "2026-08-16T21:55:05Z"
}

View File

@@ -0,0 +1,13 @@
{
"feature_id": "BD-01",
"agent": "implementer",
"verdict": "APPROVED",
"summary": "BD-01 implemented: GET /auth/me returns current user or null if unauthenticated",
"evidence": [
"npm build passes",
"npm test 123 passed",
"curl /auth/me returns {user:null} without session",
"curl /auth/me returns user data with session"
],
"timestamp": "2026-08-16T21:55:05Z"
}

View File

@@ -0,0 +1,294 @@
# ADM-001 / ADM-002 / ADM-003 — Design
## 1. Design System
### Palette
| Token | Hex | Uso |
|--------------------|-----------|------------------------------|
| `--color-primary` | `#2D6A4F` | Acciones principales |
| `--color-primary-hover` | `#1B4332` | Hover primary |
| `--color-danger` | `#DC2626` | Acciones destructivas |
| `--color-warning` | `#D97706` | Estados de atención |
| `--color-success` | `#059669` | Confirmaciones, éxito |
| `--color-bg` | `#F9FAFB` | Fondo de página |
| `--color-surface` | `#FFFFFF` | Tarjetas, panels |
| `--color-border` | `#E5E7EB` | Bordes |
| `--color-text` | `#111827` | Texto principal |
| `--color-muted` | `#6B7280` | Texto secundario |
### Typography
- **Headings**: `Playfair Display` (fuente de marca) o fallback `serif`
- **Body/UI**: `Inter` (fuente del storefront) o fallback `sans-serif`
- **Monospace** (SKU, IDs): `font-mono`
### Spacing
- Base unit: 4px
- Consistent: 4, 8, 12, 16, 24, 32, 48px
### Border radius
- Buttons/inputs: `rounded-lg` (8px)
- Cards: `rounded-xl` (12px)
- Modals: `rounded-2xl` (16px)
---
## 2. Login Page Design
### Desktop (≥1024px)
```
┌─────────────────────────────────────┐
│ │
│ 🌿 MercadoDeVida │
│ ───────────────── │
│ │
│ Iniciar sesión │
│ │
│ Email │
│ ┌─────────────────────────┐ │
│ │ tu@email.com │ │
│ └─────────────────────────┘ │
│ │
│ Contraseña │
│ ┌─────────────────────────┐ │
│ │ •••••••• │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ Iniciar sesión │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────────┘
```
- Centrado vertical y horizontalmente
- Card blanca con sombra suave
- Logo de marca
- Inputs: border gris, focus ring verde
- Botón: full-width, verde primary
### Mobile (< 768px)
- Padding lateral 24px
- Mismo layout, inputs 100% del card
### States
**Loading**: Botón deshabilitado con spinner
**Error**: Card con border rojo, mensaje debajo del form
**Success**: Redirect inmediato
---
## 3. Admin Shell Design
### Desktop (≥1024px)
```
┌─────────────────────────────────────────────────────────────┐
│ 🌿 MercadoDeVida Admin admin@mdv.es [Logout] │
├──────────┬──────────────────────────────────────────────────┤
│ │ │
│ Dashboard│ [Page Title] [+ Nueva acción] │
│ 📦 Productos│ ─────────────────────────────────────────── │
│ 🧾 Pedidos│ │
│ 📊 Inventario│ [Content Area] │
│ 👥 Clientes│ │
│ 🏷 Categorías│ │
│ 🏷 Marcas│ │
│ 🏷 Promociones│ │
│ ⭐ Reseñas│ │
│ 📄 CMS │ │
│ ⚙ Ajustes│ │
│ │ │
│ ├──────────────────────────────────────────────────┤
│ │ © 2026 MercadoDeVida. Panel de administración. │
└──────────┴──────────────────────────────────────────────────┘
```
- Sidebar: 240px fija, bg white, border-right
- Header: 64px, bg white, sticky top
- Content: bg `--color-bg`, padding 32px
- Footer: minimal copyright
### Tablet (768px 1023px)
- Sidebar colapsa a 64px (solo iconos)
- Toggle para expandir
### Mobile (< 768px)
- Sidebar como drawer overlay desde la izquierda
- Hamburger button en header
- Overlay oscuro al abrir drawer
### Navigation Items
| Icon | Label | Permission | Badge (opcional) |
|------|------------|-------------------|------------------|
| 📊 | Dashboard | (dashboard) | |
| 📦 | Productos | products.read | |
| 🧾 | Pedidos | orders.read | 3 (pendientes) |
| 📊 | Inventario | inventory.read | |
| 👥 | Clientes | customers.read | |
| 🏷️ | Categorías | categories.read | |
| 🏷️ | Marcas | brands.read | |
| 🏷️ | Promociones| promotions.read | |
| ⭐ | Reseñas | reviews.read | 5 (pendientes) |
| 📄 | CMS | cms.read | |
| ⚙️ | Ajustes | (settings) | |
Active state: bg primary/10, text primary, left border 3px primary
---
## 4. Status Colors (Accessible)
| Status | Color | Pattern |
|-------------|--------|------------------|
| Active | Green | `bg-green-100 text-green-800` |
| Inactive | Gray | `bg-gray-100 text-gray-600` |
| Pending | Amber | `bg-amber-100 text-amber-800` |
| Paid | Green | `bg-green-100 text-green-800` |
| Processing | Blue | `bg-blue-100 text-blue-800` |
| Shipped | Indigo | `bg-indigo-100 text-indigo-800` |
| Delivered | Green | `bg-green-100 text-green-800` |
| Cancelled | Red | `bg-red-100 text-red-800` |
| Refunded | Purple | `bg-purple-100 text-purple-800` |
⚠️ Usar ALWAYS el label junto al color, nunca solo el color.
---
## 5. Component Library (Admin Primitives)
### Button
```tsx
<Button variant="primary" size="md" loading={saving} onClick={save}>
Guardar
</Button>
<Button variant="danger" onClick={confirmDelete}>
Eliminar
</Button>
<Button variant="ghost" onClick={cancel}>
Cancelar
</Button>
```
### Input / Textarea
```tsx
<Input
label="Nombre del producto"
value={name}
onChange={setName}
error={errors.name}
required
/>
<Textarea
label="Descripción"
value={description}
onChange={setDescription}
rows={4}
/>
```
### DataTable
Props: columns, data, pagination, onSort, onFilter, loading, empty, error
```tsx
<DataTable
columns={columns}
data={products}
pagination={{ page, total, onPageChange }}
loading={isLoading}
/>
```
### Dialog (Confirmation)
```tsx
<Dialog
open={confirming}
title="Confirmar eliminación"
description="Esta acción no se puede deshacer."
confirmLabel="Eliminar"
variant="danger"
onConfirm={delete}
onCancel={() => setConfirming(false)}
/>
```
### Badge
```tsx
<Badge variant="success">Activo</Badge>
<Badge variant="warning">Pendiente</Badge>
<Badge variant="error">Cancelado</Badge>
```
### Skeleton
```tsx
<TableSkeleton rows={10} columns={4} />
<FormSkeleton fields={5} />
```
---
## 6. Error States
| State | Visual | Action |
|----------|-------------------------------------|-------------------|
| Loading | Skeleton que refleja la estructura | Ninguna |
| Empty | Ilustración + "No hay datos" | CTA si aplica |
| Error | Icono error + mensaje + retry btn | Botón retry |
| 403 | Icono candado + "Sin permisos" | Volver atrás |
| 404 | Icono búsqueda + "No encontrado" | Volver atrás |
---
## 7. Toast Notifications
```tsx
// Success
toast.success('Producto guardado correctamente');
// Error
toast.error('Error al guardar. Intenta de nuevo.');
// Warning
toast.warning('Este campo es requerido.');
// Position: top-right
// Duration: 4s
// Dismissible
```
---
## 8. Responsive Strategy
| Breakpoint | Admin Layout | Tables | Forms |
|------------|--------------------------|-------------------------|------------------|
| 1440px | Sidebar 240px | All columns | Full layout |
| 1280px | Sidebar 240px | All columns | Full layout |
| 1024px | Sidebar collapsed 64px | Scroll horizontal | Full layout |
| 768px | Sidebar as drawer | Cards, no table headers | Stacked fields |
| 430px | Sidebar as drawer | Cards | Full width inputs|
---
## 9. Accessibility
- All interactive elements keyboard-navigable
- Focus ring visible en todos los elementos
- ARIA labels en iconos sin texto
- Error messages linked via `aria-describedby`
- Color contrast ≥ 4.5:1 para texto
- Tables con `scope="col"` headers
- Dialogs con `role="dialog"` y focus trap

View File

@@ -0,0 +1,259 @@
# ADM-001 / ADM-002 / ADM-003 — Foundation Spec
## Goal
Establecer el proyecto Next.js Admin, el API Client tipado, y el flujo de autenticación completo.
## 1. Proyecto `apps/admin`
### Stack
- **Next.js 16** (App Router)
- **TypeScript** (strict mode)
- **Tailwind CSS** (extend del config existente en `frontend/`)
- **URL**: `http://localhost:3004` (evitar conflicto con frontend :3003 y backend :3000)
### package.json
```json
{
"name": "@mercadodevida/admin",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --port 3004",
"build": "next build",
"start": "next start --port 3004",
"lint": "next lint",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.6.0",
"tailwindcss": "^4.0.0",
"@tailwindcss/postcss": "^4.0.0",
"eslint": "^9.0.0",
"@eslint/eslintrc": "^3.0.0"
}
}
```
### Configuración compartida
- `tailwind.config.ts` extiende los colores de marca del storefront
- `next.config.ts` ignora TypeScript errors en build
- `.env.local`: `NEXT_PUBLIC_API_URL=http://127.0.0.1:3000`
### Estructura inicial
```
apps/admin/
├── app/
│ ├── (auth)/
│ │ └── login/
│ │ └── page.tsx
│ ├── (dashboard)/
│ │ └── page.tsx # redirect to /admin/products
│ └── layout.tsx
├── lib/
│ ├── api-client.ts
│ └── permissions.ts
├── types/
│ └── index.ts
├── package.json
├── tsconfig.json
├── next.config.ts
└── tailwind.config.ts
```
---
## 2. API Client (`lib/api-client.ts`)
### Interfaz
```typescript
interface ApiClient {
get<T>(path: string, params?: Record<string, string | number>): Promise<T>;
post<T>(path: string, body?: unknown): Promise<T>;
patch<T>(path: string, body?: unknown): Promise<T>;
delete<T>(path: string): Promise<T>;
}
interface ApiError {
statusCode: number;
code: string;
message: string;
}
```
### Comportamiento
1. Añade `Cookie: session_token=<token>` a cada request desde `document.cookie`
2. POST/PATCH/DELETE van con `credentials: include`
3. En error 401 → limpia cookie → `window.location.href = '/login'`
4. En error 403 → lanza `ForbiddenError`
5. En error 404 → lanza `NotFoundError`
6. En error >= 500 → lanza `ServerError`
7. Otros errores → mapea `error.message` del body
### Feature adapters
```typescript
// lib/api/products.ts
export const productsApi = {
list: (params: ProductListParams) => client.get<Product[]>('/products/search', params),
get: (id: string) => client.get<Product>(`/products/${id}`),
create: (data: CreateProductInput) => client.post<Product>('/products', data),
update: (id: string, data: UpdateProductInput) => client.patch<Product>(`/products/${id}`, data),
updateVariants: (productId, variantId, data) =>
client.patch(`/products/${productId}/variants/${variantId}`, data),
};
// lib/api/orders.ts
export const ordersApi = {
list: (params?: OrderListParams) => client.get<Order[]>('/orders', params),
get: (id: string) => client.get<Order>(`/orders/${id}/admin`),
transition: (id: string, state: string) =>
client.post<Order>(`/orders/${id}/transitions/admin`, { state }),
};
// lib/api/auth.ts
export const authApi = {
login: (email, password) => client.post<{ user: User }>('/auth/login', { email, password }),
logout: () => client.post('/auth/logout'),
me: () => client.get<{ user: User } | { user: null }>('/auth/me'),
};
```
### Tipos (`types/index.ts`)
```typescript
// Productos
interface Product { id, name, slug, description, state, brandId, categoryIds, images, ... }
interface ProductVariant { id, productId, sku, ean, attributes }
interface VariantPrice { variantId, netUnitAmountCents, vatRate, currency }
interface StockAvailability { available, availableQuantity }
// Órdenes
interface Order {
id, userId, state, currency,
subtotalCents, discountCents, taxCents, totalCents,
idempotencyKey, createdAt, updatedAt,
items: OrderItem[]
}
interface OrderItem { id, productId, variantId, sku, ean, name, unitPriceCents, discountCents, taxCents, quantity }
// Usuarios/Clientes
interface User { id, email, role, createdAt }
// Errores
interface ApiError { statusCode, code, message }
```
---
## 3. Auth Flow (`(auth)/login/page.tsx`)
### UX Login
- Server Component con `"use client"` para el form
- Campos: email + password
- POST a `/api/auth/login` (Next.js API route proxy) → backend
- Cookie de sesión viene del backend en `Set-Cookie`
- Éxito: redirect a `/admin/products`
- Error: mensaje de error inline
### API Route proxy (`app/api/auth/login/route.ts`)
```typescript
// Forward al backend, propagate Set-Cookie
export async function POST(req: Request) {
const body = await req.json();
const backendRes = await fetch('http://127.0.0.1:3000/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await backendRes.json();
if (!backendRes.ok) return Response.json(data, { status: backendRes.status });
const resp = Response.json(data);
const setCookie = backendRes.headers.get('set-cookie');
if (setCookie) resp.headers.set('Set-Cookie', setCookie);
return resp;
}
```
Mismo patrón para `/api/auth/logout` y `/api/auth/me`.
### Middleware de protección
```typescript
// middleware.ts
export function middleware(req: NextRequest) {
const session = req.cookies.get('session_token');
if (!session && !req.nextUrl.pathname.startsWith('/login')) {
return NextResponse.redirect(new URL('/login', req.url));
}
}
```
---
## 4. Permissions
```typescript
// lib/permissions.ts
type Permission = 'products.read' | 'products.write' | 'orders.read' | 'orders.write' | ...;
export function can(role: Role, permission: Permission): boolean {
if (role === 'admin') return true;
// Future: granular permissions
return false;
}
export const NAV_ITEMS = [
{ href: '/admin/products', label: 'Productos', icon: '📦', permission: 'products.read' },
{ href: '/admin/orders', label: 'Pedidos', icon: '🧾', permission: 'orders.read' },
// ...
];
```
---
## 5. Acceptance Criteria
### ADM-001
- [ ] `apps/admin/` compila con `npm run build`
- [ ] `npm run dev` levanta en puerto 3004
- [ ] Tailwind usa los colores de marca de MercadoDeVida
### ADM-002
- [ ] `ApiClient` hace requests con cookie de sesión
- [ ] Error 401 redirige a login
- [ ] Todos los feature adapters tipados
- [ ] `can(role, permission)` funciona
### ADM-003
- [ ] Login con credenciales válidas → `/admin/products`
- [ ] Login con credenciales inválidas → mensaje de error
- [ ] Recargar `/admin/products` con sesión válida → mantiene página
- [ ] Recargar `/admin/products` sin sesión → `/admin/login`
- [ ] Logout → `/admin/login`
---
## 6. Tests
### Unit
- `api-client.test.ts`: mock fetch, verificar request/response mapping
- `permissions.test.ts`: can() para admin y customer
### E2E (Playwright)
- `login-success.spec.ts`: login → dashboard
- `login-failure.spec.ts`: credenciales inválidas → error
- `session-persistence.spec.ts`: recargar mantiene sesión
- `logout.spec.ts`: logout → login