fix(dark-mode): dark mode completo + gráficas adaptadas

Dark mode fixes:
- Cards secundarias: Mis capturas, Mis clientes, Actividades, Apartados por vencer, Equipo
- Botones: Exportar, Descartar, Afluencia, Agendo, Forzar reconexión, Subir archivo
- Config: ModoTrabajo, Usuarios, Sistema, RolesPermisos, ConfigPage
- Cotizador: inputs, tabs, placeholder, botón compartir
- Alta clientes: BotonOcrIne, FileUploader
- Cobranza: toggle dashboard, modal abono
- Dashboards: tabs

Gráficas Recharts:
- CSS variables para colores chart (grid, text, tooltip)
- Helper isDark() + axisTextColor() + gridStroke() + tooltipProps()
- DashboardVendedor: grid, axes, tooltips adaptados
- TableroGeneral: grid, axes, tooltips (BarChart, Funnel, PieChart)
- MiTablero: tooltip adaptado
- Perfilamiento: grid, axes, tooltip

7 apps construidas limpias.
This commit is contained in:
2026-09-07 06:44:08 +00:00
parent c77b4c0628
commit 8b65f903d3
20 changed files with 184 additions and 105 deletions

View File

@@ -4,15 +4,13 @@ import {
Cell,
} from 'recharts'
import { fetchMiTablero, type MiTablero } from '../services/reporte-mi-tablero'
import { axisTextColor, gridStroke, tooltipProps } from '../lib/chart-theme'
import KpiCard from './KpiCard'
// ---------- Colores ----------
const COLORS = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#14B8A6']
const LIGHT_TEXT = '#64748B'
const GRID_LIGHT = '#E5E7EB'
// ---------- Sub-componentes ----------
function TituloSeccion({ children }: { children: string }) {
@@ -129,16 +127,16 @@ export default function DashboardVendedor() {
<TituloSeccion>Leads por día (últimos 30 días)</TituloSeccion>
<ResponsiveContainer width="100%" height={260}>
<BarChart data={tablero.leadsPorDia} margin={{ top: 8, right: 4, left: -16, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={GRID_LIGHT} vertical={false} />
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke()} vertical={false} />
<XAxis
dataKey="fecha"
tickFormatter={(f: string) => f.slice(8)}
tick={{ fontSize: 10, fill: LIGHT_TEXT }}
tick={{ fontSize: 10, fill: axisTextColor() }}
interval="preserveStartEnd"
/>
<YAxis tick={{ fontSize: 10, fill: LIGHT_TEXT }} allowDecimals={false} />
<YAxis tick={{ fontSize: 10, fill: axisTextColor() }} allowDecimals={false} />
<Tooltip
contentStyle={{ fontSize: 12, borderRadius: 8 }}
{...tooltipProps()}
labelFormatter={(f: unknown) => `Fecha: ${String(f)}`}
formatter={(value: unknown) => [`${Number(value)} leads`, "Leads"]}
/>
@@ -166,16 +164,16 @@ export default function DashboardVendedor() {
) : (
<ResponsiveContainer width="100%" height={260}>
<BarChart data={datosOrigen} layout="vertical" margin={{ top: 8, right: 16, left: 8, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={GRID_LIGHT} horizontal={false} />
<XAxis type="number" tick={{ fontSize: 10, fill: LIGHT_TEXT }} allowDecimals={false} />
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke()} horizontal={false} />
<XAxis type="number" tick={{ fontSize: 10, fill: axisTextColor() }} allowDecimals={false} />
<YAxis
type="category"
dataKey="origen"
tick={{ fontSize: 10, fill: LIGHT_TEXT }}
tick={{ fontSize: 10, fill: axisTextColor() }}
width={100}
/>
<Tooltip
contentStyle={{ fontSize: 12, borderRadius: 8 }}
{...tooltipProps()}
formatter={(value: unknown, name: unknown) => [`${Number(value)}`, String(name) === "total" ? "Total" : "Convertidos"]}
/>
<Bar dataKey="total" fill="#3B82F6" radius={[0, 4, 4, 0]} maxBarSize={18} />
@@ -193,16 +191,16 @@ export default function DashboardVendedor() {
) : (
<ResponsiveContainer width="100%" height={260}>
<BarChart data={datosDesarrollo} layout="vertical" margin={{ top: 8, right: 16, left: 8, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={GRID_LIGHT} horizontal={false} />
<XAxis type="number" tick={{ fontSize: 10, fill: LIGHT_TEXT }} allowDecimals={false} />
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke()} horizontal={false} />
<XAxis type="number" tick={{ fontSize: 10, fill: axisTextColor() }} allowDecimals={false} />
<YAxis
type="category"
dataKey="desarrollo"
tick={{ fontSize: 10, fill: LIGHT_TEXT }}
tick={{ fontSize: 10, fill: axisTextColor() }}
width={90}
/>
<Tooltip
contentStyle={{ fontSize: 12, borderRadius: 8 }}
{...tooltipProps()}
formatter={(value: unknown) => [`${Number(value)} ventas`, "Ventas"]}
/>
<Bar dataKey="ventas" fill="#8B5CF6" radius={[0, 4, 4, 0]} maxBarSize={18}>

View File

@@ -8,6 +8,21 @@
--color-accent-hover: #1D4ED8;
}
:root {
--chart-grid: #E5E7EB;
--chart-text: #64748B;
--chart-tooltip-bg: white;
--chart-tooltip-border: #E5E7EB;
--chart-tooltip-text: #111827;
}
.dark {
--chart-grid: #334155;
--chart-text: #94A3B8;
--chart-tooltip-bg: #1E293B;
--chart-tooltip-border: #334155;
--chart-tooltip-text: #E2E8F0;
}
@layer base {
body {
@apply bg-[#F8FAFC] text-[#111827] antialiased;

View File

@@ -0,0 +1,24 @@
/**
* Helpers de tema para gráficas Recharts.
*
* Recharts renderiza SVG puro y no resuelve CSS custom properties en
* atributos como `fill` / `stroke`, así que necesitamos funciones que
* lean el estado dark-mode del documento y devuelvan los colores
* apropiados como strings literales.
*/
/** `true` cuando el `<html>` lleva la clase `dark` (Tailwind convention). */
export const isDark = (): boolean =>
typeof document !== 'undefined' && document.documentElement.classList.contains('dark')
/** Color de texto para los ejes (XAxis / YAxis tick fill). */
export const axisTextColor = (): string => (isDark() ? '#94A3B8' : '#64748B')
/** Color de la cuadrícula (CartesianGrid stroke). */
export const gridStroke = (): string => (isDark() ? '#334155' : '#E5E7EB')
/** Props comunes de Tooltip para aplicar estilo dark/light. */
export const tooltipProps = (): Record<string, unknown> =>
isDark()
? { contentStyle: { backgroundColor: '#1E293B', borderColor: '#334155', color: '#E2E8F0', borderRadius: 8, fontSize: 12 } }
: { contentStyle: { borderRadius: 8, fontSize: 12 } }

View File

@@ -236,7 +236,7 @@ function FilaOportunidad({
type="button"
disabled={enviando}
onClick={onAfluencia}
className="rounded-lg border border-accent bg-white px-3 py-1.5 text-xs font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-accent hover:text-white disabled:opacity-50"
className="rounded-lg border border-accent bg-white px-3 py-1.5 text-xs font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-accent hover:text-white disabled:opacity-50 dark:bg-[#0F172A] dark:border-accent dark:text-accent"
>
Afluencia →
</button>
@@ -277,7 +277,7 @@ function FilaOportunidad({
type="button"
disabled={enviando || (acciones !== undefined && !acciones.includes('descartar_lead'))}
onClick={onDescartar}
className="rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 shadow-sm transition-all duration-100 ease-out hover:bg-red-50 disabled:opacity-50"
className="rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 shadow-sm transition-all duration-100 ease-out hover:bg-red-50 disabled:opacity-50 dark:bg-[#0F172A] dark:border-red-800 dark:text-red-400"
>
Descartar
</button>
@@ -339,7 +339,7 @@ function FilaApartado({
}}
className={
fase === 'Cancelacion Apartado'
? 'rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 shadow-sm transition-all duration-100 ease-out hover:bg-red-50 disabled:opacity-50'
? 'rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 shadow-sm transition-all duration-100 ease-out hover:bg-red-50 disabled:opacity-50 dark:bg-[#0F172A] dark:border-red-800 dark:text-red-400'
: fase === 'Firma Cliente (Venta)'
? 'rounded-lg bg-accent px-3 py-1.5 text-xs font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50'
: fase === apartado.estado
@@ -422,7 +422,7 @@ function PanelDocumentosApartado({ itemId }: { itemId: string }) {
{slot.obligatorio && <span className="ml-1 text-red-600">*</span>}
</p>
<label
className={`cursor-pointer rounded-lg border border-accent bg-white px-3 py-1.5 text-xs font-medium text-accent transition-all duration-100 ease-out hover:bg-accent hover:text-white ${
className={`cursor-pointer rounded-lg border border-accent bg-white px-3 py-1.5 text-xs font-medium text-accent transition-all duration-100 ease-out hover:bg-accent hover:text-white dark:bg-[#0F172A] dark:border-accent dark:text-accent ${
subiendo !== null ? 'pointer-events-none opacity-50' : ''
}`}
>
@@ -574,7 +574,7 @@ function FilaLead({
type="button"
disabled={enviando}
onClick={onConvertir}
className="rounded-lg border border-accent bg-white px-3 py-1.5 text-xs font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-accent hover:text-white disabled:opacity-50"
className="rounded-lg border border-accent bg-white px-3 py-1.5 text-xs font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-accent hover:text-white disabled:opacity-50 dark:bg-[#0F172A] dark:border-accent dark:text-accent"
>
Agendo →
</button>
@@ -582,7 +582,7 @@ function FilaLead({
type="button"
disabled={enviando || (acciones !== undefined && !acciones.includes("descartar_lead"))}
onClick={onDescartar}
className="rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 shadow-sm transition-all duration-100 ease-out hover:bg-red-50 disabled:opacity-50"
className="rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 shadow-sm transition-all duration-100 ease-out hover:bg-red-50 disabled:opacity-50 dark:bg-[#0F172A] dark:border-red-800 dark:text-red-400"
>
Descartar
</button>
@@ -854,7 +854,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
</p>
)}
{resumen?.sinMapeoMonday && (
<p className="mb-6 rounded-lg border border-[#E5E7EB] bg-white px-4 py-3 text-sm font-medium text-[#64748B] dark:text-[#94A3B8] shadow-sm">
<p className="mb-6 rounded-lg border border-[#E5E7EB] bg-white px-4 py-3 text-sm font-medium text-[#64748B] dark:text-[#94A3B8] shadow-sm dark:bg-[#1E293B] dark:border-[#1E293B]">
Tu usuario aún no está vinculado a Monday; las acciones siguen funcionando
</p>
)}
@@ -907,7 +907,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
<button
type="button"
onClick={() => exportarLeads(leadsFiltrados)}
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB] dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#CBD5E1]"
>
Exportar
</button>
@@ -949,7 +949,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
<button
type="button"
onClick={() => exportarOportunidades(opsFiltradas)}
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB] dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#CBD5E1]"
>
Exportar
</button>
@@ -995,7 +995,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
<button
type="button"
onClick={() => exportarApartados(apartadosFiltrados)}
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB] dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#CBD5E1]"
>
Exportar
</button>
@@ -1040,7 +1040,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
{resumen.misCapturas.map((c) => (
<li
key={c.leadId}
className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm"
className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm dark:bg-[#1E293B] dark:border-[#1E293B]"
>
<div>
<p className="flex items-center gap-2 text-sm font-medium">
@@ -1075,7 +1075,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
{resumen.misClientes.map((c) => (
<li
key={c.id}
className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm"
className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm dark:bg-[#1E293B] dark:border-[#1E293B]"
>
<div>
<p className="text-sm font-medium">{c.nombre}</p>
@@ -1120,7 +1120,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
{resumen.misActividadesHoy.map((a) => (
<li
key={a.id}
className="flex items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm"
className="flex items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm dark:bg-[#1E293B] dark:border-[#1E293B]"
>
<p className="text-sm">
{a.nombre}
@@ -1146,7 +1146,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
{resumen.apartadosPorVencer.map((a) => (
<li
key={a.id}
className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm"
className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm dark:bg-[#1E293B] dark:border-[#1E293B]"
>
<p className="text-sm">
{a.nombre}
@@ -1309,7 +1309,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
{resumen.equipo.porVendedor.map((v) => (
<li
key={v.mondayUserId}
className="flex items-center justify-between rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm"
className="flex items-center justify-between rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm dark:bg-[#1E293B] dark:border-[#1E293B]"
>
<p className="text-sm">{v.nombre ?? `Monday #${v.mondayUserId}`}</p>
<span className="rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">