docs: adiciona estrutura de governança por contexto (AGENT, DOMAIN, DESIGN_SYSTEM, TASKS)

This commit is contained in:
Tiago Ribeiro 2026-03-07 17:31:44 -03:00
parent 722d7bf82a
commit ebbbda5328
2 changed files with 102 additions and 74 deletions

28
frontend/DESIGN_SYSTEM.md Normal file
View file

@ -0,0 +1,28 @@
# SaveInMed - Design System
Diretrizes visuais para o frontend do SaveInMed.
## 🎨 Paleta de Cores
- **Primária**: `blue-600` (#2563EB) - Botões principais, links ativos.
- **Sucesso**: `green-600` (#16A34A) - Confirmações, status "Entregue".
- **Aviso**: `yellow-500` (#EAB308) - Alertas, status "Pendente".
- **Erro**: `red-600` (#DC2626) - Botões de deletar, mensagens de erro.
- **Fundo**: `gray-50` (#F9FAFB) - Background das páginas.
## 🔡 Tipografia
- **Títulos**: Inter ou Sans-serif, font-bold, text-gray-900.
- **Corpo**: Inter, text-gray-600.
## 🧩 Componentes Padrão
- **Botões**:
- `bg-blue-600 hover:bg-blue-700 text-white rounded-lg px-4 py-2 transition-colors`
- **Inputs**:
- `border border-gray-300 focus:ring-2 focus:ring-blue-500 rounded-lg`
- **Cards**:
- `bg-white shadow-sm border border-gray-100 rounded-xl p-6`
## 📦 Ícones
- **Biblioteca**: `Heroicons 24/outline`
- **Home**: `HomeIcon`
- **Carrinho**: `ShoppingCartIcon`
- **Usuário**: `UserIcon`

View file

@ -15,21 +15,21 @@ const LoginPageContent = () => {
const { setEmpresaId } = useEmpresa();
const searchParams = useSearchParams();
// Estados do formulÃÆ’ƒÂ¡rio
const [email, setEmail] = useState<string>(""); // Email do usuÃÆ’ƒÂ¡rio
const [password, setPassword] = useState<string>(""); // Senha do usuÃÆ’ƒÂ¡rio
// Estados do formulário
const [email, setEmail] = useState<string>(""); // Email do usuário
const [password, setPassword] = useState<string>(""); // Senha do usuário
const [name, setName] = useState<string>(""); // Nome completo (apenas para registro)
const [loading, setLoading] = useState<boolean>(false); // Estado de carregamento
const [error, setError] = useState<string>(""); // Mensagens de erro
const [isLogin, setIsLogin] = useState<boolean>(true); // Alterna entre login e registro
const [checkingAuth, setCheckingAuth] = useState<boolean>(true); // VerificaÃÆÃ†â€™Ã‚ÂÂ§ÃÆÃ†â€™Ã‚Â£o inicial de autenticaÃÆÃ†â€™Ã‚ÂÂ§ÃÆÃ†â€™Ã‚Â£o
const [checkingAuth, setCheckingAuth] = useState<boolean>(true); // Verificação inicial de autenticação
const [showPassword, setShowPassword] = useState<boolean>(false); // Controla visibilidade da senha
const [accessToken, setAccessToken] = useState<string>(""); // Token de acesso da API
const [showSuccessModal, setShowSuccessModal] = useState<boolean>(false); // Modal de sucesso do cadastro
const [showPendingModal, setShowPendingModal] = useState<boolean>(false); // Modal de cadastro pendente
/**
* Verifica parÃÆÃâÃâšÂ¢metros da URL para definir aba inicial
* Verifica par¢metros da URL para definir aba inicial
*/
useEffect(() => {
const tab = searchParams.get('tab');
@ -39,13 +39,13 @@ const LoginPageContent = () => {
}, [searchParams]);
/**
* Verifica se o usuÃÆÃâÃâšÂ¡rio jÃÆÃâÃâšÂ¡ estÃÆÃâÃâšÂ¡ autenticado ao carregar a pÃÆÃâÃâšÂ¡gina
* Verifica se o usuário está autenticado ao carregar a gina
* Se estiver autenticado, redireciona para o dashboard
*/
useEffect(() => {
const checkAuth = async () => {
try {
// Verificar se hÃÆ’ƒÂ¡ token armazenado
// Verificar se há token armazenado
const storedToken = localStorage.getItem('access_token');
if (!storedToken) {
@ -53,14 +53,14 @@ const LoginPageContent = () => {
return;
}
// Verificar autenticaÃÆ’ÂÂ§ÃÆ’£o usando API com o token no header Authorization
// Verificar autenticação usando API com o token no header Authorization
const response = await fetch(
`${API_V1_BASE_URL}/auth/me`,
{
method: "GET",
headers: {
"accept": "application/json",
"Authorization": `Bearer ${storedToken}`, // Usar Authorization ao invÃÆ’ƒÂ©s de Cookie
"Authorization": `Bearer ${storedToken}`, // Usar Authorization ao invés de Cookie
},
}
);
@ -71,7 +71,7 @@ const LoginPageContent = () => {
router.push("/dashboard");
} else {
const errorText = await response.text();
// Limpar token invÃÆ’ƒÂ¡lido
// Limpar token inválido
localStorage.removeItem('access_token');
}
} catch (error) {
@ -83,9 +83,9 @@ const LoginPageContent = () => {
}, [router]);
/**
* FunÃÆÂÂ§ÃÆÂ£o para realizar login do usuÃÆÂ¡rio usando API
* @param email - Email do usuÃÆÃâÃâšÂ¡rio (usado como identificador)
* @param password - Senha do usuÃÆÃâÃâšÂ¡rio
* Função para realizar login do usuário usando API
* @param email - Email do usuário (usado como identificador)
* @param password - Senha do usuário
*/
const login = async (email: string, password: string) => {
setLoading(true);
@ -108,32 +108,32 @@ const LoginPageContent = () => {
})
});
// Ler e logar o corpo da resposta (usando clone para nÃÆ’ƒÂ£o consumir o body original)
// Ler e logar o corpo da resposta (usando clone para não consumir o body original)
const respClone = response.clone();
const respText = await respClone.text();
let respBody: any = respText;
try {
respBody = JSON.parse(respText);
} catch {
// corpo nÃÆÃ†â€™Ã‚Â£o ÃÆ’ƒÂ© JSON, manter como texto
// corpo não é JSON, manter como texto
}
if (!response.ok) {
// Extrair mensagem de erro do backend
let errorMessage = respBody?.message || respBody?.error || respBody?.detail || respText;
// Tratar cÃÆÃ†â€™Ã‚Â³digos de status especÃÆÃ†â€™Ã‚Â­ficos
// Tratar códigos de status específicos
if (response.status === 401) {
errorMessage = "Email ou senha incorretos. Verifique suas credenciais.";
} else if (response.status === 403) {
errorMessage = "Acesso negado. Sua conta pode estar inativa.";
} else if (response.status === 404) {
errorMessage = "UsuÃÆÃ†â€™Ã‚Â¡rio nÃÆÃ†â€™Ã‚Â£o encontrado. Verifique o email informado.";
errorMessage = "Usuário não encontrado. Verifique o email informado.";
} else if (response.status >= 500) {
errorMessage = "Erro interno do servidor. Tente novamente em alguns minutos.";
}
// Aplicar traduÃÆÃ†â€™Ã‚ÂÂ§ÃÆÃ†â€™Ã‚Â£o se disponÃÆÃ†â€™Ã‚Â­vel
// Aplicar tradução se disponível
const translatedError = translateError(errorMessage);
throw new Error(translatedError);
}
@ -158,24 +158,24 @@ const LoginPageContent = () => {
if (meResponse.ok) {
const userData = await meResponse.json();
} else {
console.log("ÃÆ’¢ÂÃɉ۪ Falha no /me:", await meResponse.text());
console.log("¢Ãɉ۪ Falha no /me:", await meResponse.text());
}
} else {
throw new Error("Token nÃÆ’ƒÂ£o recebido do servidor");
throw new Error("Token não recebido do servidor");
}
// 4. Armazenar informaÃÆÃ†â€™Ã‚ÂÂ§ÃÆÃ†â€™Ã‚Âµes do usuÃÆÃ†â€™Ã‚Â¡rio no localStorage
// 4. Armazenar informaçµes do usuário no localStorage
if (loginData.user) {
localStorage.setItem('user', JSON.stringify(loginData.user));
// Verificar se o registro estÃÆ’ƒÂ¡ completo
// Verificar se o registro está completo
if (loginData.user["registro-completo"] === false) {
setShowPendingModal(true);
return; // NÃÆ’ƒÂ£o continuar com o redirecionamento
return; // Não continuar com o redirecionamento
}
// Armazenar ID do endereÃÆÃ†â€™Ã‚Â§o se disponÃÆÃ†â€™Ã‚Â­vel
// Armazenar ID do endereço se disponível
// Backend pode retornar "endereco" (singular) ou "enderecos" (plural array)
let enderecoId = loginData.user.endereco;
@ -189,7 +189,7 @@ const LoginPageContent = () => {
}
}
// 5. Verificar e armazenar empresa ID se disponÃÆ’ƒÂ­vel
// 5. Verificar e armazenar empresa ID se disponível
if (loginData.user?.empresaId) {
setEmpresaId(loginData.user.empresaId);
}
@ -198,16 +198,16 @@ const LoginPageContent = () => {
router.push("/dashboard");
} catch (error: any) {
console.error("ÃÆ’¢ÂÃɉ۪ Erro no login:", error);
console.error("¢Ãɉ۪ Erro no login:", error);
// Definir mensagem de erro amigÃÆ’ƒÂ¡vel
// Definir mensagem de erro amigável
let friendlyMessage = error.message;
// Se for um erro de rede
if (error.name === 'TypeError' || error.message.includes('fetch')) {
friendlyMessage = "Erro de conexÃÆ’ƒÂ£o. Verifique sua internet e tente novamente.";
friendlyMessage = "Erro de conexão. Verifique sua internet e tente novamente.";
}
// Se for um erro genÃÆ’ƒÂ©rico sem mensagem clara
// Se for um erro genérico sem mensagem clara
else if (!error.message || error.message.length < 5) {
friendlyMessage = "Erro inesperado. Verifique suas credenciais e tente novamente.";
}
@ -219,7 +219,7 @@ const LoginPageContent = () => {
};
/**
* FunÃÆÂÂ§ÃÆÂ£o para registrar novo usuÃÆÂ¡rio usando API
* Função para registrar novo usuário usando API
* Cria conta e mostra mensagem de sucesso
*/
const register = async () => {
@ -235,12 +235,12 @@ const LoginPageContent = () => {
'Content-Type': 'application/json',
},
body: JSON.stringify({
identificador: email, // identificador = email do usuÃÆ’ƒÂ¡rio (usado no login)
identificador: email, // identificador = email do usuário (usado no login)
email: email,
nome: name, // nome = nome completo do usuÃÆ’ƒÂ¡rio
nome: name, // nome = nome completo do usuário
senha: password,
nivel: "owner", // valor estÃÆ’ƒÂ¡tico
"registro-completo": false // valor estÃÆÃ†â€™Ã‚Â¡tico - registro incompleto por padrÃÆÃ†â€™Ã‚Â£o
nivel: "owner", // valor estático
"registro-completo": false // valor estático - registro incompleto por padrão
})
});
@ -248,25 +248,25 @@ const LoginPageContent = () => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
// Tratar cÃÆÃ†â€™Ã‚Â³digos de status especÃÆÃ†â€™Ã‚Â­ficos
// Tratar códigos de status específicos
let errorMessage = errorData.message || 'Falha no registro';
if (response.status === 409 || errorMessage.includes('already exists') || errorMessage.includes('jÃÆ’ƒÂ¡ existe')) {
errorMessage = 'Este email jÃÆ’ƒÂ¡ estÃÆÃ†â€™Ã‚Â¡ cadastrado. Tente fazer login ou use outro email.';
if (response.status === 409 || errorMessage.includes('already exists') || errorMessage.includes('já existe')) {
errorMessage = 'Este email já está cadastrado. Tente fazer login ou use outro email.';
} else if (response.status === 400) {
errorMessage = 'Dados invÃÆÃ†â€™Ã‚Â¡lidos. Verifique se todos os campos estÃÆÃ†â€™Ã‚Â£o preenchidos corretamente.';
errorMessage = 'Dados inválidos. Verifique se todos os campos estão preenchidos corretamente.';
} else if (response.status >= 500) {
errorMessage = 'Erro interno do servidor. Tente novamente em alguns minutos.';
}
// Aplicar traduÃÆÃ†â€™Ã‚ÂÂ§ÃÆÃ†â€™Ã‚Â£o se disponÃÆÃ†â€™Ã‚Â­vel
// Aplicar tradução se disponível
const translatedError = translateError(errorMessage);
throw new Error(translatedError);
}
const registerData = await response.json();
// 2. Fazer login automÃÆÃ†â€™Ã‚Â¡tico apÃÆÃ†â€™Ã‚Â³s registro para obter token
// 2. Fazer login automático após registro para obter token
const loginResponse = await fetch(`${baseUrl}/auth/login`, {
method: 'POST',
headers: {
@ -282,13 +282,13 @@ const LoginPageContent = () => {
});
if (!loginResponse.ok) {
console.error("ÃÆ’¢ÂÃɉ۪ Erro no login automÃÆÃ†â€™Ã‚Â¡tico:", loginResponse.status);
throw new Error('Erro ao fazer login automÃÆÃ†â€™Ã‚Â¡tico apÃÆÃ†â€™Ã‚Â³s registro');
console.error("¢Ãɉ۪ Erro no login automático:", loginResponse.status);
throw new Error('Erro ao fazer login automático após registro');
}
const loginData = await loginResponse.json();
// 3. Armazenar token e dados do usuÃÆ’ƒÂ¡rio
// 3. Armazenar token e dados do usuário
if (loginData.access_token) {
localStorage.setItem('access_token', loginData.access_token);
}
@ -297,20 +297,20 @@ const LoginPageContent = () => {
localStorage.setItem('user', JSON.stringify(loginData.user));
}
// 4. Redirecionar para completar registro com token vÃÆ’ƒÂ¡lido
// 4. Redirecionar para completar registro com token válido
router.push(`/completar-registro?nome=${encodeURIComponent(name)}&email=${encodeURIComponent(email)}`);
} catch (error: any) {
console.error("ÃÆ’¢ÂÃɉ۪ Erro no registro:", error);
console.error("¢Ãɉ۪ Erro no registro:", error);
// Definir mensagem de erro amigÃÆ’ƒÂ¡vel
// Definir mensagem de erro amigável
let friendlyMessage = error.message;
// Se for um erro de rede
if (error.name === 'TypeError' || error.message.includes('fetch')) {
friendlyMessage = "Erro de conexÃÆ’ƒÂ£o. Verifique sua internet e tente novamente.";
friendlyMessage = "Erro de conexão. Verifique sua internet e tente novamente.";
}
// Se for um erro genÃÆ’ƒÂ©rico sem mensagem clara
// Se for um erro genérico sem mensagem clara
else if (!error.message || error.message.length < 5) {
friendlyMessage = "Erro inesperado durante o cadastro. Tente novamente.";
}
@ -322,11 +322,11 @@ const LoginPageContent = () => {
};
/**
* FunÃÆÃâÃâšÂÂ§ÃÆÃâÃâšÂ£o para notificar administrador sobre novo cadastro
* Função para notificar administrador sobre novo cadastro
*/
const notifyAdmin = async (userData: { nome: string, email: string, identificador: string }) => {
try {
// Enviar notificaÃÆ’ƒÂÂÂ§ÃÆ’ƒÂµes via API (usando Resend para emails reais)
// Enviar notificaçµes via API (usando Resend para emails reais)
await fetch('/api/notify-admin-resend', {
method: 'POST',
headers: {
@ -335,13 +335,13 @@ const LoginPageContent = () => {
body: JSON.stringify(userData)
});
} catch (error) {
console.error("Erro ao enviar notificaÃÆ’ƒÂÂÂ§ÃÆ’ƒÂµes:", error);
// NÃÆÃ†â€™Ã‚Â£o bloquear o cadastro por erro de notificaÃÆÃ†â€™Ã‚ÂÂ§ÃÆÃ†â€™Ã‚Â£o
console.error("Erro ao enviar notificaçµes:", error);
// Não bloquear o cadastro por erro de notificação
}
};
/**
* FunÃÆÃâÃâšÂÂ§ÃÆÃâÃâšÂ£o para fechar modal de sucesso e redirecionar
* Função para fechar modal de sucesso e redirecionar
*/
const handleSuccessModalClose = () => {
setShowSuccessModal(false);
@ -349,25 +349,25 @@ const LoginPageContent = () => {
};
/**
* FunÃÆÃâÃâšÂÂ§ÃÆÃâÃâšÂ£o para fechar modal de cadastro pendente
* Função para fechar modal de cadastro pendente
*/
const handlePendingModalClose = () => {
setShowPendingModal(false);
// Fazer logout para limpar dados do usuÃÆ’ƒÂ¡rio
// Fazer logout para limpar dados do usuário
localStorage.removeItem('access_token');
localStorage.removeItem('user');
setAccessToken("");
// Redirecionar para pÃÆ’ƒÂ¡gina inicial
// Redirecionar para página inicial
router.push('/');
};
// Tela de carregamento durante verificaÃÆÃ†â€™Ã‚ÂÂ§ÃÆÃ†â€™Ã‚Â£o de autenticaÃÆÃ†â€™Ã‚ÂÂ§ÃÆÃ†â€™Ã‚Â£o
// Tela de carregamento durante verificação de autenticação
if (checkingAuth) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-gray-600">Verificando autenticaÃÆÃâÃâšÂÂ§ÃÆÃâÃâšÂ£o...</p>
<p className="text-gray-600">Verificando autenticação...</p>
</div>
</div>
);
@ -376,7 +376,7 @@ const LoginPageContent = () => {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-xl overflow-hidden w-full max-w-md">
{/* CabeÃÆ’ƒÂ§alho com logo e slogan */}
{/* Cabeçalho com logo e slogan */}
<div className="bg-gradient-to-r from-blue-600 to-blue-700 p-1 text-center">
<div className="mx-auto -mb-2 flex items-center justify-center">
<Image
@ -392,9 +392,9 @@ const LoginPageContent = () => {
<p className="text-gray-300">Plataforma B2B de Medicamentos</p>
</div>
{/* FormulÃÆ’ƒÂ¡rio de login/registro */}
{/* Formulário de login/registro */}
<div className="p-6">
{/* BotÃÆ’ƒÂµes de alternÃÆ’ƒÂ¢ncia entre Login e Cadastro */}
{/* Botµes de altern¢ncia entre Login e Cadastro */}
<div className="flex bg-gray-100 rounded-lg p-1 mb-6">
<button
onClick={() => setIsLogin(true)}
@ -464,7 +464,7 @@ const LoginPageContent = () => {
</div>
<input
type="text"
placeholder="JoÃÆ’ƒÂ£o Silva"
placeholder="João Silva"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors text-gray-900 placeholder-gray-500"
@ -527,7 +527,7 @@ const LoginPageContent = () => {
</div>
<input
type={showPassword ? "text" : "password"}
placeholder="ÃÆ’¢â‚¬ÂÂÂ¢ÃÆ’¢â‚¬ÂÂÂ¢ÃÆ’¢â‚¬ÂÂÂ¢ÃÆ’¢â‚¬ÂÂÂ¢ÃÆ’¢â‚¬ÂÂÂ¢ÃÆ’¢â‚¬ÂÂÂ¢ÃÆ’¢â‚¬ÂÂÂ¢ÃÆ’¢â‚¬Â¢"
placeholder="¢â‚¬¢Â¢Ã¢â€šÂ¬¢Â¢Ã¢â€šÂ¬¢Â¢Ã¢â€šÂ¬¢Â¢Ã¢â€šÂ¬¢Â¢Ã¢â€šÂ¬¢Â¢Ã¢â€šÂ¬¢Â¢Ã¢â€šÂ¬¢"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-10 pr-12 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors text-gray-900 placeholder-gray-500"
@ -576,7 +576,7 @@ const LoginPageContent = () => {
</div>
</div>
{/* BotÃÆ’ƒÂµes de Submit */}
{/* Botµes de Submit */}
<div className="space-y-3 pt-2">
{isLogin ? (
<button
@ -708,18 +708,18 @@ const LoginPageContent = () => {
</h2>
</div>
{/* ConteÃÆ’ƒÂºdo do Modal */}
{/* Conteúdo do Modal */}
<div className="p-6 text-center">
<div className="mb-6">
<p className="text-gray-700 text-lg leading-relaxed">
Seu cadastro foi enviado com <strong>sucesso</strong>!
</p>
<p className="text-gray-600 mt-3">
Nossa equipe entrarÃÆÃâÃâšÂ¡ em contato em breve para finalizar seu acesso ÃÆÃâÃâšÂ  plataforma.
Nossa equipe entrará em contato em breve para finalizar seu acesso   plataforma.
</p>
</div>
{/* ÃÆ’ƒÂcone decorativo */}
{/* cone decorativo */}
<div className="flex justify-center mb-6">
<div className="flex space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
@ -728,7 +728,7 @@ const LoginPageContent = () => {
</div>
</div>
{/* BotÃÆ’ƒÂ£o de OK */}
{/* Botão de OK */}
<button
onClick={handleSuccessModalClose}
className="w-full bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-4 focus:ring-green-200"
@ -766,21 +766,21 @@ const LoginPageContent = () => {
</h2>
</div>
{/* ConteÃÆ’ƒÂºdo do Modal */}
{/* Conteúdo do Modal */}
<div className="p-6 text-center">
<div className="mb-6">
<p className="text-gray-700 text-lg leading-relaxed">
Seu cadastro estÃÆÃâÃâšÂ¡ <strong>pendente de validaÃÆÃâÃâšÂÂ§ÃÆÃâÃâšÂ£o</strong>!
Seu cadastro está <strong>pendente de validação</strong>!
</p>
<p className="text-gray-600 mt-3">
Nossa equipe ainda estÃÆÃâÃâšÂ¡ analisando suas informaÃÆÃâÃâšÂÂ§ÃÆÃâÃâšÂµes. VocÃÆÃâÃâšÂª receberÃÆÃâÃâšÂ¡ uma confirmaÃÆÃâÃâšÂÂ§ÃÆÃâÃâšÂ£o em breve.
Nossa equipe ainda está analisando suas informaçµes. Você receberá uma confirmação em breve.
</p>
<p className="text-gray-600 mt-3">
Qualquer dÃÆÃâÃâšÂºvida, entre em contato conosco.
Qualquer dúvida, entre em contato conosco.
</p>
</div>
{/* ÃÆ’ƒÂcone decorativo */}
{/* cone decorativo */}
<div className="flex justify-center mb-6">
<div className="flex space-x-2">
<div className="w-3 h-3 bg-yellow-500 rounded-full animate-pulse"></div>
@ -789,7 +789,7 @@ const LoginPageContent = () => {
</div>
</div>
{/* BotÃÆ’ƒÂ£o de OK */}
{/* Botão de OK */}
<button
onClick={handlePendingModalClose}
className="w-full bg-gradient-to-r from-yellow-500 to-orange-500 hover:from-yellow-600 hover:to-orange-600 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-4 focus:ring-yellow-200"