- Modal con 28 campos agrupados por sección (Datos, Contacto, Crédito, Propiedad, Marketing, Venta) - Click en fila abre modal - Dark mode completo - Agregado 'clientes-maestro' a todos los roles
183 lines
8.9 KiB
TypeScript
183 lines
8.9 KiB
TypeScript
import { useMemo } from 'react'
|
||
import type { ClienteMaestro } from '../services/clientes-maestro'
|
||
|
||
interface Props {
|
||
datos: ClienteMaestro[]
|
||
total: number
|
||
pagina: number
|
||
limite: number
|
||
cargando: boolean
|
||
onPageChange: (pagina: number) => void
|
||
onExportar: () => void
|
||
onRowClick?: (cliente: ClienteMaestro) => void
|
||
}
|
||
|
||
/** Tabla paginada de clientes maestro con dark mode completo */
|
||
export function ClientesMaestroTable({ datos, total, pagina, limite, cargando, onPageChange, onExportar, onRowClick }: Props) {
|
||
const totalPaginas = Math.ceil(total / limite)
|
||
const inicio = (pagina - 1) * limite + 1
|
||
const fin = Math.min(pagina * limite, total)
|
||
|
||
const paginasVisibles = useMemo(() => {
|
||
const paginas: number[] = []
|
||
const start = Math.max(1, pagina - 2)
|
||
const end = Math.min(totalPaginas, pagina + 2)
|
||
for (let i = start; i <= end; i++) paginas.push(i)
|
||
return paginas
|
||
}, [pagina, totalPaginas])
|
||
|
||
const formatCurrency = (v: number | null) => {
|
||
if (v == null) return '—'
|
||
return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN', maximumFractionDigits: 0 }).format(v)
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
{/* Toolbar */}
|
||
<div className="mb-4 flex items-center justify-between">
|
||
<p className="text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||
{total > 0 ? (
|
||
<>
|
||
Mostrando <span className="font-medium text-[#111827] dark:text-[#E2E8F0]">{inicio}–{fin}</span> de{' '}
|
||
<span className="font-medium text-[#111827] dark:text-[#E2E8F0]">{total.toLocaleString()}</span> clientes
|
||
</>
|
||
) : (
|
||
'Sin resultados'
|
||
)}
|
||
</p>
|
||
<button
|
||
type="button"
|
||
onClick={onExportar}
|
||
disabled={total === 0}
|
||
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-sm font-medium text-[#374151] shadow-sm transition-colors hover:bg-[#F9FAFB] disabled:opacity-50 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#CBD5E1] dark:hover:bg-[#334155]"
|
||
>
|
||
📥 Exportar Excel
|
||
</button>
|
||
</div>
|
||
|
||
{/* Loading skeleton */}
|
||
{cargando ? (
|
||
<div className="space-y-3">
|
||
{Array.from({ length: 10 }).map((_, i) => (
|
||
<div key={i} className="h-12 animate-pulse rounded-lg bg-[#F1F5F9] dark:bg-[#1E293B]" />
|
||
))}
|
||
</div>
|
||
) : datos.length === 0 ? (
|
||
<p className="py-8 text-center text-sm text-[#64748B] dark:text-[#94A3B8]">No hay clientes para mostrar.</p>
|
||
) : (
|
||
<>
|
||
{/* Mobile: cards */}
|
||
<div className="grid gap-3 md:hidden">
|
||
{datos.map((c) => (
|
||
<div key={c.id} className="rounded-xl border border-[#E5E7EB] bg-white p-4 shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]">
|
||
<div className="flex items-start justify-between">
|
||
<p className="text-sm font-semibold text-[#111827] dark:text-[#E2E8F0]">
|
||
{c.nombre} {c.apellidoPaterno} {c.apellidoMaterno ?? ''}
|
||
</p>
|
||
{c.estatus && (
|
||
<span className="shrink-0 rounded-full bg-blue-50 px-2 py-0.5 text-xs font-medium text-accent">
|
||
{c.estatus}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">#{c.numCliente}</p>
|
||
<div className="mt-2 flex flex-col gap-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||
{c.telefono && <span>📞 {c.telefono}</span>}
|
||
{c.email && <span>✉️ {c.email}</span>}
|
||
{c.desarrollo && <span>🏠 {c.desarrollo}</span>}
|
||
{c.valorTotal != null && <span>💰 {formatCurrency(c.valorTotal)}</span>}
|
||
{c.vendedor && <span>👤 {c.vendedor}</span>}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Desktop: table */}
|
||
<div className="overflow-x-auto rounded-xl border border-[#E5E7EB] bg-white shadow dark:border-[#1E293B] dark:bg-[#1E293B]">
|
||
<table className="min-w-full text-left text-sm">
|
||
<thead>
|
||
<tr className="border-b border-[#E5E7EB] bg-[#F8FAFC] dark:border-[#1E293B] dark:bg-[#0F172A]">
|
||
<th className="px-4 py-3 font-medium text-[#64748B] dark:text-[#94A3B8]">#</th>
|
||
<th className="px-4 py-3 font-medium text-[#64748B] dark:text-[#94A3B8]">Nombre completo</th>
|
||
<th className="px-4 py-3 font-medium text-[#64748B] dark:text-[#94A3B8]">Teléfono</th>
|
||
<th className="px-4 py-3 font-medium text-[#64748B] dark:text-[#94A3B8]">Email</th>
|
||
<th className="px-4 py-3 font-medium text-[#64748B] dark:text-[#94A3B8]">Desarrollo</th>
|
||
<th className="px-4 py-3 font-medium text-[#64748B] dark:text-[#94A3B8]">Estatus</th>
|
||
<th className="px-4 py-3 font-medium text-[#64748B] dark:text-[#94A3B8] text-right">Valor</th>
|
||
<th className="px-4 py-3 font-medium text-[#64748B] dark:text-[#94A3B8]">Vendedor</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-[#E5E7EB] dark:divide-[#1E293B]">
|
||
{datos.map((c) => (
|
||
<tr key={c.id} className={`transition-colors hover:bg-[#F8FAFC] dark:hover:bg-[#0F172A] ${onRowClick ? "cursor-pointer" : ""}`} onClick={() => onRowClick?.(c)}>
|
||
<td className="px-4 py-3 text-[#64748B] dark:text-[#94A3B8]">{c.numCliente}</td>
|
||
<td className="px-4 py-3 font-medium text-[#111827] dark:text-[#E2E8F0]">
|
||
{c.nombre} {c.apellidoPaterno} {c.apellidoMaterno ?? ''}
|
||
</td>
|
||
<td className="px-4 py-3 text-[#64748B] dark:text-[#94A3B8]">{c.telefono ?? '—'}</td>
|
||
<td className="px-4 py-3 text-[#64748B] dark:text-[#94A3B8]">{c.email ?? '—'}</td>
|
||
<td className="px-4 py-3 text-[#64748B] dark:text-[#94A3B8]">{c.desarrollo ?? '—'}</td>
|
||
<td className="px-4 py-3">
|
||
{c.estatus ? (
|
||
<span className="rounded-full bg-blue-50 px-2.5 py-0.5 text-xs font-medium text-accent">
|
||
{c.estatus}
|
||
</span>
|
||
) : (
|
||
<span className="text-[#64748B] dark:text-[#94A3B8]">—</span>
|
||
)}
|
||
</td>
|
||
<td className="px-4 py-3 text-right font-medium text-[#111827] dark:text-[#E2E8F0]">
|
||
{formatCurrency(c.valorTotal)}
|
||
</td>
|
||
<td className="px-4 py-3 text-[#64748B] dark:text-[#94A3B8]">{c.vendedor ?? '—'}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Pagination */}
|
||
{totalPaginas > 1 && (
|
||
<div className="mt-4 flex items-center justify-center gap-1">
|
||
<button
|
||
type="button"
|
||
onClick={() => onPageChange(pagina - 1)}
|
||
disabled={pagina === 1}
|
||
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-sm font-medium text-[#374151] shadow-sm transition-colors hover:bg-[#F9FAFB] disabled:opacity-50 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#CBD5E1] dark:hover:bg-[#334155]"
|
||
>
|
||
Anterior
|
||
</button>
|
||
{paginasVisibles[0]! > 1 && <span className="px-2 text-sm text-[#64748B] dark:text-[#94A3B8]">…</span>}
|
||
{paginasVisibles.map((p) => (
|
||
<button
|
||
key={p}
|
||
type="button"
|
||
onClick={() => onPageChange(p)}
|
||
className={`min-w-[36px] rounded-lg px-3 py-1.5 text-sm font-medium shadow-sm transition-colors ${
|
||
p === pagina
|
||
? 'bg-accent text-white'
|
||
: 'border border-[#D1D5DB] bg-white text-[#374151] hover:bg-[#F9FAFB] dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#CBD5E1] dark:hover:bg-[#334155]'
|
||
}`}
|
||
>
|
||
{p}
|
||
</button>
|
||
))}
|
||
{paginasVisibles[paginasVisibles.length - 1]! < totalPaginas && (
|
||
<span className="px-2 text-sm text-[#64748B] dark:text-[#94A3B8]">…</span>
|
||
)}
|
||
<button
|
||
type="button"
|
||
onClick={() => onPageChange(pagina + 1)}
|
||
disabled={pagina === totalPaginas}
|
||
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-sm font-medium text-[#374151] shadow-sm transition-colors hover:bg-[#F9FAFB] disabled:opacity-50 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#CBD5E1] dark:hover:bg-[#334155]"
|
||
>
|
||
Siguiente
|
||
</button>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|