Files
cotizador/src/app/(app)/page.tsx
T
urieljarethandClaude Fable 5 69c74faabb Restaurar funcionalidades desde stash + setup de despliegue Coolify
- Recupera el trabajo revertido (doble propuesta, retainer, niveles, API Python) desde el stash de GitHub Desktop
- Dockerfile multi-stage para Next.js (migraciones automáticas al arrancar, seed opcional via RUN_SEED)
- docker-compose.coolify.yml: postgres + web + api con healthchecks y SERVICE_FQDN_*
- Endpoint público /api/health (verifica BD) para healthchecks
- seed.ts parametrizado: conexión DB_* y credenciales SEED_* por entorno (sin passwords hardcodeadas)
- .env.example, .dockerignore, uvicorn con --proxy-headers
- Limpieza: .xlsx y __pycache__ fuera del repo

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 02:21:18 -06:00

191 lines
6.5 KiB
TypeScript

import Link from "next/link";
import { FileText, Plus, Users, Database } from "lucide-react";
import { prisma } from "@/lib/db";
import { formatCurrency, formatDate } from "@/lib/calculators";
import { EstadoBadge } from "@/components/EstadoBadge";
export const dynamic = "force-dynamic";
export default async function DashboardPage() {
const [
totalCotizaciones,
cotizacionesRecientes,
totalClientes,
totalServicios,
] = await Promise.all([
prisma.cotizacion.count(),
prisma.cotizacion.findMany({
take: 10,
orderBy: { createdAt: "desc" },
select: {
id: true,
numero: true,
fecha: true,
estado: true,
cliente: { select: { nombre: true, empresa: true } },
servicios: {
select: { tipoPago: true, seleccionado: true, precio: true },
},
},
}),
prisma.cliente.count(),
prisma.servicioCatalogo.count({ where: { activo: true } }),
]);
const stats = [
{
label: "Cotizaciones",
value: totalCotizaciones,
icon: FileText,
color: "text-primary",
bg: "bg-primary-light",
},
{
label: "Clientes",
value: totalClientes,
icon: Users,
color: "text-secondary",
bg: "bg-purple-100",
},
{
label: "Servicios Activos",
value: totalServicios,
icon: Database,
color: "text-accent",
bg: "bg-cyan-100",
},
];
return (
<div>
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold text-foreground">Dashboard</h1>
<p className="text-muted text-sm mt-1">
Bienvenido al Cotizador E3
</p>
</div>
<Link
href="/cotizaciones/nueva"
className="flex items-center gap-2 px-4 py-2.5 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium text-sm"
>
<Plus className="w-4 h-4" />
Nueva Cotizacion
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
{stats.map((stat) => (
<div
key={stat.label}
className="bg-card-bg rounded-xl border border-border p-5"
>
<div className="flex items-center gap-3">
<div className={`${stat.bg} p-2.5 rounded-lg`}>
<stat.icon className={`w-5 h-5 ${stat.color}`} />
</div>
<div>
<p className="text-2xl font-bold">{stat.value}</p>
<p className="text-muted text-sm">{stat.label}</p>
</div>
</div>
</div>
))}
</div>
<div className="bg-card-bg rounded-xl border border-border">
<div className="px-5 py-4 border-b border-border">
<h2 className="font-semibold text-lg">Cotizaciones Recientes</h2>
</div>
{cotizacionesRecientes.length === 0 ? (
<div className="p-8 text-center">
<FileText className="w-12 h-12 text-muted mx-auto mb-3" />
<p className="text-muted">
No hay cotizaciones. Crea la primera.
</p>
<Link
href="/cotizaciones/nueva"
className="inline-flex items-center gap-2 mt-4 px-4 py-2 bg-primary text-white rounded-lg text-sm font-medium hover:bg-primary-dark"
>
<Plus className="w-4 h-4" />
Nueva Cotizacion
</Link>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left px-5 py-3 font-medium text-muted">
No.
</th>
<th className="text-left px-5 py-3 font-medium text-muted">
Cliente
</th>
<th className="text-left px-5 py-3 font-medium text-muted">
Fecha
</th>
<th className="text-left px-5 py-3 font-medium text-muted">
Estado
</th>
<th className="text-right px-5 py-3 font-medium text-muted">
Servicios
</th>
</tr>
</thead>
<tbody>
{cotizacionesRecientes.map((cot) => {
const totalUnico = cot.servicios
.filter((s) => s.tipoPago === "unico" && s.seleccionado)
.reduce((sum, s) => sum + s.precio, 0);
const totalMensual = cot.servicios
.filter(
(s) => s.tipoPago === "mensual" && s.seleccionado
)
.reduce((sum, s) => sum + s.precio, 0);
return (
<tr
key={cot.id}
className="border-b border-border hover:bg-gray-50 transition-colors"
>
<td className="px-5 py-3">
<Link
href={`/cotizaciones/${cot.id}`}
className="text-primary font-medium hover:underline"
>
{cot.numero}
</Link>
</td>
<td className="px-5 py-3">
{cot.cliente.empresa || cot.cliente.nombre}
</td>
<td className="px-5 py-3 text-muted">
{formatDate(cot.fecha)}
</td>
<td className="px-5 py-3">
<EstadoBadge estado={cot.estado} />
</td>
<td className="px-5 py-3 text-right font-medium">
{totalUnico > 0 && (
<span className="block text-xs text-muted">
Unico: {formatCurrency(totalUnico)}
</span>
)}
{totalMensual > 0 && (
<span className="block text-xs text-muted">
Mensual: {formatCurrency(totalMensual)}
</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}