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:
@@ -17,17 +17,17 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC] text-[#111827]">
|
||||
<div className="border-b border-[#E5E7EB] bg-white">
|
||||
<div className="min-h-screen bg-[#F8FAFC] dark:bg-[#0F172A] text-[#111827] dark:text-[#E2E8F0] dark:bg-[#0F172A] dark:text-[#E2E8F0]">
|
||||
<div className="border-b border-[#E5E7EB] bg-white dark:border-[#1E293B] dark:bg-[#1E293B]">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
||||
<span className="text-xs font-medium text-[#64748B]">
|
||||
<span className="text-xs font-medium text-[#64748B] dark:text-[#94A3B8]">
|
||||
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
||||
</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setModalPassword(true)}
|
||||
className="rounded-md px-2 py-1.5 text-xs font-medium text-[#64748B] transition-all duration-100 ease-out hover:text-[#111827]"
|
||||
className="rounded-md px-2 py-1.5 text-xs font-medium text-[#64748B] dark:text-[#94A3B8] transition-all duration-100 ease-out hover:text-[#111827] dark:text-[#E2E8F0]"
|
||||
>
|
||||
Contraseña
|
||||
</button>
|
||||
|
||||
@@ -9,7 +9,7 @@ function Burbuja({ mensaje }: { mensaje: Mensaje }) {
|
||||
className={`inline-block whitespace-pre-wrap rounded-2xl px-4 py-3 text-sm leading-relaxed ${
|
||||
esUsuario
|
||||
? 'rounded-br-md bg-accent text-white shadow-sm'
|
||||
: 'rounded-bl-md border border-[#E5E7EB] bg-white text-[#111827] shadow-sm'
|
||||
: 'rounded-bl-md border border-[#E5E7EB] bg-white text-[#111827] dark:text-[#E2E8F0] shadow-sm'
|
||||
}`}
|
||||
>
|
||||
{mensaje.contenido}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { validarConfirmacion, validarPasswordNueva } from '../lib/password'
|
||||
import { cambiarPassword } from '../services/auth'
|
||||
|
||||
const CLASE_INPUT =
|
||||
'w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20'
|
||||
const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563]'
|
||||
'w-full 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:outline-none focus:ring-2 focus:ring-accent/20'
|
||||
const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]'
|
||||
|
||||
/**
|
||||
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
|
||||
@@ -46,7 +46,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
||||
aria-label="Cambiar contraseña"
|
||||
>
|
||||
<div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl">
|
||||
<h2 className="mb-6 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
<h2 className="mb-6 text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
|
||||
Cambiar contraseña
|
||||
</h2>
|
||||
{exito ? (
|
||||
@@ -127,7 +127,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCerrar}
|
||||
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-3 text-sm font-medium text-[#374151] 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-3 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
|
||||
32
apps/agente-ia/src/components/DarkModeToggle.tsx
Normal file
32
apps/agente-ia/src/components/DarkModeToggle.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Sun, Moon } from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
function getStoredTheme(): boolean {
|
||||
if (typeof window === "undefined") return false
|
||||
try {
|
||||
const stored = localStorage.getItem("frasa_theme")
|
||||
if (stored) return stored === "dark"
|
||||
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export default function DarkModeToggle() {
|
||||
const [dark, setDark] = useState(getStoredTheme)
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.toggle("dark", dark)
|
||||
try {
|
||||
localStorage.setItem("frasa_theme", dark ? "dark" : "light")
|
||||
} catch {
|
||||
// localStorage not available (test env)
|
||||
}
|
||||
}, [dark])
|
||||
|
||||
return (
|
||||
<button onClick={() => setDark(!dark)} className="rounded-lg p-2 text-[#64748B] hover:bg-[#F1F5F9] dark:text-[#94A3B8] dark:hover:bg-[#1E293B]" aria-label="Cambiar tema">
|
||||
{dark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -23,12 +23,12 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-[#F8FAFC] px-4">
|
||||
<div className="w-full max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm">
|
||||
<div className="w-full max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-8 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
|
||||
<div className="mb-10 text-center">
|
||||
<p className="text-lg font-bold tracking-wide">
|
||||
FRASA<span className="text-[#F97316]">.</span>
|
||||
</p>
|
||||
<p className="mt-2 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
<p className="mt-2 text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
|
||||
Acceso al ecosistema
|
||||
</p>
|
||||
</div>
|
||||
@@ -36,7 +36,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
||||
<div>
|
||||
<label
|
||||
htmlFor="login-usuario"
|
||||
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||
className="mb-2 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]"
|
||||
>
|
||||
Usuario
|
||||
</label>
|
||||
@@ -48,13 +48,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
required
|
||||
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||
className="w-full 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:outline-none focus:ring-2 focus:ring-accent/20"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="login-password"
|
||||
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||
className="mb-2 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]"
|
||||
>
|
||||
Contraseña
|
||||
</label>
|
||||
@@ -65,7 +65,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||
className="w-full 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:outline-none focus:ring-2 focus:ring-accent/20"
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
|
||||
21
apps/agente-ia/src/components/Skeleton.tsx
Normal file
21
apps/agente-ia/src/components/Skeleton.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
export default function Skeleton({ className = "" }: { className?: string }) {
|
||||
return <div className={`animate-pulse rounded-lg bg-[#E2E8F0] dark:bg-[#334155] ${className}`} />
|
||||
}
|
||||
|
||||
export function SkeletonCard() {
|
||||
return (
|
||||
<div className="rounded-xl border border-[#E5E7EB] bg-white p-4 shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]">
|
||||
<Skeleton className="h-4 w-1/3 mb-3" />
|
||||
<Skeleton className="h-3 w-2/3 mb-2" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SkeletonList({ count = 5 }: { count?: number }) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: count }).map((_, i) => <SkeletonCard key={i} />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user