feat(mejoras): 14 mejoras al CRM Frasa
Fase 1 - UI: dark mode, skeletons, toasts Fase 2 - Filtros: avanzados Mi día, búsqueda global Ctrl+K Fase 3 - Datos: export Excel, notas apartados, auditoría Fase 4 - Dashboard: gráficas vendedor + métricas uso Fase 5 - Responsive: tablets optimizadas Fase 6 - PWA: IndexedDB offline, push notifications Backend: 3 tablas nuevas, 6 services, 6 endpoints nuevos 721 API tests + fronts verdes.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { getContactos, type Contacto } from '../services/api'
|
||||
import { ModalContacto } from './ModalContacto'
|
||||
import { SkeletonList } from "./Skeleton"
|
||||
import toast from "react-hot-toast"
|
||||
|
||||
interface Props {
|
||||
onVolver: () => void
|
||||
@@ -17,34 +19,48 @@ export function VistaContactos({ onVolver }: Props) {
|
||||
const [cargando, setCargando] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [seleccionado, setSeleccionado] = useState<Contacto | null>(null)
|
||||
const [busqueda, setBusqueda] = useState('')
|
||||
|
||||
/** Contactos filtrados por texto de búsqueda */
|
||||
const contactosFiltrados = useMemo(() => {
|
||||
if (!contactos) return null
|
||||
if (busqueda.trim() === '') return contactos
|
||||
const q = busqueda.toLowerCase()
|
||||
return contactos.filter(
|
||||
(c) =>
|
||||
c.nombre.toLowerCase().includes(q) ||
|
||||
(c.email && c.email.toLowerCase().includes(q)) ||
|
||||
(c.telefono && c.telefono.includes(q)),
|
||||
)
|
||||
}, [contactos, busqueda])
|
||||
|
||||
const cargar = useCallback(() => {
|
||||
setCargando(true)
|
||||
setError('')
|
||||
getContactos()
|
||||
.then(setContactos)
|
||||
.catch(() => setError('No se pudieron cargar los contactos.'))
|
||||
.catch(() => { toast.error("No se pudieron cargar los contactos."); setError("No se pudieron cargar los contactos."); })
|
||||
.finally(() => setCargando(false))
|
||||
}, [])
|
||||
|
||||
useEffect(() => cargar(), [cargar])
|
||||
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-[1280px] flex-1 px-6 py-12 sm:py-16">
|
||||
<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
|
||||
type="button"
|
||||
onClick={onVolver}
|
||||
className="rounded-lg border border-[#D1D5DB] bg-white px-4 py-2 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||
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]"
|
||||
>
|
||||
← Volver
|
||||
</button>
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-[#111827] sm:text-3xl">Contactos</h1>
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-[#111827] dark:text-[#E2E8F0] sm:text-3xl">Contactos</h1>
|
||||
</div>
|
||||
|
||||
<section aria-label="Lista de contactos" className="mt-8">
|
||||
{cargando ? (
|
||||
<p className="text-sm text-[#64748B]">Cargando contactos…</p>
|
||||
<SkeletonList count={5} />
|
||||
) : error ? (
|
||||
<div>
|
||||
<p role="alert" className="text-sm text-red-600">
|
||||
@@ -53,35 +69,76 @@ export function VistaContactos({ onVolver }: Props) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={cargar}
|
||||
className="mt-3 rounded-lg border border-[#D1D5DB] bg-white px-4 py-2 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||
className="mt-3 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]"
|
||||
>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
) : !contactos || contactos.length === 0 ? (
|
||||
<p className="text-sm text-[#64748B]">No hay contactos para mostrar.</p>
|
||||
<p className="text-sm text-[#64748B] dark:text-[#94A3B8]">No hay contactos para mostrar.</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-[#E5E7EB] rounded-xl border border-[#E5E7EB] bg-white shadow-sm">
|
||||
{contactos.map((contacto) => (
|
||||
<li key={contacto.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSeleccionado(contacto)}
|
||||
className="flex w-full flex-wrap items-center gap-x-4 gap-y-1 px-6 py-4 text-left transition-colors duration-100 hover:bg-[#F8FAFC]"
|
||||
>
|
||||
<span className="text-sm font-semibold text-[#111827]">{contacto.nombre}</span>
|
||||
{contacto.estado && (
|
||||
<span className="rounded-full bg-blue-50 px-2.5 py-0.5 text-xs font-medium text-accent">
|
||||
{contacto.estado}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm text-[#64748B]">{contacto.telefono || 'Sin teléfono'}</span>
|
||||
<span className="text-sm text-[#64748B]">{contacto.email || 'Sin email'}</span>
|
||||
<span className="ml-auto text-xs font-medium text-accent">Ver detalle →</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por nombre, email o teléfono…"
|
||||
value={busqueda}
|
||||
onChange={(e) => setBusqueda(e.target.value)}
|
||||
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-4 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||
/>
|
||||
</div>
|
||||
{contactosFiltrados && contactosFiltrados.length === 0 ? (
|
||||
<p className="text-sm text-[#64748B] dark:text-[#94A3B8]">Sin resultados para “{busqueda}”</p>
|
||||
) : (
|
||||
<>
|
||||
{/* Mobile: cards */}
|
||||
<div className="grid gap-3 md:hidden">
|
||||
{contactosFiltrados?.map((contacto) => (
|
||||
<button
|
||||
key={contacto.id}
|
||||
type="button"
|
||||
onClick={() => setSeleccionado(contacto)}
|
||||
className="flex flex-col gap-2 rounded-xl border border-[#E5E7EB] bg-white p-4 text-left shadow-sm transition-colors duration-100 hover:border-accent dark:border-[#1E293B] dark:bg-[#1E293B]"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-[#111827] dark:text-[#E2E8F0]">{contacto.nombre}</span>
|
||||
{contacto.estado && (
|
||||
<span className="rounded-full bg-blue-50 px-2.5 py-0.5 text-xs font-medium text-accent">
|
||||
{contacto.estado}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-[#64748B] dark:text-[#94A3B8]">{contacto.telefono || 'Sin teléfono'}</span>
|
||||
<span className="text-xs text-[#64748B] dark:text-[#94A3B8]">{contacto.email || 'Sin email'}</span>
|
||||
<span className="text-xs font-medium text-accent">Ver detalle →</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* Desktop: table-style list */}
|
||||
<ul className="hidden divide-y divide-[#E5E7EB] rounded-xl border border-[#E5E7EB] bg-white shadow dark:border-[#1E293B] dark:bg-[#1E293B] md:block -sm">
|
||||
{contactosFiltrados?.map((contacto) => (
|
||||
<li key={contacto.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSeleccionado(contacto)}
|
||||
className="flex w-full flex-wrap items-center gap-x-4 gap-y-1 px-6 py-4 text-left transition-colors duration-100 hover:bg-[#F8FAFC] dark:bg-[#0F172A]"
|
||||
>
|
||||
<span className="text-sm font-semibold text-[#111827] dark:text-[#E2E8F0]">{contacto.nombre}</span>
|
||||
{contacto.estado && (
|
||||
<span className="rounded-full bg-blue-50 px-2.5 py-0.5 text-xs font-medium text-accent">
|
||||
{contacto.estado}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm text-[#64748B] dark:text-[#94A3B8]">{contacto.telefono || 'Sin teléfono'}</span>
|
||||
<span className="text-sm text-[#64748B] dark:text-[#94A3B8]">{contacto.email || 'Sin email'}</span>
|
||||
<span className="ml-auto text-xs font-medium text-accent">Ver detalle →</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user