feat(ADM-018): completed feature
This commit is contained in:
@@ -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"
|
||||
}
|
||||
13
project/specs-admin/000-foundation/BD-01-auth-me.json
Normal file
13
project/specs-admin/000-foundation/BD-01-auth-me.json
Normal 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"
|
||||
}
|
||||
294
project/specs-admin/000-foundation/DESIGN.md
Normal file
294
project/specs-admin/000-foundation/DESIGN.md
Normal 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
|
||||
259
project/specs-admin/000-foundation/SPEC.md
Normal file
259
project/specs-admin/000-foundation/SPEC.md
Normal 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
|
||||
295
project/specs-admin/050-orders/SPEC.md
Normal file
295
project/specs-admin/050-orders/SPEC.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# ADM-013 / ADM-014 / ADM-015 — Orders Spec
|
||||
|
||||
## Goal
|
||||
|
||||
Gestión completa de pedidos: listado, detalle, y transiciones de estado.
|
||||
|
||||
---
|
||||
|
||||
## 1. Order List (`/admin/orders`)
|
||||
|
||||
### API Contract
|
||||
|
||||
```
|
||||
GET /orders
|
||||
Authorization: session_token (admin role)
|
||||
|
||||
Query params:
|
||||
?offset=0&limit=20
|
||||
?state=PENDING
|
||||
?q=maria@ejemplo.com (búsqueda por email de cliente)
|
||||
|
||||
Response: OrderSummary[]
|
||||
```
|
||||
|
||||
```typescript
|
||||
interface OrderSummary {
|
||||
id: string;
|
||||
userId: string;
|
||||
state: OrderState;
|
||||
totalCents: number;
|
||||
currency: 'EUR';
|
||||
itemCount: number;
|
||||
createdAt: string; // ISO
|
||||
}
|
||||
```
|
||||
|
||||
### Desktop UX
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Pedidos [Estado ▼] [Búsqueda: ________] [🔍] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ #ID Cliente Total Estado Fecha │
|
||||
│ ──────────────────────────────────────────────────────────────── │
|
||||
│ abc-123 maria@email.com €45.83 ● Paid 15 ago 2026 │
|
||||
│ def-456 juan@email.com €23.10 ● Pending 14 ago 2026 │
|
||||
│ ghi-789 ana@email.com €89.00 ● Shipped 10 ago 2026 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ [← Anterior] Página 1 de 5 [Siguiente →] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Filters
|
||||
|
||||
- **Estado**: PENDING, AWAITING_PAYMENT, PAID, PROCESSING, SHIPPED, DELIVERED, CANCELLED, REFUNDED, PARTIALLY_REFUNDED
|
||||
- **Búsqueda**: por email de cliente (o ID)
|
||||
- Estado por defecto: todos
|
||||
|
||||
### Responsive
|
||||
|
||||
Mobile: Cards en lugar de tabla
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ #abc-123 │
|
||||
│ maria@email.com │
|
||||
│ €45.83 · ● Paid │
|
||||
│ 15 ago 2026 │
|
||||
│ [Ver detalle →] │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Order Detail (`/admin/orders/[id]`)
|
||||
|
||||
### API Contract
|
||||
|
||||
```
|
||||
GET /orders/:id/admin
|
||||
Authorization: session_token (admin role)
|
||||
|
||||
Response: Order (full)
|
||||
```
|
||||
|
||||
```typescript
|
||||
interface Order {
|
||||
id: string;
|
||||
userId: string;
|
||||
state: OrderState;
|
||||
subtotalCents: number;
|
||||
discountCents: number;
|
||||
taxCents: number;
|
||||
totalCents: number;
|
||||
currency: 'EUR';
|
||||
idempotencyKey: string | null;
|
||||
items: OrderItem[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface OrderItem {
|
||||
id: string;
|
||||
productId: string;
|
||||
variantId: string;
|
||||
sku: string;
|
||||
ean: string | null;
|
||||
name: string;
|
||||
unitPriceCents: number;
|
||||
discountCents: number;
|
||||
taxCents: number;
|
||||
quantity: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Desktop UX — Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ← Volver a pedidos │
|
||||
├───────────────────────────────────┬─────────────────────────────┤
|
||||
│ HEADER │ ACCIONES │
|
||||
│ #abc-123-def │ [Cambiar estado ▼] │
|
||||
│ ● Paid · 15 ago 2026 │ │
|
||||
├───────────────────────────────────┴─────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌─────────────────────────────┐ │
|
||||
│ │ RESUMEN DEL PEDIDO │ │ CLIENTE │ │
|
||||
│ │ │ │ maria@email.com │ │
|
||||
│ │ Subtotal €37.88 │ │ │ │
|
||||
│ │ Descuentos €0.00 │ │ ENVÍO │ │
|
||||
│ │ IVA €7.95 │ │ Calle Gran Vía 42 │ │
|
||||
│ │ ─────────────────── │ │ Madrid, 28013 │ │
|
||||
│ │ TOTAL €45.83 │ │ │ │
|
||||
│ └─────────────────────────┘ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ PRODUCTOS PEDIDOS │ │
|
||||
│ │ ───────────────────────────────────────────────────── │ │
|
||||
│ │ Almendras Crudas 2 × €8.95 = €17.90 │ │
|
||||
│ │ Vitamina D3 + K2 2 × €9.99 = €19.98 │ │
|
||||
│ │ ───────────────────────────────────────────────────── │ │
|
||||
│ │ Envío estándar + €4.99 │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ HISTORIAL │ │
|
||||
│ │ ───────────────────────────────────────────────────── │ │
|
||||
│ │ ● 15 ago 2026 14:32 Creado │ │
|
||||
│ │ ● 15 ago 2026 14:35 Pagado │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Payment Transactions
|
||||
|
||||
```
|
||||
GET /payments/orders/:orderId/transactions
|
||||
Response: { transactions: PaymentTransaction[] }
|
||||
```
|
||||
|
||||
Mostrar en sección separada: fecha, método, monto, estado.
|
||||
|
||||
---
|
||||
|
||||
## 3. Order State Transitions
|
||||
|
||||
### API Contract
|
||||
|
||||
```
|
||||
POST /orders/:id/transitions/admin
|
||||
Authorization: session_token (admin role)
|
||||
|
||||
Body: { state: OrderState }
|
||||
|
||||
Response: Order (updated)
|
||||
```
|
||||
|
||||
### State Machine
|
||||
|
||||
```
|
||||
PENDING ──────────→ AWAITING_PAYMENT (customer paid)
|
||||
│ │
|
||||
├─→ CANCELLED └─→ PAID ──→ PROCESSING ──→ SHIPPED ──→ DELIVERED
|
||||
│ │ │
|
||||
│ ├─→ CANCELLED│
|
||||
│ │ ├─→ CANCELLED
|
||||
│ │ │
|
||||
│ ├─→ REFUNDED (full)
|
||||
│ └─→ PARTIALLY_REFUNDED
|
||||
│ │
|
||||
└─→ CANCELLED ←──────────────────────────────┘
|
||||
```
|
||||
|
||||
### Allowed transitions visible in UI
|
||||
|
||||
| Current State | Actions Available |
|
||||
|-------------------|------------------------------------------------------------|
|
||||
| PENDING | Marcar como Pagado · Cancelar pedido |
|
||||
| AWAITING_PAYMENT | Marcar como Pagado · Cancelar pedido |
|
||||
| PAID | Procesar pedido · Cancelar pedido · Reembolsar |
|
||||
| PROCESSING | Marcar como enviado · Cancelar pedido · Reembolsar |
|
||||
| SHIPPED | Marcar como entregado · Reembolso parcial |
|
||||
| DELIVERED | Reembolso parcial |
|
||||
| CANCELLED | (ninguna) |
|
||||
| REFUNDED | (ninguna) |
|
||||
| PARTIALLY_REFUNDED| (ninguna) |
|
||||
|
||||
### UX para transición
|
||||
|
||||
1. Dropdown o botones con las acciones válidas
|
||||
2. Al hacer click en acción → Dialog de confirmación:
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ Cambiar estado del pedido │
|
||||
│ │
|
||||
│ ¿Marcar este pedido como PAGADO? │
|
||||
│ │
|
||||
│ Estado actual: ● Pendiente │
|
||||
│ Nuevo estado: ● Pagado │
|
||||
│ │
|
||||
│ [Cancelar] [Confirmar] │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
3. Para Cancelar y Refund: campo obligatorio de "Motivo"
|
||||
4. Submit → esperar respuesta → refresh del detalle
|
||||
|
||||
### Cancellation UX
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ ⚠️ Cancelar pedido │
|
||||
│ │
|
||||
│ ¿Estás seguro de cancelar el pedido │
|
||||
│ #abc-123-def? │
|
||||
│ │
|
||||
│ Motivo * │
|
||||
│ ┌────────────────────────────────────┐│
|
||||
│ │ Cliente solicitó cancelación ││
|
||||
│ └────────────────────────────────────┘│
|
||||
│ │
|
||||
│ [Volver] [Cancelar pedido] │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Acceptance Criteria
|
||||
|
||||
### ADM-013 — Order List
|
||||
- [ ] Órdenes listadas con paginación (20 por página)
|
||||
- [ ] Filtro por estado funcional
|
||||
- [ ] Búsqueda por email/ID funcional
|
||||
- [ ] Link a detalle
|
||||
- [ ] Empty state si no hay pedidos
|
||||
- [ ] Loading skeleton
|
||||
|
||||
### ADM-014 — Order Detail
|
||||
- [ ] Resumen con todos los totales
|
||||
- [ ] Lista de productos con precios
|
||||
- [ ] Información de cliente
|
||||
- [ ] Línea de tiempo con historial de estados
|
||||
- [ ] Transacciones de pago visibles
|
||||
- [ ] Estado actual con badge de color + texto
|
||||
|
||||
### ADM-015 — State Transitions
|
||||
- [ ] Solo acciones válidas visibles según estado actual
|
||||
- [ ] Confirmación antes de ejecutar
|
||||
- [ ] Campo "Motivo" obligatorio para Cancelar y Refund
|
||||
- [ ] Refresh del detalle post-transición
|
||||
- [ ] Timeline actualizada
|
||||
- [ ] Toast de éxito/error
|
||||
|
||||
---
|
||||
|
||||
## 5. Tests
|
||||
|
||||
### Unit
|
||||
- State machine: verify only valid transitions shown
|
||||
- Transition formatter: state → label + color
|
||||
|
||||
### Component
|
||||
- OrderStatusBadge renders correct color for each state
|
||||
- TransitionDialog shows only valid actions
|
||||
- OrderTimeline renders in correct order
|
||||
|
||||
### Integration
|
||||
- Transition PENDING → PAID → PROCESSING → SHIPPED → DELIVERED
|
||||
- Attempt invalid transition → backend error shown
|
||||
|
||||
### E2E
|
||||
- Filter orders by status
|
||||
- View order detail
|
||||
- Transition order through valid states
|
||||
- Cancel order with reason
|
||||
- Error when backend rejects transition
|
||||
342
project/specs-admin/SPEC.md
Normal file
342
project/specs-admin/SPEC.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# Admin Panel SDD — MercadoDeVida
|
||||
|
||||
## 1. Repository Assessment
|
||||
|
||||
### 1.1 Estructura actual
|
||||
|
||||
```
|
||||
mercadodevida/
|
||||
├── project/ # Backend (Fastify + Node.js)
|
||||
│ └── src/modules/ # 20 módulos de dominio
|
||||
├── frontend/ # Storefront (Next.js 16 + Tailwind)
|
||||
│ └── src/
|
||||
│ ├── app/ # Rutas Next.js
|
||||
│ ├── components/ # Componentes React
|
||||
│ ├── contexts/ # CartContext, AuthContext
|
||||
│ ├── lib/ # api.ts (fetch functions)
|
||||
│ └── types/ # API types
|
||||
├── specs/ # specs de backend (F-001–F-035)
|
||||
└── work/ # artefactos por feature
|
||||
```
|
||||
|
||||
### 1.2 Ubicación recomendada para Admin
|
||||
|
||||
```
|
||||
apps/admin/ # Nueva app Next.js independiente
|
||||
├── app/
|
||||
│ ├── (auth)/
|
||||
│ │ └── login/
|
||||
│ ├── (dashboard)/
|
||||
│ │ ├── layout.tsx # Shell con sidebar
|
||||
│ │ ├── page.tsx # Dashboard
|
||||
│ │ ├── products/
|
||||
│ │ ├── orders/
|
||||
│ │ ├── customers/
|
||||
│ │ ├── categories/
|
||||
│ │ ├── brands/
|
||||
│ │ ├── promotions/
|
||||
│ │ ├── inventory/
|
||||
│ │ ├── reviews/
|
||||
│ │ ├── cms/
|
||||
│ │ └── settings/
|
||||
│ └── api/ # Proxies al backend (auth)
|
||||
├── features/ # Feature-sliced modules
|
||||
│ ├── auth/
|
||||
│ ├── products/
|
||||
│ ├── orders/
|
||||
│ └── ...
|
||||
├── components/ # Componentes compartidos
|
||||
│ ├── ui/ # Primitivos de diseño
|
||||
│ └── admin/ # Componentes domain-specific
|
||||
├── lib/
|
||||
│ ├── api-client.ts # Cliente HTTP único
|
||||
│ └── permissions.ts # can("action")
|
||||
├── types/ # Tipos compartidos
|
||||
└── middleware/ # Auth middleware
|
||||
```
|
||||
|
||||
### 1.3 Por qué NO en `frontend/src/`
|
||||
|
||||
- Admin y Storefront tienen lifecycle diferentes
|
||||
- Admin es Desktop-first, Storefront es Mobile-first
|
||||
- Admin necesita auth compleja, Storefront no
|
||||
- Mezclaría contextos (CartContext no aplica a admin)
|
||||
- El Shell de Admin es completamente diferente
|
||||
|
||||
### 1.4 Build system
|
||||
|
||||
- Backend: `cd project && npm run build` → `dist/`
|
||||
- Frontend: `cd frontend && npm run build` → `.next/`
|
||||
- Admin: `cd apps/admin && npm run build` → `.next/`
|
||||
- Monorepo en nivel de scripts (`npm run` en root)
|
||||
|
||||
---
|
||||
|
||||
## 2. API Capability Assessment
|
||||
|
||||
### 2.1 Endpoints disponibles para Admin
|
||||
|
||||
| Feature | Método | Endpoint | Auth | Admin | Notas |
|
||||
|---------------|--------|----------------------------------|------|-------|--------------------------|
|
||||
| Products | GET | GET /products/search | No | — | público, paginado |
|
||||
| Products | POST | POST /products | ✓ | ✓ | Crear producto |
|
||||
| Products | PATCH | PATCH /products/:id | ✓ | ✓ | Editar producto |
|
||||
| Products | GET | GET /products/:id/variants | No | — | Variantes |
|
||||
| Products | GET | GET /products/:id/images | No | — | Imágenes |
|
||||
| Products | POST | POST /products/:id/images | ✓ | ✓ | Subir imagen |
|
||||
| Products | DELETE | DELETE /products/:id/images/:id | ✓ | ✓ | Eliminar imagen |
|
||||
| Products | POST | POST /products/:id/variants | ✓ | ✓ | Crear variante |
|
||||
| Products | PATCH | PATCH /products/:id/variants/:id | ✓ | ✓ | Editar variante |
|
||||
| Products | PATCH | PATCH /products/:id/rich-data | ✓ | ✓ | Datos enriquecidos |
|
||||
| Brands | GET | GET /brands | No | — | Listado público |
|
||||
| Brands | GET | GET /marca/:slug | No | — | Detalle público |
|
||||
| Brands | POST | POST /brands | ✓ | ✓ | Crear marca |
|
||||
| Brands | PATCH | PATCH /brands/:id | ✓ | ✓ | Editar marca |
|
||||
| Categories | GET | GET /categories/tree | No | — | Árbol categorías |
|
||||
| Categories | GET | GET /categoria/:slug | No | — | Detalle público |
|
||||
| Categories | POST | POST /categories | ✓ | ✓ | Crear categoría |
|
||||
| Categories | PATCH | PATCH /categories/:id | ✓ | ✓ | Editar categoría |
|
||||
| Categories | DELETE | DELETE /categories/:id | ✓ | ✓ | Eliminar categoría |
|
||||
| Orders | GET | GET /orders | ✓ | ✓ | **NUEVO — listado admin** |
|
||||
| Orders | GET | GET /orders/:id/admin | ✓ | ✓ | **NUEVO — detalle admin**|
|
||||
| Orders | POST | POST /orders/:id/transitions/admin | ✓ | ✓ | **NUEVO — transición** |
|
||||
| Orders | GET | GET /orders/:id | ✓ | — | Detalle cliente |
|
||||
| Orders | POST | POST /orders/:id/transitions | ✓ | — | Transición cliente |
|
||||
| Inventory | GET | GET /inventory/:variantId/availability | No | — | Disponibilidad |
|
||||
| Inventory | PUT | PUT /inventory/:variantId/stock | ✓ | ✓ | Ajustar stock |
|
||||
| Inventory | POST | POST /inventory/:variantId/reservations | No | — | Reservas |
|
||||
| Payments | GET | GET /payments/orders/:id/transactions | ✓ | — | Transacciones |
|
||||
| Users | GET | GET /users | ✓ | ✓ | Listado clientes |
|
||||
| Users | GET | GET /users/:id | ✓ | ✓ | Detalle cliente |
|
||||
| Users | PATCH | PATCH /users/:id | ✓ | ✓ | Editar cliente |
|
||||
| Users | GET | GET /users/:id/addresses | ✓ | ✓ | Direcciones |
|
||||
| Users | POST | POST /users/:id/addresses | ✓ | ✓ | Crear dirección |
|
||||
| Users | PATCH | PATCH /users/:id/addresses/:id | ✓ | ✓ | Editar dirección |
|
||||
| Users | DELETE | DELETE /users/:id/addresses/:id | ✓ | ✓ | Eliminar dirección |
|
||||
| Promotions | POST | POST /promotions | ✓ | ✓ | Crear promoción |
|
||||
| Reviews | GET | GET /reviews?productId= | No | — | Reseñas publicadas |
|
||||
| Reviews | PATCH | PATCH /reviews/:id/moderate | ✓ | ✓ | Moderar reseña |
|
||||
| CMS | GET | GET /cms/pages/:slug | No | — | Página pública |
|
||||
| CMS | POST | POST /cms/pages | ✓ | ✓ | Crear página |
|
||||
| CMS | PATCH | PATCH /cms/pages/:id | ✓ | ✓ | Editar página |
|
||||
| CMS | POST | POST /cms/pages/:id/publish | ✓ | ✓ | Publicar |
|
||||
| CMS | POST | POST /cms/pages/:id/unpublish | ✓ | ✓ | Despublicar |
|
||||
| Shipping | POST | POST /shipping/zones | ✓ | ✓ | Crear zona envío |
|
||||
| Shipping | POST | POST /shipping/methods | ✓ | ✓ | Crear método envío |
|
||||
|
||||
### 2.2 Endpoints FALTANTES en el backend
|
||||
|
||||
| # | Capacidad requerida | Endpoint necesario | Prioridad |
|
||||
|---|--------------------------------|---------------------------------------|-----------|
|
||||
| BD-01 | Auth/me (sesión actual) | GET /auth/me | CRÍTICA |
|
||||
| BD-02 | Listar promociones | GET /promotions | ALTA |
|
||||
| BD-03 | Editar promoción | PATCH /promotions/:id | ALTA |
|
||||
| BD-04 | Eliminar promoción | DELETE /promotions/:id | MEDIA |
|
||||
| BD-05 | Listar reseñas (todas) | GET /reviews/admin | ALTA |
|
||||
| BD-06 | Actualizar producto estado | PATCH /products/:id/state | MEDIA |
|
||||
| BD-07 | Eliminar producto | DELETE /products/:id | MEDIA |
|
||||
| BD-08 | Eliminar marca | DELETE /brands/:id | MEDIA |
|
||||
| BD-09 | Bulk stock adjustment | POST /inventory/bulk-adjust | BAJA |
|
||||
| BD-10 | Audit log explorer | GET /admin/audit | MEDIA |
|
||||
| BD-11 | Dashboard stats | GET /admin/stats | MEDIA |
|
||||
|
||||
### 2.3 Roles existentes en backend
|
||||
|
||||
```typescript
|
||||
type Role = 'customer' | 'admin';
|
||||
```
|
||||
|
||||
Solo existen dos roles. No hay granularidad (Catalog Manager, Order Manager, etc).
|
||||
|
||||
---
|
||||
|
||||
## 3. Admin API Client Design
|
||||
|
||||
### 3.1 Cliente HTTP único
|
||||
|
||||
```typescript
|
||||
// apps/admin/lib/api-client.ts
|
||||
class ApiClient {
|
||||
constructor(private baseUrl: string, private getToken: () => string | null) {}
|
||||
|
||||
async request<T>(method, path, body?, opts?): Promise<T>
|
||||
async get<T>(path): Promise<T>
|
||||
async post<T>(path, body): Promise<T>
|
||||
async patch<T>(path, body): Promise<T>
|
||||
async delete<T>(path): Promise<T>
|
||||
}
|
||||
|
||||
// Feature API adapters
|
||||
// apps/admin/features/orders/api.ts
|
||||
export const ordersApi = {
|
||||
list: (params) => client.get('/orders', params),
|
||||
get: (id) => client.get(`/orders/${id}/admin`),
|
||||
transition: (id, state) => client.post(`/orders/${id}/transitions/admin`, { state }),
|
||||
};
|
||||
```
|
||||
|
||||
### 3.2 Permisos
|
||||
|
||||
```typescript
|
||||
// apps/admin/lib/permissions.ts
|
||||
type Permission =
|
||||
| 'products.read' | 'products.write' | 'products.delete'
|
||||
| 'orders.read' | 'orders.write'
|
||||
| 'inventory.read' | 'inventory.write'
|
||||
| 'customers.read' | 'customers.write'
|
||||
| 'categories.read' | 'categories.write' | 'categories.delete'
|
||||
| 'brands.read' | 'brands.write' | 'brands.delete'
|
||||
| 'promotions.read' | 'promotions.write'
|
||||
| 'reviews.read' | 'reviews.moderate'
|
||||
| 'cms.read' | 'cms.write';
|
||||
|
||||
export function can(user: User, permission: Permission): boolean {
|
||||
if (user.role === 'admin') return true;
|
||||
// Granular permissions would go here when backend supports them
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Arquitectura de rutas Admin
|
||||
|
||||
```
|
||||
apps/admin/app/
|
||||
├── (auth)/
|
||||
│ └── login/
|
||||
│ └── page.tsx
|
||||
├── (dashboard)/
|
||||
│ ├── layout.tsx # AdminShell: sidebar + header
|
||||
│ ├── page.tsx # /admin — Dashboard
|
||||
│ ├── products/
|
||||
│ │ ├── page.tsx # /admin/products — Listado
|
||||
│ │ └── [id]/
|
||||
│ │ └── page.tsx # /admin/products/[id] — Editor
|
||||
│ ├── orders/
|
||||
│ │ ├── page.tsx # /admin/orders — Listado
|
||||
│ │ └── [id]/
|
||||
│ │ └── page.tsx # /admin/orders/[id] — Detalle
|
||||
│ ├── customers/
|
||||
│ │ ├── page.tsx # /admin/customers
|
||||
│ │ └── [id]/
|
||||
│ │ └── page.tsx
|
||||
│ ├── categories/
|
||||
│ │ └── page.tsx # /admin/categories
|
||||
│ ├── brands/
|
||||
│ │ └── page.tsx # /admin/brands
|
||||
│ ├── inventory/
|
||||
│ │ └── page.tsx # /admin/inventory
|
||||
│ ├── promotions/
|
||||
│ │ └── page.tsx # /admin/promotions
|
||||
│ ├── reviews/
|
||||
│ │ └── page.tsx # /admin/reviews
|
||||
│ ├── cms/
|
||||
│ │ └── page.tsx # /admin/cms
|
||||
│ └── settings/
|
||||
│ └── page.tsx # /admin/settings
|
||||
└── api/
|
||||
└── auth/
|
||||
└── [...nextauth]/route.ts # Auth handler
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. RBAC Matrix
|
||||
|
||||
| Feature | admin | customer |
|
||||
|-------------|-------|----------|
|
||||
| Products R | ✓ | — |
|
||||
| Products W | ✓ | — |
|
||||
| Orders R | ✓ | own only |
|
||||
| Orders W | ✓ | — |
|
||||
| Inventory R | ✓ | — |
|
||||
| Inventory W | ✓ | — |
|
||||
| Customers R | ✓ | — |
|
||||
| Customers W | ✓ | own only |
|
||||
| Categories | ✓ | — |
|
||||
| Brands | ✓ | — |
|
||||
| Promotions | ✓ | — |
|
||||
| Reviews | ✓ | own only |
|
||||
| CMS | ✓ | — |
|
||||
| Audit | ✓ | — |
|
||||
|
||||
---
|
||||
|
||||
## 6. MVP Scope
|
||||
|
||||
### Priority 1 (crítico para operar la tienda)
|
||||
|
||||
1. Auth — login, logout, sesión
|
||||
2. Admin Shell — sidebar, layout, permisos
|
||||
3. Products — listado, editor (nombre, precio, stock, imágenes, estado)
|
||||
4. Orders — listado, detalle, transiciones de estado
|
||||
5. Inventory — vista de stock, ajuste rápido
|
||||
|
||||
### Priority 2 (operación normal)
|
||||
|
||||
6. Customers — listado, detalle de cliente
|
||||
7. Categories — CRUD básico
|
||||
8. Brands — CRUD básico
|
||||
9. Dashboard — stats operativos
|
||||
|
||||
### Priority 3 (mejora)
|
||||
|
||||
10. Promotions — CRUD
|
||||
11. Reviews — moderación
|
||||
12. CMS — gestión de páginas
|
||||
13. Audit log
|
||||
|
||||
---
|
||||
|
||||
## 7. Backend Dependencies (BD-01 a BD-11)
|
||||
|
||||
Ver sección 2.2. Cada una es un ticket de backend separado.
|
||||
|
||||
---
|
||||
|
||||
## 8. Parallelization Plan
|
||||
|
||||
```
|
||||
FASE 1 — Foundation (sequencial)
|
||||
│
|
||||
├── ADM-001: Nuevo proyecto Next.js en apps/admin
|
||||
├── ADM-002: API Client + types
|
||||
├── ADM-003: Auth (BD-01 + login page)
|
||||
│
|
||||
FASE 2 — Admin Shell (sequencial)
|
||||
│
|
||||
├── ADM-004: Admin layout (sidebar, header, permisos nav)
|
||||
│
|
||||
FASE 3 — MVP Features (paralelo)
|
||||
│
|
||||
├── Stream A: Products (ADM-005 a ADM-012)
|
||||
├── Stream B: Orders (ADM-013 a ADM-019)
|
||||
└── Stream C: Inventory (ADM-020 a ADM-022)
|
||||
|
||||
FASE 4 — Secondary Features (paralelo)
|
||||
│
|
||||
├── Stream D: Customers (ADM-023 a ADM-025)
|
||||
├── Stream E: Categories + Brands (ADM-026 a ADM-030)
|
||||
└── Stream F: Dashboard (ADM-031)
|
||||
|
||||
FASE 5 — Extensions (paralelo)
|
||||
│
|
||||
├── Stream G: Promotions (BD-02, BD-03 + ADM-032 a ADM-034)
|
||||
├── Stream H: Reviews (BD-05 + ADM-035 a ADM-037)
|
||||
└── Stream I: CMS (ADM-038 a ADM-040)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Risk Assessment
|
||||
|
||||
| Riesgo | Severidad | Mitigación |
|
||||
|--------|-----------|------------|
|
||||
| No existe GET /auth/me | CRÍTICA | BD-01 debe implementarse antes de cualquier código de admin |
|
||||
| Promotions sin listado | ALTA | BD-02 debe existir para tener UI de promociones |
|
||||
| Reviews sin listado admin | ALTA | BD-05 debe existir para moderación |
|
||||
| Sin granularidad de roles | MEDIA | Trabajar con 'admin' vs 'customer' en frontend; backend no valida permisos finos |
|
||||
| Concurrencia de inventario | MEDIA | Siempre esperar respuesta del backend antes de mostrar nuevo stock |
|
||||
| Estado de orden inválido | MEDIA | Backend valida transiciones; UI solo muestra acciones permitidas |
|
||||
| Bulk operations | BAJA | No implementar sin endpoint de backend dedicado (BD-09) |
|
||||
667
project/specs-admin/TASKS.md
Normal file
667
project/specs-admin/TASKS.md
Normal file
@@ -0,0 +1,667 @@
|
||||
# Admin Panel Tasks — MercadoDeVida
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Cada tarea = un archivo `ADM-XXX.md` en `specs/admin/`
|
||||
- Cada tarea tiene: Goal, API contracts, Desktop UX, Responsive UX, Security, Error States, Acceptance Criteria, Tests
|
||||
- Dependencias: otras tareas o BD-XXX
|
||||
|
||||
---
|
||||
|
||||
## BACKEND DEPENDENCIES (precondiciones)
|
||||
|
||||
| ID | Descripción | Prioridad |
|
||||
|-------|------------------------------------------|-----------|
|
||||
| BD-01 | GET /auth/me → { user, role } | CRÍTICA |
|
||||
| BD-02 | GET /promotions (listado con paginación) | ALTA |
|
||||
| BD-03 | PATCH /promotions/:id | ALTA |
|
||||
| BD-04 | DELETE /promotions/:id | MEDIA |
|
||||
| BD-05 | GET /reviews/admin (todas, con filtros) | ALTA |
|
||||
| BD-06 | PATCH /products/:id/state | MEDIA |
|
||||
| BD-07 | DELETE /products/:id | MEDIA |
|
||||
| BD-08 | DELETE /brands/:id | MEDIA |
|
||||
| BD-09 | POST /inventory/bulk-adjust | BAJA |
|
||||
| BD-10 | GET /admin/audit | MEDIA |
|
||||
| BD-11 | GET /admin/stats (KPIs del dashboard) | MEDIA |
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1 — Foundation
|
||||
|
||||
### ADM-001 — Crear proyecto Next.js en apps/admin
|
||||
|
||||
**Goal:** Nuevo workspace Next.js 16 + TypeScript + Tailwind bajo `apps/admin/`
|
||||
|
||||
**Dependencies:** Ninguna
|
||||
|
||||
**Modules:** `apps/admin/` (nuevo)
|
||||
|
||||
**API:** N/A (no hay código aún)
|
||||
|
||||
**Permissions:** N/A
|
||||
|
||||
**Desktop UX:** N/A
|
||||
|
||||
**Responsive UX:** N/A
|
||||
|
||||
**Security:** N/A
|
||||
|
||||
**Error States:** N/A
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- `apps/admin/` existe con `package.json`, `tsconfig.json`, `next.config.ts`, `tailwind.config.ts`
|
||||
- `npm run build` compila sin errores en `apps/admin/`
|
||||
- Rutas dinámicas ignoradas en `next.config.ts`
|
||||
|
||||
**Tests:** Unit: verificar que el proyecto compila
|
||||
|
||||
---
|
||||
|
||||
### ADM-002 — API Client + Types compartidos
|
||||
|
||||
**Goal:** Cliente HTTP tipado que consume el backend, con adapters por feature
|
||||
|
||||
**Dependencies:** BD-01 (GET /auth/me)
|
||||
|
||||
**Modules:** `apps/admin/lib/api-client.ts`, `apps/admin/lib/permissions.ts`, `apps/admin/types/`
|
||||
|
||||
**API:** Todos los endpoints del backend (sección 2.1 de SPEC.md)
|
||||
|
||||
**Permissions:** `can(user, permission)` function
|
||||
|
||||
**Desktop UX:** N/A
|
||||
|
||||
**Responsive UX:** N/A
|
||||
|
||||
**Security:**
|
||||
- Token de sesión en cookie `HttpOnly`
|
||||
- CSRF protection vía SameSite=Lax
|
||||
- No exponer token en JS
|
||||
|
||||
**Error States:**
|
||||
- 401 → redirect a /login
|
||||
- 403 → toast "Sin permisos"
|
||||
- 404 → página de no encontrado
|
||||
- 500 → toast "Error del servidor"
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- `ApiClient` hace fetch al backend con cookie de sesión
|
||||
- Mapeo de errores HTTP → mensajes de usuario
|
||||
- Tipos para Products, Orders, Customers, etc.
|
||||
- Feature API adapters (ordersApi, productsApi, etc.)
|
||||
|
||||
**Tests:**
|
||||
- Unit: ApiClient.get/post/patch/delete
|
||||
- Unit: can() permission checks
|
||||
- Unit: error mapping
|
||||
|
||||
---
|
||||
|
||||
### ADM-003 — Login + Auth flow
|
||||
|
||||
**Goal:** Página de login funcional, logout, sesión persistente
|
||||
|
||||
**Dependencies:** BD-01, ADM-002
|
||||
|
||||
**Modules:** `apps/admin/app/(auth)/login/`, `apps/admin/app/api/auth/[...nextauth]/`
|
||||
|
||||
**API:** POST /auth/login, POST /auth/logout, GET /auth/me
|
||||
|
||||
**Permissions:** N/A
|
||||
|
||||
**Desktop UX:**
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ │
|
||||
│ [MercadoDeVida Logo] │
|
||||
│ │
|
||||
│ Iniciar sesión │
|
||||
│ │
|
||||
│ Email [_______________] │
|
||||
│ Password [_______________] │
|
||||
│ │
|
||||
│ [ Iniciar sesión ] │
|
||||
│ │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Responsive UX:** Mismo layout, centrado en móvil
|
||||
|
||||
**Security:**
|
||||
- HttpOnly cookie para sesión
|
||||
- Rate limit del backend (429)
|
||||
- No mostrar errores específicos de credenciales
|
||||
|
||||
**Error States:**
|
||||
- Credenciales inválidas → "Email o contraseña incorrectos"
|
||||
- Rate limited → "Demasiados intentos. Espera X segundos"
|
||||
- Servidor caído → "Error de conexión. Intenta de nuevo"
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Login exitoso → redirect a /admin
|
||||
- Sesión válida al recargar → mantiene /admin
|
||||
- Sesión inválida → redirect a /admin/login
|
||||
- Logout → limpia cookie, redirect a /admin/login
|
||||
|
||||
**Tests:**
|
||||
- E2E: login exitoso
|
||||
- E2E: login fallido
|
||||
- E2E: logout
|
||||
- E2E: sesión expirada
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2 — Admin Shell
|
||||
|
||||
### ADM-004 — Admin Shell (sidebar + layout)
|
||||
|
||||
**Goal:** Layout reutilizable con navegación, usuario, y protección de rutas
|
||||
|
||||
**Dependencies:** ADM-002, ADM-003
|
||||
|
||||
**Modules:** `apps/admin/app/(dashboard)/layout.tsx`, `apps/admin/components/admin/`
|
||||
|
||||
**API:** N/A
|
||||
|
||||
**Permissions:** `can()` en navegación
|
||||
|
||||
**Desktop UX:**
|
||||
```
|
||||
┌──────────────┬─────────────────────────────────┐
|
||||
│ 🏠 Dashboard │ Header: [MercadoDeVida Admin] │
|
||||
│ 📦 Productos │ User: admin@mdv.es | Logout │
|
||||
│ 🧾 Pedidos ├─────────────────────────────────┤
|
||||
│ 📊 Inventario│ │
|
||||
│ 👥 Clientes │ CONTENT │
|
||||
│ 🏷️ Categorías│ │
|
||||
│ 🏷️ Marcas │ │
|
||||
│ 🏷️ Promociones│ │
|
||||
│ ⭐ Reseñas │ │
|
||||
│ 📄 CMS │ │
|
||||
│ ⚙️ Ajustes │ │
|
||||
└──────────────┴─────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Responsive UX:**
|
||||
- Mobile: sidebar colapsado en hamburger menu
|
||||
- Tablet: sidebar como drawer
|
||||
- Desktop: sidebar fija 240px
|
||||
|
||||
**Security:**
|
||||
- Middleware que verifica sesión en cada request
|
||||
- Nav items ocultos según permisos
|
||||
- Rutas /admin/* requieren auth (redirect a /admin/login)
|
||||
|
||||
**Error States:**
|
||||
- Sin sesión → redirect login
|
||||
- Error de permisos → toast
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Sidebar muestra items según permisos del usuario
|
||||
- Header muestra email + logout
|
||||
- Mobile toggle funciona
|
||||
- Navegación activa con estilo distintivo
|
||||
- Loading skeleton en transición de auth
|
||||
|
||||
**Tests:**
|
||||
- Component: Sidebar renders correct items for role
|
||||
- E2E: unauthorized redirected to login
|
||||
- E2E: sidebar navigation works
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3 — MVP: Products
|
||||
|
||||
### ADM-005 — Product List Route
|
||||
|
||||
**Goal:** Ruta `/admin/products` con tabla paginada, búsqueda y filtros
|
||||
|
||||
**Dependencies:** ADM-004
|
||||
|
||||
**Modules:** `apps/admin/app/(dashboard)/products/page.tsx`
|
||||
|
||||
**API:** GET /products/search?q=&limit=20&offset=0
|
||||
|
||||
**Permissions:** products.read
|
||||
|
||||
**Desktop UX:**
|
||||
```
|
||||
Header + Sidebar
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Productos [+ Crear producto] │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 🔍 Buscar... [Filtros ▼] │
|
||||
├────┬────────────────────┬────────┬────────┬────────────┤
|
||||
│ │ Producto │ Marca │ Precio │ Estado │
|
||||
├────┼────────────────────┼────────┼────────┼────────────┤
|
||||
│ □ │ Almendras Crudas │ EcoVida│ €8.95 │ ● Activo │
|
||||
│ □ │ Vitamina D3+K2 │ NatPhar│ €12.50 │ ● Activo │
|
||||
├────┴────────────────────┴────────┴────────┴────────────┤
|
||||
│ ← Anterior Página 1 de 3 Siguiente → │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Responsive UX:** Tabla scroll horizontal en móvil; cards en mobile
|
||||
|
||||
**Security:**products.read
|
||||
|
||||
**Error States:**
|
||||
- Loading: skeleton
|
||||
- Empty: "No hay productos"
|
||||
- Error: retry button
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Productos listados con nombre, marca, precio
|
||||
- Búsqueda por nombre
|
||||
- Paginación funcional
|
||||
- Filtro por estado (activo/inactivo)
|
||||
- Link a detalle
|
||||
|
||||
**Tests:**
|
||||
- Unit: pagination params
|
||||
- Component: ProductTable renders
|
||||
- E2E: search filters products
|
||||
|
||||
---
|
||||
|
||||
### ADM-006 — Product Editor Shell
|
||||
|
||||
**Goal:** Shell del editor con tabs y detección de cambios sin guardar
|
||||
|
||||
**Dependencies:** ADM-005
|
||||
|
||||
**Modules:** `apps/admin/features/products/components/ProductEditor.tsx`
|
||||
|
||||
**API:** N/A (state local)
|
||||
|
||||
**Permissions:** products.write
|
||||
|
||||
**Desktop UX:** Tabs: General | Precios | Inventario | Imágenes | SEO | Publicar
|
||||
|
||||
**Responsive UX:** Tabs colapsados en accordion en móvil
|
||||
|
||||
**Security:**products.write
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Cada tab es una sección separada
|
||||
- Unsaved changes detected al navegar
|
||||
- Confirm dialog antes de salir
|
||||
|
||||
**Tests:** Component: unsaved changes detected
|
||||
|
||||
---
|
||||
|
||||
### ADM-007 — Product General Fields
|
||||
|
||||
**Goal:** Campos generales: nombre, slug, descripción, marca, categorías
|
||||
|
||||
**Dependencies:** ADM-006
|
||||
|
||||
**Modules:** `apps/admin/features/products/components/sections/GeneralSection.tsx`
|
||||
|
||||
**API:** PATCH /products/:id (nombre, descripción, brandId, categoryIds)
|
||||
|
||||
**Permissions:** products.write
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Campos pre-poblados con datos actuales
|
||||
- Slug auto-generado desde nombre
|
||||
- Selector de marca
|
||||
- Selector de categorías (multiselect)
|
||||
- Validación de requerido
|
||||
|
||||
**Tests:** Unit: slug auto-generation
|
||||
|
||||
---
|
||||
|
||||
### ADM-008 — Product Pricing Fields
|
||||
|
||||
**Goal:** Campos de precio: precio neto, tasa IVA, variantes
|
||||
|
||||
**Dependencies:** ADM-006
|
||||
|
||||
**Modules:** `apps/admin/features/products/components/sections/PricingSection.tsx`
|
||||
|
||||
**API:** GET /products/:id/variants, PATCH /products/:id/variants/:variantId
|
||||
|
||||
**Permissions:** products.write
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Lista de variantes con SKU, EAN, atributos
|
||||
- Precio por variante
|
||||
- IVA (general 21%, reducido 10%)
|
||||
|
||||
**Tests:** Unit: gross price calculation (only display, backend owns truth)
|
||||
|
||||
---
|
||||
|
||||
### ADM-009 — Product Inventory Section
|
||||
|
||||
**Goal:** Ver y ajustar stock por variante
|
||||
|
||||
**Dependencies:** ADM-006
|
||||
|
||||
**Modules:** `apps/admin/features/products/components/sections/InventorySection.tsx`
|
||||
|
||||
**API:** GET /inventory/:variantId/availability, PUT /inventory/:variantId/stock
|
||||
|
||||
**Permissions:** inventory.write
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Stock actual visible por variante
|
||||
- Input para nuevo stock
|
||||
- Botón "Actualizar"
|
||||
- Confirmación de cambio
|
||||
|
||||
**Tests:** Integration: stock update flow
|
||||
|
||||
---
|
||||
|
||||
### ADM-010 — Product Images Section
|
||||
|
||||
**Goal:** Gestionar imágenes: subir, reordenar, eliminar
|
||||
|
||||
**Dependencies:** ADM-006
|
||||
|
||||
**Modules:** `apps/admin/features/products/components/sections/ImagesSection.tsx`
|
||||
|
||||
**API:** GET /products/:id/images, POST /products/:id/images, DELETE /products/:id/images/:imageId, PATCH /products/:id/images/reorder
|
||||
|
||||
**Permissions:** products.write
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Grid de imágenes actuales
|
||||
- Upload de nueva imagen
|
||||
- Drag to reorder
|
||||
- Marcar como principal
|
||||
- Eliminar con confirmación
|
||||
|
||||
**Tests:** E2E: image upload + reorder
|
||||
|
||||
---
|
||||
|
||||
### ADM-011 — Product SEO Section
|
||||
|
||||
**Goal:** Campos SEO: title, description, index/noindex
|
||||
|
||||
**Dependencies:** ADM-006
|
||||
|
||||
**Modules:** `apps/admin/features/products/components/sections/SEOSection.tsx`
|
||||
|
||||
**API:** PATCH /products/:id (seoTitle, seoDescription)
|
||||
|
||||
**Permissions:** products.write
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- SEO title con contador (max 60 chars)
|
||||
- Meta description con contador (max 160 chars)
|
||||
- Preview de snippet Google
|
||||
- Toggle index/noindex
|
||||
|
||||
**Tests:** Component: character counters
|
||||
|
||||
---
|
||||
|
||||
### ADM-012 — Product Create Flow
|
||||
|
||||
**Goal:** Ruta `/admin/products/new` para crear producto nuevo
|
||||
|
||||
**Dependencies:** ADM-006
|
||||
|
||||
**Modules:** `apps/admin/app/(dashboard)/products/new/page.tsx`
|
||||
|
||||
**API:** POST /products
|
||||
|
||||
**Permissions:** products.write
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Formulario completo con todos los campos
|
||||
- POST al crear
|
||||
- Redirect a detalle del nuevo producto
|
||||
|
||||
**Tests:** E2E: create product end-to-end
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3 — MVP: Orders
|
||||
|
||||
### ADM-013 — Order List Route
|
||||
|
||||
**Goal:** Ruta `/admin/orders` con listado paginado y filtros
|
||||
|
||||
**Dependencies:** ADM-004
|
||||
|
||||
**Modules:** `apps/admin/app/(dashboard)/orders/page.tsx`
|
||||
|
||||
**API:** GET /orders (el nuevo endpoint creado en F-047)
|
||||
|
||||
**Permissions:** orders.read
|
||||
|
||||
**Desktop UX:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Pedidos Filtros: [Estado ▼] │
|
||||
├──────┬─────────────┬──────────┬────────┬────────────────┤
|
||||
│ #ID │ Cliente │ Total │ Estado │ Fecha │
|
||||
├──────┼─────────────┼──────────┼────────┼────────────────┤
|
||||
│ abc… │ maria@… │ €45.83 │ ● Paid │ 15 ago 2026 │
|
||||
│ abc… │ juan@… │ €23.10 │ ● Pend │ 14 ago 2026 │
|
||||
├──────┴─────────────┴──────────┴────────┴────────────────┤
|
||||
│ ← Anterior Página 1 Siguiente → │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Responsive UX:** Cards en móvil
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Filtros: estado (PENDING, PAID, PROCESSING, SHIPPED, DELIVERED, CANCELLED)
|
||||
- Búsqueda por ID o email de cliente
|
||||
- Paginación
|
||||
- Link a detalle
|
||||
|
||||
**Tests:** E2E: filter orders by status
|
||||
|
||||
---
|
||||
|
||||
### ADM-014 — Order Detail Page
|
||||
|
||||
**Goal:** Ruta `/admin/orders/[id]` con detalle completo y acciones
|
||||
|
||||
**Dependencies:** ADM-013
|
||||
|
||||
**Modules:** `apps/admin/app/(dashboard)/orders/[id]/page.tsx`
|
||||
|
||||
**API:** GET /orders/:id/admin, GET /payments/orders/:id/transactions
|
||||
|
||||
**Permissions:** orders.read
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Header: ID, estado badge, fecha
|
||||
- Resumen: subtotal, descuentos, IVA, total
|
||||
- Línea de tiempo de estados
|
||||
- Lista de items con nombres, cantidades, precios
|
||||
- Dirección de envío
|
||||
- Transacciones de pago
|
||||
- Botones de acción según estado
|
||||
|
||||
**Tests:** E2E: view order detail
|
||||
|
||||
---
|
||||
|
||||
### ADM-015 — Order State Transitions
|
||||
|
||||
**Goal:** Botones de acción para cambiar estado de pedido
|
||||
|
||||
**Dependencies:** ADM-014
|
||||
|
||||
**Modules:** `apps/admin/app/(dashboard)/orders/[id]/page.tsx` (actions)
|
||||
|
||||
**API:** POST /orders/:id/transitions/admin
|
||||
|
||||
**Permissions:** orders.write
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Solo acciones válidas para el estado actual se muestran
|
||||
- Confirmación antes de ejecutar
|
||||
- Optimistic: esperar respuesta del backend
|
||||
- Refresh del detalle post-transición
|
||||
- Timeline actualizada
|
||||
|
||||
**State machine visible en UI:**
|
||||
```
|
||||
PENDING → [Marcar Pagado] | [Cancelar]
|
||||
PAID → [Procesar] | [Cancelar] | [Reembolsar]
|
||||
PROCESSING → [Enviar] | [Cancelar] | [Reembolsar]
|
||||
SHIPPED → [Marcar Entregado] | [Reembolso parcial]
|
||||
```
|
||||
|
||||
**Tests:** E2E: transition order state through valid path
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3 — MVP: Inventory
|
||||
|
||||
### ADM-020 — Inventory Overview
|
||||
|
||||
**Goal:** Vista de inventario por variante con stock y estado
|
||||
|
||||
**Dependencies:** ADM-004
|
||||
|
||||
**Modules:** `apps/admin/app/(dashboard)/inventory/page.tsx`
|
||||
|
||||
**API:** GET /products/search (todas), GET /products/:id/variants, GET /inventory/:variantId/availability
|
||||
|
||||
**Permissions:** inventory.read
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Tabla: SKU, producto, variante, stock disponible, estado
|
||||
- Filtros: sin stock, stock bajo, en stock
|
||||
- Búsqueda por SKU o nombre
|
||||
|
||||
**Tests:** E2E: filter by stock status
|
||||
|
||||
---
|
||||
|
||||
### ADM-021 — Quick Stock Adjustment
|
||||
|
||||
**Goal:** Ajuste rápido de stock desde la vista de inventario
|
||||
|
||||
**Dependencies:** ADM-020
|
||||
|
||||
**Modules:** Inline en inventory page
|
||||
|
||||
**API:** PUT /inventory/:variantId/stock
|
||||
|
||||
**Permissions:** inventory.write
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- Input inline para nuevo stock
|
||||
- Validación: número >= 0
|
||||
- Confirmación antes de guardar
|
||||
- Refresco del valor tras guardado
|
||||
|
||||
**Tests:** Integration: stock adjustment flow
|
||||
|
||||
---
|
||||
|
||||
## PHASE 4 — Secondary Features
|
||||
|
||||
### ADM-023 — Customer List
|
||||
|
||||
**Goal:** `/admin/customers` — listado de clientes
|
||||
|
||||
**Dependencies:** ADM-004
|
||||
|
||||
**API:** GET /users
|
||||
|
||||
**Permissions:** customers.read
|
||||
|
||||
### ADM-024 — Customer Detail
|
||||
|
||||
**Goal:** `/admin/customers/[id]` — detalle con pedidos
|
||||
|
||||
**Dependencies:** ADM-023
|
||||
|
||||
**API:** GET /users/:id, GET /orders (filtrado por user)
|
||||
|
||||
**Permissions:** customers.read
|
||||
|
||||
### ADM-025 — Customer Edit
|
||||
|
||||
**Goal:** Editar datos de cliente
|
||||
|
||||
**Dependencies:** ADM-024
|
||||
|
||||
**API:** PATCH /users/:id
|
||||
|
||||
**Permissions:** customers.write
|
||||
|
||||
### ADM-026 — Categories CRUD
|
||||
|
||||
**Goal:** `/admin/categories` — gestión de categorías
|
||||
|
||||
**Dependencies:** ADM-004
|
||||
|
||||
**API:** GET /categories/tree, POST /categories, PATCH /categories/:id, DELETE /categories/:id
|
||||
|
||||
**Permissions:** categories.write
|
||||
|
||||
### ADM-027 — Brands CRUD
|
||||
|
||||
**Goal:** `/admin/brands` — gestión de marcas
|
||||
|
||||
**Dependencies:** ADM-004
|
||||
|
||||
**API:** GET /brands, POST /brands, PATCH /brands/:id
|
||||
|
||||
**Permissions:** brands.write
|
||||
|
||||
### ADM-031 — Dashboard Widgets
|
||||
|
||||
**Goal:** `/admin` — stats operativos
|
||||
|
||||
**Dependencies:** ADM-004, BD-11
|
||||
|
||||
**API:** GET /admin/stats (o agregación de endpoints existentes)
|
||||
|
||||
**Permissions:** orders.read, inventory.read
|
||||
|
||||
---
|
||||
|
||||
## PHASE 5 — Extensions
|
||||
|
||||
### ADM-032 — Promotions CRUD (requiere BD-02, BD-03)
|
||||
|
||||
**Goal:** `/admin/promotions` — gestión de promociones
|
||||
|
||||
**API:** GET /promotions, POST /promotions, PATCH /promotions/:id, DELETE /promotions/:id
|
||||
|
||||
**Permissions:** promotions.write
|
||||
|
||||
### ADM-035 — Reviews Moderation (requiere BD-05)
|
||||
|
||||
**Goal:** `/admin/reviews` — moderación de reseñas
|
||||
|
||||
**API:** GET /reviews/admin, PATCH /reviews/:id/moderate
|
||||
|
||||
**Permissions:** reviews.moderate
|
||||
|
||||
### ADM-038 — CMS Pages
|
||||
|
||||
**Goal:** `/admin/cms` — gestión de páginas de contenido
|
||||
|
||||
**API:** GET /cms/pages/:slug, POST /cms/pages, PATCH /cms/pages/:id, POST /cms/pages/:id/publish, POST /cms/pages/:id/unpublish
|
||||
|
||||
**Permissions:** cms.write
|
||||
|
||||
---
|
||||
|
||||
## Definición de Done por tarea
|
||||
|
||||
Cada tarea ADM-XXX se marca DONE cuando:
|
||||
- [ ] SPEC.md de la tarea aprobado
|
||||
- [ ] Código implementado
|
||||
- [ ] Tests unitarios pasando
|
||||
- [ ] Tests E2E pasando (si aplica)
|
||||
- [ ] Build pasa sin errores TS
|
||||
- [ ] Verificado con curl/curl manual
|
||||
Reference in New Issue
Block a user