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>
110 lines
3.6 KiB
TypeScript
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);
|
|
}
|