feat(clientes-maestro): modal detalle + fix permisos roles

- 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
This commit is contained in:
2026-09-08 02:47:29 +00:00
parent 2fc817887d
commit dc43cbe1ce
3 changed files with 116 additions and 2 deletions

View File

@@ -9,10 +9,11 @@ interface Props {
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 }: Props) {
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)
@@ -108,7 +109,7 @@ export function ClientesMaestroTable({ datos, total, pagina, limite, cargando, o
</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]">
<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 ?? ''}

View File

@@ -0,0 +1,104 @@
import { X } from 'lucide-react'
import type { ClienteMaestro } from '../services/clientes-maestro'
interface Props {
cliente: ClienteMaestro
onCerrar: () => void
}
const SECCIONES = [
{ titulo: 'Datos Personales', campos: [
{ key: 'numCliente', label: '# Cliente' },
{ key: 'nombre', label: 'Nombre' },
{ key: 'apellidoPaterno', label: 'Apellido Paterno' },
{ key: 'apellidoMaterno', label: 'Apellido Materno' },
{ key: 'sexo', label: 'Sexo' },
{ key: 'estadoCivil', label: 'Estado Civil' },
{ key: 'rangoEdad', label: 'Rango Edad' },
]},
{ titulo: 'Contacto', campos: [
{ key: 'telefono', label: 'Teléfono' },
{ key: 'email', label: 'Email' },
]},
{ titulo: 'Crédito e Ingresos', campos: [
{ key: 'credito', label: 'Tipo Crédito' },
{ key: 'institucion', label: 'Institución' },
{ key: 'rangoIngresos', label: 'Rango Ingresos' },
{ key: 'ocupacion', label: 'Ocupación' },
]},
{ titulo: 'Propiedad', campos: [
{ key: 'desarrollo', label: 'Desarrollo' },
{ key: 'manzana', label: 'Manzana' },
{ key: 'lote', label: 'Lote' },
{ key: 'interior', label: 'Interior' },
{ key: 'tipoCasa', label: 'Tipo Casa' },
{ key: 'etapa', label: 'Etapa' },
{ key: 'valorTotal', label: 'Valor Total', format: (v: unknown) => v != null ? `$${Number(v).toLocaleString('es-MX')}` : '-' },
]},
{ titulo: 'Marketing', campos: [
{ key: 'medioContacto', label: 'Medio Contacto' },
{ key: 'canal', label: 'Canal' },
]},
{ titulo: 'Venta', campos: [
{ key: 'estatus', label: 'Estatus' },
{ key: 'vendedor', label: 'Vendedor' },
{ key: 'vendedorId', label: 'ID Vendedor' },
]},
]
export default function ModalClienteMaestro({ cliente, onCerrar }: Props) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4" role="dialog" aria-modal="true">
<div className="w-full max-w-lg max-h-[90vh] overflow-y-auto rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-xl dark:border-[#1E293B] dark:bg-[#0F172A]">
<div className="mb-4 flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-[#111827] dark:text-[#E2E8F0]">
{cliente.nombre} {cliente.apellidoPaterno}
</h2>
<p className="text-sm text-[#64748B] dark:text-[#94A3B8]">
Cliente #{cliente.numCliente}
</p>
</div>
<button onClick={onCerrar} className="rounded-lg p-1 text-[#64748B] hover:bg-[#F1F5F9] dark:text-[#94A3B8] dark:hover:bg-[#1E293B]" aria-label="Cerrar">
<X className="h-5 w-5" />
</button>
</div>
<div className="space-y-4">
{SECCIONES.map((sec) => {
const camposConValor = sec.campos.filter((c) => {
const v = cliente[c.key as keyof ClienteMaestro]
return v != null && v !== ''
})
if (camposConValor.length === 0) return null
return (
<div key={sec.titulo} className="rounded-lg border border-[#E5E7EB] bg-[#F9FAFB] p-4 dark:border-[#334155] dark:bg-[#1E293B]">
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
{sec.titulo}
</h3>
<dl className="grid grid-cols-2 gap-x-4 gap-y-2">
{camposConValor.map((c) => {
const val = cliente[c.key as keyof ClienteMaestro]
const display = c.format ? c.format(val) : String(val)
return (
<div key={c.key}>
<dt className="text-xs text-[#94A3B8] dark:text-[#64748B]">{c.label}</dt>
<dd className="text-sm font-medium text-[#111827] dark:text-[#E2E8F0]">{display}</dd>
</div>
)
})}
</dl>
</div>
)
})}
</div>
<div className="mt-4 flex justify-end">
<button onClick={onCerrar} className="rounded-lg border border-[#D1D5DB] bg-white px-4 py-2 text-sm font-medium text-[#374151] hover:bg-[#F9FAFB] dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#CBD5E1] dark:hover:bg-[#1E293B]">
Cerrar
</button>
</div>
</div>
</div>
)
}

View File

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react'
import { listarClientesMaestro, exportarClientesMaestro, type ClienteMaestro } from '../services/clientes-maestro'
import { ClientesMaestroTable } from '../components/ClientesMaestroTable'
import ModalClienteMaestro from '../components/ModalClienteMaestro'
import { getSesion } from '../services/auth'
import toast from 'react-hot-toast'
@@ -19,6 +20,7 @@ const ESTATUSES = ['', 'VENDIDO', 'APARTADO', 'ESCRITURADO', 'PENDIENTE', 'CANCE
*/
export function ClientesMaestroPage({ onVolver }: Props) {
const [datos, setDatos] = useState<ClienteMaestro[]>([])
const [clienteSeleccionado, setClienteSeleccionado] = useState<ClienteMaestro | null>(null)
const [total, setTotal] = useState(0)
const [pagina, setPagina] = useState(1)
const [cargando, setCargando] = useState(true)
@@ -119,6 +121,7 @@ export function ClientesMaestroPage({ onVolver }: Props) {
}
return (
<>
<main className="mx-auto w-full max-w-[1280px] flex-1 px-4 py-12 sm:px-6 sm:py-16">
<div className="flex items-center gap-4">
<button
@@ -183,6 +186,7 @@ export function ClientesMaestroPage({ onVolver }: Props) {
{/* Tabla */}
<section className="mt-6">
<ClientesMaestroTable
onRowClick={setClienteSeleccionado}
datos={datos}
total={total}
pagina={pagina}
@@ -193,5 +197,10 @@ export function ClientesMaestroPage({ onVolver }: Props) {
/>
</section>
</main>
{clienteSeleccionado && (
<ModalClienteMaestro cliente={clienteSeleccionado} onCerrar={() => setClienteSeleccionado(null)} />
)}
</>
)
}