Imported from LucianoBertero/gestionpro (
apps/web/AGENTS.md). Install upstream withnpx skills add LucianoBertero/gestionpro --skill web. Copyright stays with the author.
AGENTS.md — Frontend Next.js (GestiónPro)
Template base: Kiranism/next-shadcn-dashboard-starter. Adaptado a JWT propio, sin Clerk. Este es el archivo canónico para IAs.
Stack Real — GestiónPro
| Tecnología | Rol |
|---|---|
| Next.js 16 (App Router) + TypeScript | Framework |
| Tailwind CSS v4 + shadcn/ui (New York) | Estilos + componentes base |
| Zustand v5 | Estado global (auth store, UI state) |
| Axios | Cliente HTTP al backend NestJS |
| TanStack React Query v5 | Fetching, cache, invalidación |
| TanStack Table v8 | Tablas con sort/filter/pagination |
| TanStack Form + Zod | Formularios con validación |
| Nuqs | URL search params state management |
| Recharts | Gráficos del dashboard |
| FullCalendar | Vista de calendario (a instalar) |
Ya NO usa: Clerk (auth), mock APIs, api-client.ts fetch wrapper.
Auth — JWT + Zustand + Axios
Arquitectura
Login page (user selector) → API /v1/auth/login → accessToken (15min) + refreshToken (7d, httpOnly cookie)
↓
Zustand authStore (accessToken en memoria)
↓
Axios interceptor (adjunta Bearer token a cada request)
↓
401 → intenta refresh → si falla → logout
Zustand Auth Store (src/features/auth/store.ts)
interface AuthState {
user: Usuario | null;
accessToken: string | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
refresh: () => Promise<void>;
logout: () => Promise<void>;
}
Axios Instance (src/lib/auth/axios-instance.ts)
const api = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL });
// Request interceptor: adjunta Bearer token
api.interceptors.request.use((config) => {
const token = useAuthStore.getState().accessToken;
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Response interceptor: 401 → intenta refresh
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401 && !error.config._retry) {
error.config._retry = true;
await useAuthStore.getState().refresh();
return api(error.config);
}
return Promise.reject(error);
}
);
Route Protection (src/middleware.ts)
/dashboard/*→ requiere autenticación/auth/*→ público- Sin token → redirect a
/auth/login
Roles
- SOCIO: acceso total a todos los features
- COLABORADOR: solo ve sus clientes/tareas asignados (el backend filtra por
usuarioId)
Data Fetching — API Real
Service Layer Pattern (igual que el template, pero con Axios)
src/features/<name>/api/
types.ts ← Tipos (response shapes, filters, payloads)
service.ts ← Funciones que llaman a la API via axios-instance
queries.ts ← React Query options + key factories
mutations.ts ← Mutation options (create/update/delete)
service.ts — llamadas reales al backend NestJS:
import api from '@/lib/auth/axios-instance'; // NO usar fetch, NO mock data
import type { Cliente, ClienteFilters } from './types';
export async function getClientes(filters: ClienteFilters): Promise<{ data: Cliente[]; meta: PaginationMeta }> {
const { data } = await api.get('/v1/clientes', { params: filters });
return data; // ya viene con el envelope { data: [...], meta: {...} }
}
export async function getCliente(id: string): Promise<Cliente> {
const { data } = await api.get(`/v1/clientes/${id}`);
return data.data; // el backend envuelve en { data: {...} }
}
export async function createCliente(payload: CreateClientePayload): Promise<Cliente> {
const { data } = await api.post('/v1/clientes', payload);
return data.data;
}
Rutas API backend: siempre /v1/<recurso> (kebab-case plural). Ver apps/api/AGENTS.md para endpoints disponibles.
React Query (mismo patrón del template)
- Server:
void queryClient.prefetchQuery(options)+HydrationBoundary+dehydrate - Client:
useSuspenseQuery(options)+<Suspense fallback={...}> - Mutations:
useMutation({ mutationFn, onSuccess: invalidateQueries }) - Query key factories con invalidación jerárquica
URL State (nuqs)
searchParamsCacheen server componentsuseQueryState+shallow: trueen client components- Tablas:
page,perPage,sort, filtros en URL
Project Structure (actual)
src/
├── app/
│ ├── auth/
│ │ └── login/ ← Login page con user selector
│ └── dashboard/
│ ├── overview/ ← Dashboard con métricas
│ ├── clientes/ ← CRUD clientes (Etapa 1)
│ └── ...
├── components/
│ ├── ui/ ← shadcn/ui (NO modificar)
│ ├── layout/ ← Sidebar, header, PageContainer
│ ├── icons.tsx ← Registro central de iconos (Tabler)
│ └── ...
├── features/
│ ├── auth/ ← Login + auth store
│ ├── users/ ← Gestión de usuarios (React Query + nuqs)
│ │ ├── api/{types, service, queries, mutations}
│ │ └── components/ ← Table, form, cell-action
│ ├── clientes/ ← CRUD clientes (en desarrollo)
│ │ └── api/{types, service, queries, mutations}
│ └── ...
├── config/
│ └── nav-config.ts ← Navegación con roles
├── hooks/
│ ├── use-nav.ts ← Filtrado de nav por rol (SOCIO/COLABORADOR)
│ └── use-data-table.ts ← Estado de tabla
├── lib/
│ ├── auth/
│ │ └── axios-instance.ts ← Axios con interceptor JWT
│ ├── utils.ts ← cn(), formatters
│ └── searchparams.ts ← Search param utilities
├── middleware.ts ← Protección de rutas
└── providers.tsx ← QueryProvider + AuthProvider + ThemeProvider
Navegación y RBAC
nav-config.ts usa roles array (no Clerk access properties):
export const navGroups: NavGroup[] = [
{
label: 'Principal',
items: [
{ title: 'Dashboard', url: '/dashboard/overview', icon: 'dashboard', roles: ['SOCIO', 'COLABORADOR'] },
{ title: 'Clientes', url: '/dashboard/clientes', icon: 'users', roles: ['SOCIO', 'COLABORADOR'] },
{ title: 'Usuarios', url: '/dashboard/users', icon: 'user', roles: ['SOCIO'] }, // solo SOCIO
]
}
];
use-nav.ts filtra según authStore.user.role. Sin rol → hidden. Esto es UX; la seguridad real está en el backend.
Convenciones Específicas del Frontend
Iconos
- Siempre
import { Icons } from '@/components/icons' - Nunca
import { IconX } from '@tabler/icons-react'directo - Para agregar: registrar en
icons.tsx→ usarIcons.keyName
Formularios
useAppFormde@/components/ui/tanstack-form+ Zod schemas enfeatures/<name>/schemas/- Submit buttons: usar
SubmitButton(manejaisSubmittingautomático) - Nunca usar
useStatedentro deAppFieldrender props
Tablas
- Column definitions en
features/<name>/components/<name>-tables/columns.tsx DataTablede@/components/ui/table/data-table.tsx- Filtros via nuqs search params, no estado local
Page Headers
- Usar props de
PageContainer:pageTitle,pageDescription,pageHeaderAction - Nunca importar
<Heading>manualmente
Estilos
cn()para class merging, nunca concatenación manual'use client'solo cuando se usan hooks/browser APIs- Server components por defecto
Environment Variables
NEXT_PUBLIC_API_URL=http://localhost:3001/v1 # URL del backend NestJS
NEXT_PUBLIC_SENTRY_DSN=... # Opcional
NEXT_PUBLIC_SENTRY_DISABLED="true" # Deshabilitar en dev
Ya NO usa: NEXT_PUBLIC_CLERK_*, CLERK_SECRET_KEY.
Build & Development
pnpm dev # desde raíz del monorepo (Turborepo)
pnpm build # build completo
pnpm lint # ESLint
# O desde apps/web directamente:
bun run dev # Next.js dev server en :3000
bun run build
Testing
NO escribir tests a menos que el usuario lo pida explícitamente. Cuando se pidan: Vitest recomendado.
Reglas para el Agente IA (frontend)
- Nunca importar de
@/constants/mock-api*— todo va poraxios-instanceal backend real - Nunca referenciar Clerk — no existe en el proyecto
- Nunca importar iconos directo de
@tabler/icons-react - Nunca modificar
src/components/ui/— extender, no modificar - Seguir el patrón de service layer: types → service → queries → mutations → components
- Leer
apps/api/AGENTS.mdpara conocer los endpoints disponibles antes de crear un service - Un feature a la vez: no tocar múltiples features sin pedirlo
- Nuevos features van en
src/features/<name>/, páginas ensrc/app/dashboard/<name>/ - Navegación se registra en
src/config/nav-config.tsconroles: ['SOCIO']o['SOCIO', 'COLABORADOR'] - Variables de entorno:
NEXT_PUBLIC_para client-side, sin prefijo para server-side
Errores Comunes (Pitfalls)
- Usar
fetch()en vez deaxios-instance→ no tiene interceptor JWT, no maneja refresh - Importar de
@/constants/mock-api*→ son datos falsos del template, el backend es real - Asumir que Clerk existe → no hay
useAuth(),useUser(),@clerk/nextjs - Usar
useQueryen vez deuseSuspenseQuery→ rompe el patrón de streaming SSR - No poner
@Expose()en DTOs del backend → el frontend recibe campos vacíos - Olvidar el prefijo
/v1/en las llamadas a la API - Formato de respuesta: el backend envía
{ data: T, meta?: PaginationMeta }— siempre destructurar{ data }
Estandarización de Código
Reglas obligatorias para todo código nuevo. Aplican también al refactor de código existente.
1. i18n — Todo texto visible va a i18n
- Prohibido hardcodear strings visibles al usuario en componentes.
- Usar
const t = useT()desde@/lib/i18n/clienty llamart('key'). - Las keys viven en
src/locales/{lang}/{namespace}.json. Default language:es-AR. - Convención de naming:
<dominio>.<entidad>.<campo>en snake/camel según contexto.common.cancel,common.save,common.deletepara acciones genéricastarea.title,tarea.add,tarea.confirmDeletepara dominio específicotarea.options.estado.PENDIENTEpara labels de enumstarea.placeholderTitulopara placeholders
- Toast messages también van a i18n:
toast.success(t('tarea.created')). - Si la key no existe,
useTrecibe un fallback como segundo arg:t('foo.bar', { defaultValue: 'Fallback' }).
2. Constants — Una sola fuente de verdad
- Valores de enums (e.g.
PRIORIDAD_VALUES,ESTADO_TAREA_VALUES) →@/constants(re-export de@/constants/tareas.ts). - Labels de enums (e.g.
PRIORIDAD_LABELS,ESTADO_TAREA_LABELS) → mismo lugar, no hardcodear. - Badge variants → funciones
getXxxBadgeVariant(value)desde@/constants/badges.ts. - Mappings de color/clase Tailwind por dominio → constantes en
@/constants/<dominio>.ts(e.g.PRIORIDAD_DOT_CLASS). - Prohibido definir el mismo array/objeto dos veces en distintos archivos.
3. Utils — Lógica reutilizable
- Formateo de fechas →
src/lib/format.ts(formatDate,formatDateShort,formatDateTime,formatRelative). Nuncanew Date().toLocaleDateString(...)inline. - Manejo de className →
cn()desrc/lib/utils.ts. - Formateo específico de dominio (e.g. fecha de vencimiento con fallback "Sin fecha") → función en
src/lib/format.tscon su key i18n correspondiente. - Prohibido duplicar helpers de formato en archivos de feature.
4. Estructura de carpetas
src/constants/— valores, labels, badges, enums, mappings de UIsrc/lib/— utilidades puras (format, utils, parsers, query-client, i18n)src/locales/— archivos de traducciónsrc/components/ui/— shadcn (no modificar)src/features/<name>/— cada feature conapi/,components/,schemas/
5. Toasts
- Mensaje siempre desde i18n:
toast.success(t('tarea.created')). - Errores:
toast.error(t('error.generic'))o específico si existe. - Prohibido strings hardcodeados en
toast.success('Tarea creada').
6. Estilo de imports
- Orden: React → third-party →
@/...→ relativos. - Tipos:
import type { ... }separado de imports de valor cuando son puros. - Constantes: importar desde
@/constants(barrel), nunca desde archivos internos. - Iconos:
import { Icons } from '@/components/icons', nunca directo de@tabler/icons-react.
7. Componentes de feature
'use client'solo si usan hooks/browser APIs.useQuerypara fetch,useMutationpara write — siempre conmutationOptions/queryOptionsdel archivoapi/.- Invalidaciones de cache después de mutations:
getQueryClient().invalidateQueries({ queryKey: <key> }). - Loading:
Skeletonde@/components/ui/skeleton, no spinners inline. - Errores: dejar que el
ResponseExceptionFilterdel backend los muestre, no duplicar.