Files
tiagoandClaude Sonnet 5 323912748f Frontend completo (Etapas 11-18) e extensões de backend (Etapas 13 e 18)
Adiciona o frontend Next.js em frontend/ (setup, design system,
autenticação/tema, layout/navegação, área master, área tenant completa:
clientes, produtos, vendas, recebimentos, whatsapp, e-mail, alertas,
equipe e perfil). Estende o backend de forma aditiva e autorizada:
cookie httpOnly para refresh token (Etapa 13) e CRUD de equipe +
edição de perfil do tenant (Etapa 18, módulo src/modules/equipe/ novo
e PUT /tenant/perfil). Detalhes completos em Etapas/V2/*.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 09:41:13 -03:00

110 lines
3.6 KiB
TypeScript

'use client';
import * as React from 'react';
import { fetchTenantTheme, type TenantTheme } from '@/lib/theme/resolve-theme';
import { ensureAccessibleTextColor } from '@/lib/theme/contrast';
interface TenantThemeContextValue {
theme: TenantTheme | null;
loading: boolean;
/**
* Atualiza o tema em runtime sem depender de um novo fetch (Etapa 18 —
* `/app/perfil`, critério de aceite "reflete instantaneamente sem
* reload"): aplica o patch tanto no estado (consumido por `TenantBrand`
* para nome/logo) quanto nas CSS variables de `:root` quando
* `corPrimaria` muda, mesma técnica de `ensureAccessibleTextColor` já
* usada na resolução inicial do tema.
*/
applyThemeUpdate: (patch: Partial<TenantTheme>) => void;
}
const TenantThemeContext = React.createContext<TenantThemeContextValue>({
theme: null,
loading: false,
applyThemeUpdate: () => {},
});
/**
* Resolve e aplica o tema do tenant (seção 6 do schema) via CSS variables
* em `:root`, com o ajuste de contraste da Etapa 13 embutido.
*
* `tenantId` é passado explicitamente pelo consumidor porque a origem
* muda conforme o contexto:
* - Pré-login (`/login`): só é conhecido se vier de `?tenant=<uuid>` na
* URL — o schema não tem `slug`/subdomínio (ver observações da Etapa
* 13 no controle de progresso), então sem esse parâmetro não há como
* identificar o tenant antes de autenticar, e o tema padrão da
* plataforma é usado (já é o fallback em `styles/tokens.css`).
* - Pós-login (`/app/*`, a partir da Etapa 14): `tenant_id` decodificado
* do access token do usuário autenticado.
*
* Sem `tenantId`, não faz nada — os valores padrão de `tokens.css`
* continuam valendo.
*/
export function TenantThemeProvider({
tenantId,
children,
}: {
tenantId: string | null;
children: React.ReactNode;
}) {
const [theme, setTheme] = React.useState<TenantTheme | null>(null);
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
if (!tenantId) {
document.documentElement.style.removeProperty('--color-primary');
document.documentElement.style.removeProperty('--color-primary-on-light');
setTheme(null);
return;
}
let cancelled = false;
setLoading(true);
fetchTenantTheme(tenantId).then((result) => {
if (cancelled) return;
setLoading(false);
setTheme(result);
if (result?.corPrimaria) {
document.documentElement.style.setProperty('--color-primary', result.corPrimaria);
document.documentElement.style.setProperty(
'--color-primary-on-light',
ensureAccessibleTextColor(result.corPrimaria),
);
}
});
return () => {
cancelled = true;
};
}, [tenantId]);
const applyThemeUpdate = React.useCallback((patch: Partial<TenantTheme>) => {
if (patch.corPrimaria) {
document.documentElement.style.setProperty('--color-primary', patch.corPrimaria);
document.documentElement.style.setProperty(
'--color-primary-on-light',
ensureAccessibleTextColor(patch.corPrimaria),
);
}
setTheme((prev) => ({
nome: patch.nome ?? prev?.nome ?? 'SeConectai Vendas',
logoUrl: patch.logoUrl !== undefined ? patch.logoUrl : (prev?.logoUrl ?? null),
corPrimaria: patch.corPrimaria ?? prev?.corPrimaria ?? null,
}));
}, []);
const value = React.useMemo(
() => ({ theme, loading, applyThemeUpdate }),
[theme, loading, applyThemeUpdate],
);
return <TenantThemeContext.Provider value={value}>{children}</TenantThemeContext.Provider>;
}
export function useTenantTheme(): TenantThemeContextValue {
return React.useContext(TenantThemeContext);
}