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>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
'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);
|
||||
}
|
||||
Reference in New Issue
Block a user