# Guía para Integrar Reportes de Asterisk (CDR y CEL)

Este documento te guiará paso a paso para añadir una sección de reportes en tu panel de administración, mostrando los datos de las tablas `cdr` y `cel` de Asterisk que ya tienes en tu base de datos PostgreSQL.

## Arquitectura Propuesta

Seguiremos el patrón que ya tienes en tu aplicación:

1.  **Esquema Drizzle**: Definiremos las tablas `cdr` y `cel` para que Drizzle las conozca.
2.  **API Endpoints**: Crearemos rutas en `src/app/api/` para exponer los datos de CDR y CEL de forma segura y paginada.
3.  **Componentes de Frontend**: Construiremos una nueva página de reportes y un componente de tabla reutilizable (similar a `UsersTable`) para mostrar los datos.
4.  **Navegación**: Añadiremos el nuevo apartado al menú lateral (sidebar).

---

### Paso 1: Definir los Esquemas de Drizzle para CDR y CEL

Como las tablas ya existen en la base de datos, no necesitamos generar una migración (`db:generate`), solo necesitamos que Drizzle conozca su estructura.

1.  Crea un nuevo archivo para los esquemas de Asterisk: `src/server/db/schemas/asterisk.ts`.

2.  Añade el siguiente contenido a `src/server/db/schemas/asterisk.ts`. Este código traduce las tablas SQL del "Taller 8" al formato de Drizzle:

    ```ts
    // src/server/db/schemas/asterisk.ts
    import { integer, pgTable, text, timestamp, serial } from "drizzle-orm/pg-core";

    // Esquema para la tabla CDR (Call Detail Records)
    export const cdr = pgTable("cdr", {
      calldate: timestamp("calldate", { withTimezone: true, mode: 'string' }).defaultNow(),
      clid: text("clid").default(''),
      src: text("src").default(''),
      dst: text("dst").default(''),
      dcontext: text("dcontext").default(''),
      channel: text("channel").default(''),
      dstchannel: text("dstchannel").default(''),
      lastapp: text("lastapp").default(''),
      lastdata: text("lastdata").default(''),
      duration: integer("duration").default(0),
      billsec: integer("billsec").default(0),
      disposition: text("disposition").default(''),
      amaflags: integer("amaflags").default(0),
      accountcode: text("accountcode").default(''),
      uniqueid: text("uniqueid").primaryKey().default(''), // Asumimos uniqueid como PK para simplicidad
      linkedid: text("linkedid").default(''),
      peeraccount: text("peeraccount").default(''),
      sequence: integer("sequence").default(0),
      userfield: text("userfield").default(''),
      recording_path: text("recording_path"), // Puede ser null
    });

    // Esquema para la tabla CEL (Channel Event Logging)
    export const cel = pgTable("cel", {
      id: serial("id").primaryKey(),
      eventtime: timestamp("eventtime", { withTimezone: true, mode: 'string' }).defaultNow(),
      eventtype: text("eventtype").default(''),
      userdeftype: text("userdeftype").default(''),
      cid_name: text("cid_name").default(''),
      cid_num: text("cid_num").default(''),
      cid_ani: text("cid_ani").default(''),
      cid_rdnis: text("cid_rdnis").default(''),
      cid_dnid: text("cid_dnid").default(''),
      exten: text("exten").default(''),
      context: text("context").default(''),
      channame: text("channame").default(''),
      appname: text("appname").default(''),
      appdata: text("appdata").default(''),
      amaflags: integer("amaflags").default(0),
      accountcode: text("accountcode").default(''),
      peeraccount: text("peeraccount").default(''),
      uniqueid: text("uniqueid").default(''),
      linkedid: text("linkedid").default(''),
      userfield: text("userfield").default(''),
      peer: text("peer").default(''),
    });
    ```

3.  Ahora, exporta estos nuevos esquemas desde el archivo principal de esquemas. Modifica `src/server/db/schemas/index.ts`:

    ```ts
    // src/server/db/schemas/index.ts
    export * from "./auth";
    export * from "./asterisk"; // Añade esta línea
    ```

### Paso 2: Crear el Endpoint de la API para el Reporte CDR

Crearemos una API para obtener los registros CDR de forma paginada y con capacidad de búsqueda, siguiendo el patrón de tu API de usuarios.

1.  Crea la siguiente estructura de carpetas: `src/app/api/reportes/cdr/`.

2.  Dentro de esa carpeta, crea un archivo `route.ts` con el siguiente contenido:

    ```ts
    // src/app/api/reportes/cdr/route.ts
    import { headers } from "next/headers";
    import { NextResponse } from "next/server";

    import { db } from "@/server/db";
    import { cdr } from "@/server/db/schemas";
    import { and, desc, eq, like, or, sql } from "drizzle-orm";

    import { auth } from "@/lib/auth/auth";

    /**
     * GET /api/reportes/cdr
     * Obtiene la lista de registros CDR con paginación y filtros.
     * Accesible para admin y superAdmin.
     */
    export async function GET(request: Request) {
      try {
        const session = await auth.api.getSession({
          headers: await headers(),
        });

        if (!session) {
          return NextResponse.json(
            { success: false, error: "No autorizado" },
            { status: 401 },
          );
        }

        const userRole = (session.user as { role?: string }).role ?? "user";
        if (userRole !== "admin" && userRole !== "superAdmin") {
          return NextResponse.json(
            { success: false, error: "No tienes permisos" },
            { status: 403 },
          );
        }

        const { searchParams } = new URL(request.url);
        const page = parseInt(searchParams.get("page") ?? "1");
        const limit = parseInt(searchParams.get("limit") ?? "15");
        const offset = (page - 1) * limit;
        const search = searchParams.get("search");

        // Construir filtros
        const filters = [];
        if (search) {
          filters.push(
            or(
              like(cdr.src, `%${search}%`),
              like(cdr.dst, `%${search}%`),
              like(cdr.accountcode, `%${search}%`),
            ),
          );
        }
        
        // Consulta principal para obtener los datos paginados
        const cdrData = await db
          .select()
          .from(cdr)
          .where(filters.length > 0 ? and(...filters) : undefined)
          .orderBy(desc(cdr.calldate)) // Ordenar por más reciente
          .limit(limit)
          .offset(offset);

        // Contar el total de registros que coinciden con el filtro
        const totalResult = await db
          .select({ count: sql<number>`count(*)::int` })
          .from(cdr)
          .where(filters.length > 0 ? and(...filters) : undefined);
          
        const total = totalResult[0]?.count ?? 0;
        const totalPages = Math.ceil(total / limit);

        return NextResponse.json({
          success: true,
          data: cdrData,
          pagination: {
            page,
            limit,
            total,
            totalPages,
          },
        });
      } catch (error) {
        console.error("[API /reportes/cdr] Error:", error);
        return NextResponse.json(
          { success: false, error: "Error al obtener registros CDR" },
          { status: 500 },
        );
      }
    }
    ```

### Paso 3: Crear la Página y Componente de Frontend

Ahora construiremos la interfaz de usuario para visualizar los datos.

1.  Crea la estructura de la nueva página: `src/app/(dashboard)/reportes/cdr/`.

2.  Dentro, crea el archivo `page.tsx` que actuará como el contenedor principal:

    ```tsx
    // src/app/(dashboard)/reportes/cdr/page.tsx
    "use client";

    import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
    import { CdrTable } from "@/components/reportes/CdrTable"; // Crearemos este componente a continuación

    export default function CdrReportPage() {
      return (
        <div className="space-y-6">
          {/* Header */}
          <div>
            <h1 className="text-3xl font-bold text-slate-900">Reporte de Llamadas (CDR)</h1>
            <p className="text-slate-600">
              Historial detallado de todas las llamadas procesadas.
            </p>
          </div>

          {/* Tabla de CDRs */}
          <Card>
            <CardHeader>
              <CardTitle>Registros de Llamadas</CardTitle>
            </CardHeader>
            <CardContent>
              <CdrTable />
            </CardContent>
          </Card>
        </div>
      );
    }
    ```

3.  Ahora, crea el componente principal para la tabla. Crea la carpeta `src/components/reportes/` y dentro el archivo `CdrTable.tsx`. Este componente se basará en tu `UsersTable.tsx`.

    ```tsx
    // src/components/reportes/CdrTable.tsx
    "use client";

    import { useEffect, useState } from "react";
    import { ChevronLeft, ChevronRight, Search } from "lucide-react";

    import { Badge } from "@/components/ui/badge";
    import { Button } from "@/components/ui/button";
    import { Input } from "@/components/ui/input";
    import { Skeleton } from "@/components/ui/skeleton";
    import {
      Table,
      TableBody,
      TableCell,
      TableHead,
      TableHeader,
      TableRow,
    } from "@/components/ui/table";

    // Define una interfaz para los datos de CDR y paginación
    interface CdrRecord {
      calldate: string;
      src: string;
      dst: string;
      disposition: string;
      duration: number;
      billsec: number;
      accountcode: string;
      uniqueid: string;
      recording_path: string | null;
    }

    interface CdrResponse {
      success: boolean;
      data: CdrRecord[];
      pagination: {
        page: number;
        limit: number;
        total: number;
        totalPages: number;
      };
    }

    export function CdrTable() {
      const [records, setRecords] = useState<CdrRecord[]>([]);
      const [loading, setLoading] = useState(true);
      const [search, setSearch] = useState("");
      const [page, setPage] = useState(1);
      const [pagination, setPagination] = useState({
        total: 0,
        totalPages: 0,
        limit: 15,
      });

      const fetchRecords = async (currentPage = page, currentSearch = search) => {
        setLoading(true);
        try {
          const params = new URLSearchParams({
            page: currentPage.toString(),
            limit: pagination.limit.toString(),
          });

          if (currentSearch) {
            params.append("search", currentSearch);
          }

          const response = await fetch(`/api/reportes/cdr?${params.toString()}`);
          const data = (await response.json()) as CdrResponse;

          if (data.success) {
            setRecords(data.data);
            setPagination(data.pagination);
          }
        } catch (error) {
          console.error("Error fetching CDR records:", error);
        } finally {
          setLoading(false);
        }
      };

      useEffect(() => {
        void fetchRecords(page, search);
      }, [page]);

      const handleSearch = () => {
        setPage(1); // Reinicia a la primera página al buscar
        void fetchRecords(1, search);
      };
      
      const getDispositionBadge = (disposition: string) => {
          switch (disposition) {
              case 'ANSWERED':
                  return <Badge className="bg-green-500">Contestada</Badge>;
              case 'NO ANSWER':
                  return <Badge variant="secondary">No Contestada</Badge>;
              case 'BUSY':
                  return <Badge variant="destructive" className="bg-yellow-500">Ocupado</Badge>;
              case 'FAILED':
                  return <Badge variant="destructive">Fallida</Badge>;
              default:
                  return <Badge variant="outline">{disposition}</Badge>;
          }
      }

      return (
        <div className="space-y-4">
          {/* Búsqueda */}
          <div className="flex gap-2">
            <div className="relative flex-1">
              <Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-slate-400" />
              <Input
                placeholder="Buscar por origen, destino, tipo..."
                value={search}
                onChange={(e) => setSearch(e.target.value)}
                onKeyDown={(e) => e.key === "Enter" && handleSearch()}
                className="pl-9"
              />
            </div>
            <Button onClick={handleSearch} disabled={loading}>Buscar</Button>
          </div>

          {/* Tabla */}
          <div className="rounded-lg border border-slate-200">
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>Fecha y Hora</TableHead>
                  <TableHead>Origen</TableHead>
                  <TableHead>Destino</TableHead>
                  <TableHead>Resultado</TableHead>
                  <TableHead>Duración (Total/Voz)</TableHead>
                  <TableHead>Tipo</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {loading ? (
                  Array.from({ length: 5 }).map((_, i) => (
                    <TableRow key={i}>
                      <TableCell colSpan={6}><Skeleton className="h-6 w-full" /></TableCell>
                    </TableRow>
                  ))
                ) : records.length === 0 ? (
                  <TableRow>
                    <TableCell colSpan={6} className="text-center text-slate-500">
                      No se encontraron registros.
                    </TableCell>
                  </TableRow>
                ) : (
                  records.map((rec) => (
                    <TableRow key={rec.uniqueid}>
                      <TableCell>{new Date(rec.calldate).toLocaleString()}</TableCell>
                      <TableCell className="font-medium">{rec.src}</TableCell>
                      <TableCell>{rec.dst}</TableCell>
                      <TableCell>{getDispositionBadge(rec.disposition)}</TableCell>
                      <TableCell>{rec.duration}s / {rec.billsec}s</TableCell>
                      <TableCell><Badge variant="outline">{rec.accountcode || 'N/A'}</Badge></TableCell>
                    </TableRow>
                  ))
                )}
              </TableBody>
            </Table>
          </div>

          {/* Paginación */}
          {pagination.totalPages > 1 && (
            <div className="flex items-center justify-between">
              <p className="text-sm text-slate-600">
                Mostrando {records.length} de {pagination.total} registros
              </p>
              <div className="flex gap-2">
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => setPage((p) => Math.max(1, p - 1))}
                  disabled={page === 1 || loading}
                >
                  <ChevronLeft className="h-4 w-4" /> Anterior
                </Button>
                <span className="flex items-center text-sm text-slate-600">
                  Página {page} de {pagination.totalPages}
                </span>
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => setPage((p) => Math.min(pagination.totalPages, p + 1))}
                  disabled={page === pagination.totalPages || loading}
                >
                  Siguiente <ChevronRight className="h-4 w-4" />
                </Button>
              </div>
            </div>
          )}
        </div>
      );
    }
    ```

### Paso 4: Añadir Enlace de Navegación en el Sidebar

Para que los usuarios puedan acceder a la nueva página, agrégala al menú lateral.

1.  Abre el archivo `src/components/section/Sidebar.tsx`.
2.  Importa un ícono adecuado, por ejemplo `FileClock` de `lucide-react`.

    ```tsx
    import { Home, Settings, User, Users, FileClock } from "lucide-react";
    ```

3.  Añade la nueva ruta al array `navigation`. Puedes crear una nueva "sección" lógica para los reportes.

    ```tsx
    // src/components/section/Sidebar.tsx

    const navigation = [
      {
        name: "Dashboard",
        href: "/dashboard",
        icon: Home,
      },
      // ... otros items ...
      {
        name: "Reporte CDR", // Nuevo item
        href: "/reportes/cdr", // Ruta de la nueva página
        icon: FileClock, // Ícono importado
        adminOnly: true, // Solo visible para administradores
      },
      {
        name: "Mi Perfil",
        href: "/dashboard/perfil",
        icon: User,
      },
      // ... resto de items ...
    ];
    ```
    *Nota: Tu ruta completa será `/dashboard/reportes/cdr` por la estructura de carpetas, pero `declarative-routing` podría generar una URL diferente si no configuras los `layout groups` para que sean ignorados. Por ahora, `/reportes/cdr` debería funcionar si lo añades al sidebar.*

### Paso 5: Actualizar el Sistema de Rutas (Declarative Routing)

Tu proyecto utiliza `declarative-routing`, así que debemos registrar la nueva página.

1.  Dentro de `src/app/(dashboard)/reportes/cdr/`, crea el archivo `page.info.ts`.

2.  Añade el siguiente contenido:

    ```ts
    // src/app/(dashboard)/reportes/cdr/page.info.ts
    import { z } from "zod";

    export const Route = {
      name: "DashboardReportesCdr",
      params: z.object({}),
    };
    ```

3.  Finalmente, actualiza las rutas generadas ejecutando en tu terminal:

    ```bash
    pnpm dr:build
    ```

    Esto regenerará `src/routes/index.ts` para incluir tu nueva ruta `DashboardReportesCdr`.

---

### Ejemplos de Consultas SQL para Inspiración

Puedes usar estas consultas (extraídas del "Taller 8") como base para crear nuevos endpoints de API y visualizaciones en tu dashboard.

**Reporte Diario Ejecutivo:**

```sql
SELECT
    DATE(calldate) as fecha,
    COUNT(*) as total_llamadas,
    SUM(CASE WHEN disposition = 'ANSWERED' THEN 1 ELSE 0 END) as contestadas,
    ROUND( (SUM(CASE WHEN disposition = 'ANSWERED' THEN 1 ELSE 0 END) * 100.0 / COUNT(*)), 2) as tasa_exito_porcentaje,
    SUM(CASE WHEN disposition = 'ANSWERED' THEN billsec ELSE 0 END) / 60 as tiempo_total_minutos
FROM cdr
WHERE calldate >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY DATE(calldate)
ORDER BY fecha DESC;
```

**Top 5 Destinos más llamados (últimos 30 días):**

```sql
SELECT
    dst as destino,
    COUNT(*) as llamadas,
    SUM(billsec) as tiempo_total_seg
FROM cdr
WHERE calldate >= CURRENT_DATE - INTERVAL '30 days'
    AND accountcode LIKE '%SALIENTE%'
GROUP BY dst
HAVING COUNT(*) >= 2
ORDER BY llamadas DESC
LIMIT 5;
```

**Flujo completo de una llamada usando CEL:**

```sql
-- Reemplaza 'UNIQUE_ID_DE_UNA_LLAMADA'
SELECT
    to_char(eventtime, 'HH24:MI:SS.MS') as tiempo,
    eventtype,
    cid_num as callerid,
    exten,
    context,
    channame,
    appname
FROM cel
WHERE uniqueid = 'UNIQUE_ID_DE_UNA_LLAMADA'
ORDER BY eventtime;
```

### Próximos Pasos

1.  **Reporte CEL**: Crea una estructura similar (`/api/reportes/cel/[uniqueid]`, página y componente) para mostrar los eventos detallados de una llamada específica al hacer clic en una fila de la tabla CDR.
2.  **Filtros Avanzados**: Añade filtros por rango de fechas, por `disposition` o por `accountcode` a la API y al frontend.
3.  **Dashboard de Métricas**: Crea una página principal de "Dashboard de Reportes" con tarjetas (`Card`) que muestren resúmenes usando las consultas de ejemplo (ej. total de llamadas hoy, duración promedio, etc.).
4.  **Grabaciones**: Si el campo `recording_path` tiene un valor, muestra un ícono o botón para reproducir o descargar la grabación. Esto requerirá una configuración adicional para servir los archivos de audio de forma segura.