Files
FRASA-CAS/apps/cotizador/src/components/MisCotizaciones.tsx

144 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import type { CotizacionGuardada } from '../services/api'
import { formatoMoneda } from '../lib/formatters'
interface Props {
cotizaciones: CotizacionGuardada[]
cargando: boolean
error: string | null
/** Restaura la cotización en el cotizador (unidad + parámetros) */
onRetomar: (c: CotizacionGuardada) => void
}
const TITULO_PROPUESTA: Record<string, string> = {
BANCARIO: 'Crédito Bancario',
INFONAVIT: 'Crédito Infonavit-Cofinavit',
COFINAVIT: 'Crédito Infonavit-Cofinavit',
CONTADO: 'Contado',
SEMI_CONTADO: 'Semi Contado',
}
const formatoFechaHora = (iso: string): string =>
new Date(iso).toLocaleString('es-MX', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
/**
* Mensualidades guardadas: "N × $m". Las cotizaciones viejas (plazo 0/null, sin
* mensualidad) muestran "—".
*/
const formatoMensualidades = (c: CotizacionGuardada): string => {
if (c.mensualidad === null || c.numMensualidades === 0) return '—'
return `${c.numMensualidades} × ${formatoMoneda(c.mensualidad)}`
}
/** Sección colapsable "Mis cotizaciones": historial del vendedor logueado */
export function MisCotizaciones({ cotizaciones, cargando, error, onRetomar }: Props) {
const [abierto, setAbierto] = useState(false)
return (
<section aria-labelledby="mis-cotizaciones-titulo">
<div className="flex flex-wrap items-center justify-between gap-4">
<h2
id="mis-cotizaciones-titulo"
className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]"
>
Mis cotizaciones
</h2>
<button
type="button"
aria-expanded={abierto}
aria-controls="mis-cotizaciones-contenido"
onClick={() => setAbierto((v) => !v)}
className="rounded-lg border border-[#D1D5DB] bg-white px-4 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
>
{abierto ? 'Ocultar historial' : `Ver historial (${cotizaciones.length})`}
</button>
</div>
{abierto && (
<div id="mis-cotizaciones-contenido" className="mt-6 rounded-xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
{cargando && <p className="text-sm text-[#64748B] dark:text-[#94A3B8]">Cargando historial…</p>}
{error && <p className="text-xs text-red-700">{error}</p>}
{!cargando && !error && cotizaciones.length === 0 && (
<p className="text-sm text-[#64748B] dark:text-[#94A3B8]">
Aún no tienes cotizaciones guardadas. Se guardan al generar el PDF.
</p>
)}
{cotizaciones.length > 0 && (
<div className="overflow-x-auto">
<table className="w-full min-w-[640px] border-collapse text-sm">
<thead>
<tr className="bg-[#F8FAFC] dark:bg-[#0F172A]">
{['Fecha', 'Folio', 'Cliente', 'Unidad', 'Propuesta', 'Mensualidades', 'Precio final', ''].map(
(h) => (
<th
key={h}
scope="col"
className="p-3 text-left text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]"
>
{h}
</th>
),
)}
</tr>
</thead>
<tbody>
{cotizaciones.map((c) => (
<tr key={c.id} className="hover:bg-[#F8FAFC] dark:bg-[#0F172A]">
<td className="border-t border-[#E5E7EB] dark:border-[#1E293B] p-3 whitespace-nowrap">
{formatoFechaHora(c.createdAt)}
</td>
<td className="border-t border-[#E5E7EB] dark:border-[#1E293B] p-3 whitespace-nowrap">
{c.folio ?? '—'}
</td>
<td className="border-t border-[#E5E7EB] dark:border-[#1E293B] p-3">{c.clienteNombre}</td>
<td className="border-t border-[#E5E7EB] dark:border-[#1E293B] p-3 whitespace-nowrap">
{c.unidadCodigo}
<span className="ml-1 text-xs text-[#64748B] dark:text-[#94A3B8]">{c.desarrollo}</span>
</td>
<td className="border-t border-[#E5E7EB] dark:border-[#1E293B] p-3 whitespace-nowrap">
{TITULO_PROPUESTA[c.tipoPropuesta] ?? c.tipoPropuesta}
</td>
<td className="border-t border-[#E5E7EB] dark:border-[#1E293B] p-3 whitespace-nowrap">
{formatoMensualidades(c)}
</td>
<td className="border-t border-[#E5E7EB] dark:border-[#1E293B] p-3 text-right whitespace-nowrap">
{formatoMoneda(c.precioFinal)}
</td>
<td className="border-t border-[#E5E7EB] dark:border-[#1E293B] p-3 text-right">
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => onRetomar(c)}
className="rounded-lg border border-accent bg-white px-3 py-1.5 text-xs font-medium text-accent transition-all duration-100 ease-out hover:bg-accent hover:text-white"
>
Retomar
</button>
{/* El alta prellenada requiere la cotización en el servidor; las locales (CTE-LOCAL-*) no la tienen */}
{!c.id.startsWith('local-') && (
<a
href={`/client/?cotizacion=${c.id}`}
className="rounded-lg border border-[#D1D5DB] bg-white px-3 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-1.5 text-xs font-medium text-[#374151] dark:text-[#CBD5E1] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
>
Iniciar alta
</a>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
</section>
)
}