144 lines
6.6 KiB
TypeScript
144 lines
6.6 KiB
TypeScript
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>
|
||
)
|
||
}
|