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:
@@ -6,6 +6,9 @@ import QRCode from 'qrcode'
|
||||
import { useUnidades } from '../hooks/useUnidades'
|
||||
import { useCotizacion } from '../hooks/useCotizacion'
|
||||
import BannerModoLocal from '../components/BannerModoLocal'
|
||||
import DarkModeToggle from '../components/DarkModeToggle'
|
||||
import { SkeletonList } from "../components/Skeleton"
|
||||
import toast from "react-hot-toast"
|
||||
import { UnitSelector } from '../components/UnitSelector'
|
||||
import { UnitInfoCard } from '../components/UnitInfoCard'
|
||||
import { ProposalCard } from '../components/ProposalCard'
|
||||
@@ -45,7 +48,7 @@ const campoInputBase =
|
||||
const campoInput = `${campoInputBase} border-[#D1D5DB]`
|
||||
const campoInputError = `${campoInputBase} border-red-400`
|
||||
|
||||
const etiquetaCampo = 'mb-2 block text-xs font-medium text-[#4B5563]'
|
||||
const etiquetaCampo = 'mb-2 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]'
|
||||
|
||||
export default function CotizadorPage() {
|
||||
const { unidades, cargando, error, usandoSeed } = useUnidades()
|
||||
@@ -178,10 +181,11 @@ export default function CotizadorPage() {
|
||||
})
|
||||
const conFolio = { ...guardada, folio: guardada.folio ?? folioCotizacion(guardada.id) }
|
||||
setUltimaGuardada(conFolio)
|
||||
toast.success('Cotización guardada correctamente')
|
||||
setMisCotizaciones((prev) => [conFolio, ...prev].slice(0, 50))
|
||||
return conFolio
|
||||
} catch (err) {
|
||||
console.warn('[api] No se pudo guardar la cotización; el PDF sale sin folio/QR.', err)
|
||||
toast.error('No se pudo guardar la cotización'); console.warn('[api] No se pudo guardar la cotización; el PDF sale sin folio/QR.', err)
|
||||
setAviso('Sin conexión al servidor: la cotización no se guardó (PDF sin folio/QR).')
|
||||
return null
|
||||
}
|
||||
@@ -300,16 +304,19 @@ export default function CotizadorPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC]">
|
||||
<div className="min-h-screen bg-[#F8FAFC] dark:bg-[#0F172A]">
|
||||
<BannerModoLocal />
|
||||
<main className="mx-auto max-w-[1280px] px-4 py-8 sm:px-6 lg:py-10">
|
||||
{/* Encabezado de página */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-xl font-semibold text-[#111827]">Cotizador</h1>
|
||||
<p className="mt-1 text-sm text-[#64748B] capitalize">{formatoFechaLarga(new Date())}</p>
|
||||
<p className="text-sm text-[#64748B]">
|
||||
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-[#111827] dark:text-[#E2E8F0]">Cotizador</h1>
|
||||
<p className="mt-1 text-sm text-[#64748B] dark:text-[#94A3B8] capitalize">{formatoFechaLarga(new Date())}</p>
|
||||
<p className="text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Vigencia: hasta el {formatoFechaLarga(calcularVigencia())}
|
||||
</p>
|
||||
</div>
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
|
||||
{oportunidadId !== null && nombreCliente.trim() !== '' && (
|
||||
@@ -321,16 +328,16 @@ export default function CotizadorPage() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="grid items-start gap-6 lg:grid-cols-[380px_minmax(0,1fr)] lg:gap-8">
|
||||
<div className="grid items-start gap-6 md:grid-cols-1 lg:grid-cols-[380px_minmax(0,1fr)] lg:gap-8">
|
||||
{/* Panel de configuración */}
|
||||
<aside className="space-y-6 lg:sticky lg:top-24">
|
||||
<section className="space-y-6 rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||
<section className="space-y-6 rounded-xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
|
||||
{error && <p className="text-xs text-red-700">{error}</p>}
|
||||
{usandoSeed && !cargando && (
|
||||
<p className="text-xs text-amber-700">Datos de demostración (sin conexión al servidor)</p>
|
||||
)}
|
||||
{cargando ? (
|
||||
<p className="text-sm text-[#64748B]">Cargando unidades…</p>
|
||||
<SkeletonList count={3} />
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
@@ -351,18 +358,18 @@ export default function CotizadorPage() {
|
||||
className={`px-4 py-2.5 text-xs font-medium transition-colors duration-100 ${
|
||||
activo
|
||||
? 'bg-accent text-white'
|
||||
: 'bg-white text-[#4B5563] hover:bg-[#F9FAFB]'
|
||||
: 'bg-white text-[#4B5563] dark:text-[#CBD5E1] hover:bg-[#F9FAFB]'
|
||||
}`}
|
||||
>
|
||||
{obtenerDesarrollo(id).nombre}
|
||||
<span className={`ml-1 font-normal ${activo ? 'text-blue-100' : 'text-[#64748B]'}`}>
|
||||
<span className={`ml-1 font-normal ${activo ? 'text-blue-100' : 'text-[#64748B] dark:text-[#94A3B8]'}`}>
|
||||
{disponibles}/{total}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-[#64748B]">
|
||||
<p className="mt-2 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||
{unidadesDesarrollo.length} unidades · {disponiblesDesarrollo} disponibles
|
||||
</p>
|
||||
</div>
|
||||
@@ -391,7 +398,7 @@ export default function CotizadorPage() {
|
||||
className={`px-5 py-2.5 text-xs font-medium transition-colors duration-100 ${
|
||||
activo
|
||||
? 'bg-accent text-white'
|
||||
: 'bg-white text-[#4B5563] hover:bg-[#F9FAFB]'
|
||||
: 'bg-white text-[#4B5563] dark:text-[#CBD5E1] hover:bg-[#F9FAFB]'
|
||||
}`}
|
||||
>
|
||||
{opcion}
|
||||
@@ -421,7 +428,7 @@ export default function CotizadorPage() {
|
||||
{errorApartado ? (
|
||||
<p className="mt-1 text-xs text-red-700">{errorApartado}</p>
|
||||
) : (
|
||||
<p className="mt-1 text-xs text-[#64748B]">{formatoMoneda(apartado)}</p>
|
||||
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">{formatoMoneda(apartado)}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -451,7 +458,7 @@ export default function CotizadorPage() {
|
||||
{errorEnganche ? (
|
||||
<p className="mt-1 text-xs text-red-700">{errorEnganche}</p>
|
||||
) : (
|
||||
<p className="mt-1 text-xs text-[#64748B]">
|
||||
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||
Aplica a crédito bancario e Infonavit/Cofinavit; mínimo 2.5%
|
||||
{unidad ? ` (${formatoMoneda((precioLista * Math.max(enganchePct ?? 2.5, 2.5)) / 100)})` : ''}
|
||||
</p>
|
||||
@@ -515,11 +522,11 @@ export default function CotizadorPage() {
|
||||
</div>
|
||||
|
||||
{unidad && (
|
||||
<div className="border-t border-[#E5E7EB] pt-6">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
<div className="border-t border-[#E5E7EB] dark:border-[#1E293B] pt-6">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
|
||||
Precio de lista
|
||||
</p>
|
||||
<p className="mt-2 text-3xl font-semibold text-[#111827]">
|
||||
<p className="mt-2 text-3xl font-semibold text-[#111827] dark:text-[#E2E8F0]">
|
||||
{formatoMoneda(precioLista)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -529,7 +536,7 @@ export default function CotizadorPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNueva}
|
||||
className="w-full 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="w-full 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]"
|
||||
>
|
||||
Nueva cotización
|
||||
</button>
|
||||
@@ -543,13 +550,13 @@ export default function CotizadorPage() {
|
||||
<section className="min-w-0">
|
||||
{!unidad ? (
|
||||
<div className="flex h-64 items-center justify-center rounded-xl border border-dashed border-[#D1D5DB] bg-white">
|
||||
<p className="text-sm text-[#64748B]">
|
||||
<p className="text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Selecciona una unidad para ver las propuestas comerciales.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{propuestas.map((p, i) => (
|
||||
<ProposalCard
|
||||
key={p.tipo}
|
||||
@@ -565,8 +572,8 @@ export default function CotizadorPage() {
|
||||
propuestas={propuestas}
|
||||
titulos={propuestas.map((p) => p.titulo)}
|
||||
/>
|
||||
<div className="rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-sm">
|
||||
<div className="flex flex-wrap items-center justify-end gap-4">
|
||||
<div className="rounded-xl border border-[#E5E7EB] bg-white p-5 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-end sm:justify-end">
|
||||
<div>
|
||||
<label htmlFor="propuesta-elegida" className={etiquetaCampo}>
|
||||
Propuesta elegida
|
||||
@@ -575,7 +582,7 @@ export default function CotizadorPage() {
|
||||
id="propuesta-elegida"
|
||||
value={Math.min(propuestaIndex, propuestas.length - 1)}
|
||||
onChange={(e) => setPropuestaIndex(Number(e.target.value))}
|
||||
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:ring-2 focus:ring-accent/20 focus:outline-none"
|
||||
className="rounded-lg border border-[#D1D5DB] bg-white px-3 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2.5 text-sm transition-colors focus:border-accent focus:ring-2 focus:ring-accent/20 focus:outline-none"
|
||||
>
|
||||
{propuestas.map((p, i) => (
|
||||
<option key={p.tipo} value={i}>
|
||||
@@ -601,9 +608,9 @@ export default function CotizadorPage() {
|
||||
{guardando ? 'Guardando…' : 'Guardar y generar PDF'}
|
||||
</button>
|
||||
</div>
|
||||
{aviso && <p className="mt-2 text-right text-xs text-[#64748B]">{aviso}</p>}
|
||||
{aviso && <p className="mt-2 text-right text-xs text-[#64748B] dark:text-[#94A3B8]">{aviso}</p>}
|
||||
{!puedeGenerarPdf && !guardando && (
|
||||
<p className="mt-2 text-right text-xs text-[#64748B]">
|
||||
<p className="mt-2 text-right text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||
Selecciona una unidad para guardar y generar el PDF.
|
||||
</p>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user