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,5 +1,7 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import AppHeader from '../components/AppHeader'
|
||||
import { SkeletonCard } from "../components/Skeleton"
|
||||
import toast from "react-hot-toast"
|
||||
import BannerModoLocal from '../components/BannerModoLocal'
|
||||
import Stepper from '../components/Stepper'
|
||||
import DatosGeneralesForm from '../forms/DatosGeneralesForm'
|
||||
@@ -67,7 +69,7 @@ const formatoFechaHora = (iso: string): string =>
|
||||
|
||||
/** Tarjeta del paso actual del wizard (el stepper queda fuera, a la izquierda en lg) */
|
||||
function TarjetaPaso({ children }: { children: ReactNode }) {
|
||||
return <div className="rounded-xl border border-[#E5E7EB] bg-white shadow-sm">{children}</div>
|
||||
return <div className="rounded-xl border border-[#E5E7EB] bg-white shadow dark:border-[#1E293B] dark:bg-[#1E293B] -sm">{children}</div>
|
||||
}
|
||||
|
||||
/** Cuadro con las iniciales de la opción (icono de la tarjeta de entrada) */
|
||||
@@ -226,6 +228,7 @@ export default function AltaClientePage() {
|
||||
const guardar = async () => {
|
||||
try {
|
||||
const b = await guardarBorrador({ ...(borradorId ? { id: borradorId } : {}), paso, datos })
|
||||
toast.success("Borrador guardado")
|
||||
setBorradorId(b.id)
|
||||
setAvisoBorrador(
|
||||
`Borrador guardado ${new Date().toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' })}`,
|
||||
@@ -283,6 +286,7 @@ export default function AltaClientePage() {
|
||||
const res = datos.progresoId
|
||||
? await completarProgreso(datos.progresoId, alta)
|
||||
: await crearAltaCliente(alta)
|
||||
toast.success("Alta de cliente completada")
|
||||
if (res.ok) {
|
||||
await subirDocumentos(res.id, alta)
|
||||
setRegistro(res.id)
|
||||
@@ -322,9 +326,9 @@ export default function AltaClientePage() {
|
||||
|
||||
if (pantalla === 'cargando') {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC]">
|
||||
<div className="min-h-screen bg-[#F8FAFC] dark:bg-[#0F172A]">
|
||||
<main className="mx-auto max-w-[1280px] px-4 py-16 sm:px-6">
|
||||
<p className="text-sm text-[#64748B]">Cargando…</p>
|
||||
<SkeletonCard />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
@@ -332,23 +336,23 @@ export default function AltaClientePage() {
|
||||
|
||||
if (pantalla === 'entrada') {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC]">
|
||||
<div className="min-h-screen bg-[#F8FAFC] dark:bg-[#0F172A]">
|
||||
<AppHeader titulo="Alta de Clientes" />
|
||||
<BannerModoLocal />
|
||||
<main className="mx-auto max-w-[1280px] px-4 py-8 sm:px-6 sm:py-12">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<p className="text-sm text-[#64748B]">
|
||||
<p className="text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Elige cómo registrar al interesado: captura ligera de afluencia o formato completo de alta.
|
||||
</p>
|
||||
<div className="mt-6 grid gap-6 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPantalla('afluencia')}
|
||||
className="rounded-xl border border-[#E5E7EB] bg-white p-6 text-left shadow-sm transition-all duration-200 ease-in-out hover:border-accent hover:shadow-md"
|
||||
className="rounded-xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] text-left shadow-sm transition-all duration-200 ease-in-out hover:border-accent hover:shadow-md"
|
||||
>
|
||||
<IconoIniciales iniciales="AA" />
|
||||
<span className="mt-4 block text-lg font-semibold text-[#111827]">Alta Afluencia</span>
|
||||
<span className="mt-2 block text-sm text-[#64748B]">
|
||||
<span className="mt-4 block text-lg font-semibold text-[#111827] dark:text-[#E2E8F0]">Alta Afluencia</span>
|
||||
<span className="mt-2 block text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Captura rápida del interesado; entra al embudo de ventas.
|
||||
</span>
|
||||
<span className="mt-5 inline-flex items-center gap-1.5 text-sm font-medium text-accent">
|
||||
@@ -359,11 +363,11 @@ export default function AltaClientePage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPantalla('picker')}
|
||||
className="rounded-xl border border-[#E5E7EB] bg-white p-6 text-left shadow-sm transition-all duration-200 ease-in-out hover:border-accent hover:shadow-md"
|
||||
className="rounded-xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] text-left shadow-sm transition-all duration-200 ease-in-out hover:border-accent hover:shadow-md"
|
||||
>
|
||||
<IconoIniciales iniciales="AC" />
|
||||
<span className="mt-4 block text-lg font-semibold text-[#111827]">Alta de Cliente</span>
|
||||
<span className="mt-2 block text-sm text-[#64748B]">
|
||||
<span className="mt-4 block text-lg font-semibold text-[#111827] dark:text-[#E2E8F0]">Alta de Cliente</span>
|
||||
<span className="mt-2 block text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Formato completo para quien aparta.
|
||||
</span>
|
||||
<span className="mt-5 inline-flex items-center gap-1.5 text-sm font-medium text-accent">
|
||||
@@ -380,14 +384,14 @@ export default function AltaClientePage() {
|
||||
|
||||
if (pantalla === 'afluencia') {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC]">
|
||||
<div className="min-h-screen bg-[#F8FAFC] dark:bg-[#0F172A]">
|
||||
<AppHeader titulo="Alta Afluencia" />
|
||||
<main className="mx-auto max-w-[1280px] px-4 py-8 sm:px-6 sm:py-10">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<p className="text-sm text-[#64748B]">
|
||||
<p className="text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Captura rápida del interesado; entra al embudo de ventas.
|
||||
</p>
|
||||
<div className="mt-6 rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||
<div className="mt-6 rounded-xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
|
||||
<AltaAfluenciaForm
|
||||
valoresIniciales={afluenciaPrevia?.valores}
|
||||
oportunidadId={afluenciaPrevia?.oportunidadId}
|
||||
@@ -406,22 +410,22 @@ export default function AltaClientePage() {
|
||||
|
||||
if (pantalla === 'picker') {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC]">
|
||||
<div className="min-h-screen bg-[#F8FAFC] dark:bg-[#0F172A]">
|
||||
<AppHeader titulo="Alta de Clientes" />
|
||||
<main className="mx-auto max-w-[1280px] px-4 py-8 sm:px-6 sm:py-10">
|
||||
<div className="max-w-3xl">
|
||||
{borradores.length > 0 && (
|
||||
<>
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
|
||||
Tienes {borradores.length} borrador{borradores.length === 1 ? '' : 'es'} guardado
|
||||
{borradores.length === 1 ? '' : 's'}
|
||||
</h2>
|
||||
<ul className="mt-6 divide-y divide-[#E5E7EB] rounded-xl border border-[#E5E7EB] bg-white shadow-sm">
|
||||
<ul className="mt-6 divide-y divide-[#E5E7EB] rounded-xl border border-[#E5E7EB] bg-white shadow dark:border-[#1E293B] dark:bg-[#1E293B] -sm">
|
||||
{borradores.map((b) => (
|
||||
<li key={b.id} className="flex flex-wrap items-center justify-between gap-4 px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm text-[#111827]">{b.nombre}</p>
|
||||
<p className="mt-1 text-xs text-[#64748B]">
|
||||
<p className="truncate text-sm text-[#111827] dark:text-[#E2E8F0]">{b.nombre}</p>
|
||||
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||
Paso {b.paso + 1} de {PASOS.length} · {formatoFechaHora(b.updatedAt)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -455,11 +459,11 @@ export default function AltaClientePage() {
|
||||
Empezar nuevo
|
||||
</button>
|
||||
|
||||
<section aria-labelledby="titulo-lead" className="mt-12 border-t border-[#E5E7EB] pt-8">
|
||||
<h3 id="titulo-lead" className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
<section aria-labelledby="titulo-lead" className="mt-12 border-t border-[#E5E7EB] dark:border-[#1E293B] pt-8">
|
||||
<h3 id="titulo-lead" className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
|
||||
Desde un lead de Monday
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-[#64748B]">
|
||||
<p className="mt-2 text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Empieza un alta nueva con el nombre, email y celular del lead ya capturados.
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
@@ -473,13 +477,13 @@ export default function AltaClientePage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC]">
|
||||
<div className="min-h-screen bg-[#F8FAFC] dark:bg-[#0F172A]">
|
||||
<AppHeader
|
||||
titulo="Alta de Clientes"
|
||||
acciones={
|
||||
<>
|
||||
{avisoBorrador && (
|
||||
<span role="status" className="text-xs text-[#64748B]">
|
||||
<span role="status" className="text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||
{avisoBorrador}
|
||||
</span>
|
||||
)}
|
||||
@@ -491,7 +495,7 @@ export default function AltaClientePage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void guardar()}
|
||||
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 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-5 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2.5 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||
>
|
||||
Guardar borrador
|
||||
</button>
|
||||
@@ -500,8 +504,8 @@ export default function AltaClientePage() {
|
||||
/>
|
||||
|
||||
<main className="mx-auto max-w-[1280px] px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div className="lg:grid lg:grid-cols-[260px_minmax(0,1fr)] lg:gap-8">
|
||||
<div className="mb-6 lg:sticky lg:top-20 lg:mb-0 lg:self-start">
|
||||
<div className="md:grid md:grid-cols-[260px_minmax(0,1fr)] md:gap-8 lg:gap-8">
|
||||
<div className="mb-6 md:sticky md:top-20 md:mb-0 md:self-start">
|
||||
<Stepper pasos={[...PASOS]} pasoActual={paso} onIrAPaso={irAPaso} />
|
||||
</div>
|
||||
|
||||
@@ -648,23 +652,23 @@ export default function AltaClientePage() {
|
||||
</p>
|
||||
)}
|
||||
{registro && (
|
||||
<div className="mb-6 rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
<div className="mb-6 rounded-xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
|
||||
Firma en remoto
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-[#64748B]">
|
||||
<p className="mt-2 text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Genera una liga pública (vigencia 72 h) para que el cliente firme y suba sus
|
||||
documentos desde su celular, sin usuario ni contraseña.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void generarLiga()}
|
||||
className="mt-4 rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||
className="mt-4 rounded-lg border border-[#D1D5DB] bg-white px-5 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2.5 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||
>
|
||||
Generar liga para firmar en remoto
|
||||
</button>
|
||||
{ligaUrl && (
|
||||
<p className="mt-4 break-all text-xs text-[#111827]">
|
||||
<p className="mt-4 break-all text-xs text-[#111827] dark:text-[#E2E8F0]">
|
||||
{ligaUrl}
|
||||
{ligaCopiada && (
|
||||
<span role="status" className="ml-2 font-medium text-accent">
|
||||
|
||||
Reference in New Issue
Block a user