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:
@@ -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 ?? ''}
|
||||
|
||||
104
apps/landing/src/components/ModalClienteMaestro.tsx
Normal file
104
apps/landing/src/components/ModalClienteMaestro.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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)} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user