App Router Overview
ICL Cotizaciones uses Next.js 16 App Router with file-system based routing, server components by default, and co-located API routes.
The application uses route groups, dynamic routes, and layout-based authentication without implementing Next.js middleware.
Route Tree
src/app/
├── layout.tsx ← Root layout (HTML, Work Sans font)
├── page.tsx ← Redirect: /cotizaciones if logged in, /login if not
│
├── login/
│ └── page.tsx ← Login form (client component)
│
└── (dashboard)/ ← Route group (no URL segment)
├── layout.tsx ← Protected layout: session guard + Header + Sidebar
│
├── cotizaciones/
│ ├── page.tsx ← Quotations list with filters
│ ├── nueva/
│ │ └── page.tsx ← New quotation form
│ └── [id]/
│ └── page.tsx ← View/edit quotation (dynamic route)
│
├── dashboard/
│ ├── general/
│ │ └── page.tsx ← General dashboard (charts)
│ ├── por-cliente/
│ │ └── page.tsx ← Dashboard grouped by client
│ ├── filtradas/
│ │ └── page.tsx ← Dashboard with advanced filters
│ ├── por-vendedor/
│ │ └── page.tsx ← Dashboard grouped by sales rep
│ └── contactos/
│ └── page.tsx ← Contacts dashboard
│
└── maestros/
├── clientes/
│ └── page.tsx ← Clients CRUD (admin via API; sidebar hidden for non-admin)
├── origenes/
│ └── page.tsx ← Origins CRUD
├── vias/
│ └── page.tsx ← Routes CRUD
├── acuerdos/
│ └── page.tsx ← Commercial agreements CRUD
├── usuarios/
│ └── page.tsx ← Users CRUD
├── locaciones/
│ └── page.tsx ← Locations CRUD (origins + routes)
├── pricing-netos/
│ └── page.tsx ← Net pricing table (unified FCL + LCL)
└── tarifario-puertos-base/
└── page.tsx ← Base port rates
Route Table
Public Routes
URL Purpose Layout Auth Required Data Source /Smart redirect Root No (reads session) getSession() server-side/loginLogin form Root No POST /api/auth/login
Protected Routes (Dashboard)
All routes under (dashboard)/ are protected by the layout session guard.
Quotations
URL Purpose Data APIs /cotizacionesPaginated quotations list GET /api/cotizaciones/cotizaciones/nuevaCreate new quotation GET /api/cotizaciones/next-nroGET /api/clientesGET /api/origenesGET /api/viasGET /api/usuariosGET /api/auth/meGET /api/acuerdosGET /api/tarifario-puertos-base/cotizaciones/[id]View/edit quotation GET /api/cotizaciones/[id] Same APIs as /nueva
Dashboards
URL Purpose Data APIs /dashboard/generalGeneral charts GET /api/dashboard?tipo=general/dashboard/por-clienteClient metrics GET /api/dashboard?tipo=por-cliente/dashboard/filtradasFiltered dashboard GET /api/dashboard?tipo=filtradas/dashboard/por-vendedorSales rep metrics GET /api/dashboard?tipo=por-vendedor/dashboard/contactosContacts dashboard GET /api/dashboard?tipo=contactos
Master Data (Admin Only)
The sidebar shows master data sections only for admin roles (DIRECTOR, GERENTE, ADMINISTRACION). API endpoints enforce authorization with isAdmin() checks.
URL Purpose Data APIs /maestros/clientesClients CRUD GET/POST /api/clientesPUT/DELETE /api/clientes/[id]/maestros/origenesOrigins CRUD GET/POST /api/origenesPUT/DELETE /api/origenes/[id]/maestros/viasRoutes CRUD GET/POST /api/viasPUT/DELETE /api/vias/[id]/maestros/acuerdosCommercial agreements CRUD GET/POST /api/acuerdosGET/PUT/DELETE /api/acuerdos/[id]/maestros/usuariosUsers CRUD GET/POST /api/usuariosPUT/DELETE /api/usuarios/[id]/maestros/locacionesLocations CRUD GET/POST /api/origenesGET/POST /api/vias Plus respective [id] routes/maestros/pricing-netosNet pricing table GET/POST /api/pricing-netosPUT/DELETE /api/pricing-netos/[id]/maestros/tarifario-puertos-baseBase port rates GET/POST /api/tarifario-puertos-basePUT/DELETE /api/tarifario-puertos-base/[id]
Master data pages are client components without built-in guards. Effective protection is enforced at the API level. Non-admin users who manually navigate to /maestros/* cannot perform mutations.
API Routes
Authentication
Endpoint Methods Access Control Purpose /api/auth/loginPOST Public Validate credentials, create session /api/auth/logoutPOST Authenticated Destroy session /api/auth/meGET Authenticated Get current user data
Quotations
Endpoint Methods Access Control Purpose /api/cotizacionesGET Filtered by user_id for non-admin List quotations /api/cotizacionesPOST Any authenticated user Create quotation /api/cotizaciones/[id]GET Owner or admin Get quotation by ID /api/cotizaciones/[id]PUT Owner or admin Update quotation /api/cotizaciones/[id]DELETE Owner or admin Delete quotation /api/cotizaciones/next-nroGET Authenticated Generate next quote number
Master Data
Clients
Users
Locations
Agreements
Pricing
Endpoint Methods Access Control /api/clientesGET Any authenticated user /api/clientesPOST Admin only /api/clientes/[id]PUT Admin only /api/clientes/[id]DELETE Admin only
Endpoint Methods Access Control /api/usuariosGET Admin only /api/usuariosPOST Admin only /api/usuarios/[id]PUT Admin only /api/usuarios/[id]DELETE Admin only
Endpoint Methods Access Control /api/origenesGET Any authenticated user /api/origenesPOST Admin only /api/origenes/[id]PUT Admin only /api/origenes/[id]DELETE Admin only /api/viasGET Any authenticated user /api/viasPOST Admin only /api/vias/[id]PUT Admin only /api/vias/[id]DELETE Admin only
Endpoint Methods Access Control /api/acuerdosGET Any authenticated user /api/acuerdosPOST Admin only /api/acuerdos/[id]GET Any authenticated user /api/acuerdos/[id]PUT Admin only /api/acuerdos/[id]DELETE Admin only
Endpoint Methods Access Control /api/pricing-netosGET Any authenticated user /api/pricing-netosPOST Admin only /api/pricing-netos/[id]PUT Admin only /api/pricing-netos/[id]DELETE Admin only /api/tarifario-puertos-baseGET Any authenticated user /api/tarifario-puertos-basePOST Admin only /api/tarifario-puertos-base/[id]PUT Admin only /api/tarifario-puertos-base/[id]DELETE Admin only
Analytics
Endpoint Methods Access Control Purpose /api/dashboardGET Authenticated (filtered by role) Dashboard data with ?tipo= query param
Admin role = DIRECTOR, GERENTE, or ADMINISTRACION (see src/lib/utils.ts:isAdmin() at line 36).
Route Groups
(dashboard) Group
The (dashboard) folder is a route group that:
Does not create a URL segment (/dashboard does not exist)
Shares a common layout ((dashboard)/layout.tsx)
Applies session guard to all nested routes
Renders Header and Sidebar for authenticated users
Why use route groups?
Organize related routes without affecting URLs
Share layouts across multiple route segments
Apply authentication guards at a single point
Layout Hierarchy
Root Layout (app/layout.tsx)
├── Applies global styles and fonts
├── Wraps entire application
│
└── Dashboard Layout ((dashboard)/layout.tsx)
├── Checks session with getSession()
├── Redirects to /login if not authenticated
├── Renders Header component
├── Renders Sidebar component (filtered by role)
└── Renders page content in <main>
Root Layout
// src/app/layout.tsx
import { Work_Sans } from "next/font/google" ;
import "./globals.css" ;
const workSans = Work_Sans ({ subsets: [ "latin" ] });
export default function RootLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
return (
< html lang = "es" >
< body className = {workSans. className } > { children } </ body >
</ html >
);
}
Dashboard Layout
// src/app/(dashboard)/layout.tsx
import { redirect } from "next/navigation" ;
import { getSession } from "@/lib/session" ;
import { Header } from "@/components/Header" ;
import { Sidebar } from "@/components/Sidebar" ;
export default async function DashboardLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
const session = await getSession ();
if ( ! session . isLoggedIn ) {
redirect ( "/login" );
}
return (
< div className = "flex h-screen" >
< Sidebar role = {session. rol } />
< div className = "flex-1 flex flex-col" >
< Header user = { session } />
< main className = "flex-1 overflow-auto p-6" >
{ children }
</ main >
</ div >
</ div >
);
}
Dynamic Routes
Quotation Detail [id]
Dynamic route for viewing and editing individual quotations.
// src/app/(dashboard)/cotizaciones/[id]/page.tsx
import { db } from "@/db/db" ;
import { quotations } from "@/db/schema" ;
import { eq } from "drizzle-orm" ;
interface PageProps {
params : { id : string };
}
export default async function QuotationPage ({ params } : PageProps ) {
const quotation = await db
. select ()
. from ( quotations )
. where ( eq ( quotations . id , parseInt ( params . id )))
. get ();
if ( ! quotation ) {
return < div > Cotización no encontrada </ div > ;
}
return < QuotationForm initialData ={ quotation } mode = "edit" />;
}
URL Examples:
/cotizaciones/1 → params.id = "1"
/cotizaciones/42 → params.id = "42"
API Dynamic Routes
API routes also support dynamic segments.
// src/app/api/cotizaciones/[id]/route.ts
import { NextResponse } from "next/server" ;
import { db } from "@/db/db" ;
import { quotations } from "@/db/schema" ;
import { eq } from "drizzle-orm" ;
import { getSession } from "@/lib/session" ;
interface RouteContext {
params : { id : string };
}
export async function GET (
request : Request ,
{ params } : RouteContext
) {
const session = await getSession ();
if ( ! session . isLoggedIn ) {
return NextResponse . json (
{ error: "No autorizado" },
{ status: 401 }
);
}
const quotation = await db
. select ()
. from ( quotations )
. where ( eq ( quotations . id , parseInt ( params . id )))
. get ();
if ( ! quotation ) {
return NextResponse . json (
{ error: "No encontrado" },
{ status: 404 }
);
}
// Check ownership
if ( ! isAdmin ( session . rol ) && quotation . user_id !== session . userId ) {
return NextResponse . json (
{ error: "Acceso denegado" },
{ status: 403 }
);
}
return NextResponse . json ( quotation );
}
Redirect Logic
Root Page Redirect
// src/app/page.tsx
import { redirect } from "next/navigation" ;
import { getSession } from "@/lib/session" ;
export default async function HomePage () {
const session = await getSession ();
if ( session . isLoggedIn ) {
redirect ( "/cotizaciones" );
} else {
redirect ( "/login" );
}
}
After Login Redirect
// src/app/login/page.tsx (client component)
"use client" ;
import { useRouter } from "next/navigation" ;
import { useState } from "react" ;
export default function LoginPage () {
const router = useRouter ();
const [ error , setError ] = useState ( "" );
async function handleSubmit ( e : React . FormEvent < HTMLFormElement >) {
e . preventDefault ();
const formData = new FormData ( e . currentTarget );
const res = await fetch ( "/api/auth/login" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
email: formData . get ( "email" ),
password: formData . get ( "password" ),
}),
});
if ( res . ok ) {
router . push ( "/cotizaciones" ); // Redirect on success
} else {
setError ( "Credenciales inválidas" );
}
}
return < form onSubmit ={ handleSubmit }>{ /* ... */ } </ form > ;
}
No Middleware Implementation
The application does not implement Next.js middleware (middleware.ts). All authentication is handled via:
Layout-level session checks (server-side redirect)
API route manual guards
This approach is simpler but means authentication logic is duplicated across API routes.
Alternative with Middleware:
// middleware.ts (NOT currently implemented)
import { NextResponse } from "next/server" ;
import type { NextRequest } from "next/server" ;
import { getSession } from "@/lib/session" ;
export async function middleware ( request : NextRequest ) {
const session = await getSession ();
// Protect all routes under /cotizaciones, /dashboard, /maestros
if (
request . nextUrl . pathname . startsWith ( "/cotizaciones" ) ||
request . nextUrl . pathname . startsWith ( "/dashboard" ) ||
request . nextUrl . pathname . startsWith ( "/maestros" )
) {
if ( ! session . isLoggedIn ) {
return NextResponse . redirect ( new URL ( "/login" , request . url ));
}
}
return NextResponse . next ();
}
export const config = {
matcher: [ "/((?!api|_next/static|_next/image|favicon.ico).*)" ],
};
Best Practices
Server Components by Default
Use server components for pages that fetch data. Only add "use client" when you need:
Event handlers (onClick, onChange, etc.)
React hooks (useState, useEffect, etc.)
Browser APIs (window, localStorage, etc.)
Keep API routes in app/api/ organized by resource: app/api/
├── auth/
├── cotizaciones/
├── clientes/
└── usuarios/
Use Route Groups for Layout Sharing
Route groups like (dashboard) allow you to:
Share layouts without creating URL segments
Apply authentication guards at a single point
Organize related routes logically
Always check session status at the top of:
Protected layouts (redirect if not logged in)
API route handlers (return 401 if not authenticated)
Server actions (validate before mutations)
Authentication Learn about iron-session and authorization guards
API Reference Browse all API endpoints
Next.js App Router Official Next.js App Router documentation
Database Schema View complete database schema and relationships