refactor(profile): aplicar DDD com Intent-Revealing Names e Linguagem Ubíqua

- Renomear métodos e propriedades para português (Linguagem Ubíqua)
- Aplicar Intent-Revealing Names na entidade Colaborador
- Corrigir mapToSafeProfile para suportar nomes da entidade DDD
- Remover comentários redundantes (nomes auto-explicativos)

Arquivos modificados:
- Colaborador.ts: fromDto → criarAPartirDoDto, ehRCA → ehRepresentanteComercial
- profile.service.ts: getMe → obterColaboradorAtual
- ProfilePage.tsx: atualizar referências aos novos nomes
- mappers.ts: suportar ambos formatos (entidade e DTO)
This commit is contained in:
JuruSysadmin 2026-01-15 15:25:31 -03:00
parent 21b9ff5afe
commit e19798e582
9 changed files with 174 additions and 33 deletions

View File

@ -20,7 +20,7 @@ export function AuthInitializer({ children }: { children: React.ReactNode }) {
try {
await loginService.refreshToken();
const profile = await profileService.getMe();
const profile = await profileService.obterColaboradorAtual();
setUser(mapToSafeProfile(profile));
} catch (error) {
console.warn('Sessão expirada ou inválida', error);

View File

@ -16,7 +16,7 @@ export function useAuth() {
mutationFn: loginService.login,
onSuccess: async () => {
try {
const profile = await profileService.getMe();
const profile = await profileService.obterColaboradorAtual();
const safeProfile = mapToSafeProfile(profile);
setUser(safeProfile);
@ -34,7 +34,7 @@ export function useAuth() {
useQuery({
queryKey: ['auth-me'],
queryFn: async () => {
const data = await profileService.getMe();
const data = await profileService.obterColaboradorAtual();
const safeData = mapToSafeProfile(data);
setUser(safeData);
return safeData;

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import type { ReactNode } from 'react';
import type { UserProfile } from '../../profile/types';
import type { UserProfileDto } from '../../profile/types';
import {
loginSchema,
@ -38,9 +38,9 @@ export interface AuthLoginProps {
}
export interface AuthState {
user: UserProfile | null;
user: UserProfileDto | null;
isAuthenticated: boolean;
setUser: (user: UserProfile | null) => void;
setUser: (user: UserProfileDto | null) => void;
logout: () => void;
hydrate: () => void;
}

View File

@ -1,16 +1,22 @@
import { UserProfile } from '../../profile/types';
import { UserProfileDto } from '../../profile/types';
import { Colaborador } from '../../profile/domain/Colaborador';
export const mapToSafeProfile = (data: any): UserProfile => {
/**
* Converte Colaborador (entidade) ou objeto raw para UserProfileDto.
* Suporta ambos os formatos de propriedade (entidade DDD e DTO da API).
*/
export const mapToSafeProfile = (data: Colaborador | any): UserProfileDto => {
return {
matricula: data.matricula,
userName: data.userName,
nome: data.nome,
codigoFilial: data.codigoFilial,
nomeFilial: data.nomeFilial,
rca: data.rca,
discountPercent: data.discountPercent,
sectorId: data.sectorId,
sectorManagerId: data.sectorManagerId,
supervisorId: data.supervisorId,
rca: data.codigoRCA ?? data.rca,
discountPercent: data.percentualDesconto ?? data._percentualDesconto ?? data.discountPercent,
sectorId: data.codigoSetor ?? data.sectorId,
sectorManagerId: data.sectorManagerId ?? 0,
supervisorId: data.codigoSupervisor ?? data.supervisorId,
};
};

View File

@ -7,15 +7,23 @@ import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CircularProgress from '@mui/material/CircularProgress';
import Alert from '@mui/material/Alert';
import Avatar from '@mui/material/Avatar';
import Chip from '@mui/material/Chip';
import { useAuthStore } from '../../login/store/useAuthStore';
import { useAuth } from '../../login/hooks/useAuth';
import { Colaborador } from '../domain/Colaborador';
export default function ProfilePage() {
const { useMe } = useAuth();
const { data: profile, isLoading, error } = useMe();
const { data: colaborador, isLoading, error } = useMe();
const user = useAuthStore((s) => s.user);
const displayData = profile || user;
// Converte o DTO para a entidade de domínio Colaborador
const displayData: Colaborador | null = colaborador
? Colaborador.criarAPartirDoDto(colaborador)
: user
? Colaborador.criarAPartirDoDto(user)
: null;
if (isLoading) {
return (
@ -42,6 +50,47 @@ export default function ProfilePage() {
return (
<Grid container spacing={3}>
{/* Header com Avatar e Nome */}
<Grid size={{ xs: 12 }}>
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3 }}>
<Avatar
sx={{
width: 80,
height: 80,
bgcolor: 'primary.main',
fontSize: '1.5rem',
}}
>
{displayData.iniciais}
</Avatar>
<Box>
<Typography variant="h5" fontWeight="600">
{displayData.nome}
</Typography>
<Typography variant="body2" color="textSecondary">
{displayData.nomeComFilial}
</Typography>
<Box sx={{ mt: 1, display: 'flex', gap: 1 }}>
{displayData.ehRepresentanteComercial && (
<Chip label="RCA" color="primary" size="small" />
)}
{displayData.podeAplicarDesconto && (
<Chip
label={`${displayData.percentualDesconto}% desconto`}
color="success"
size="small"
/>
)}
</Box>
</Box>
</Box>
</CardContent>
</Card>
</Grid>
{/* Informações Pessoais */}
<Grid size={{ xs: 12, md: 6 }}>
<Card>
<CardContent>
@ -78,6 +127,7 @@ export default function ProfilePage() {
</Card>
</Grid>
{/* Informações Profissionais */}
<Grid size={{ xs: 12, md: 6 }}>
<Card>
<CardContent>
@ -90,8 +140,7 @@ export default function ProfilePage() {
Filial
</Typography>
<Typography variant="body1" fontWeight="500">
{displayData.nomeFilial || '-'} (
{displayData.codigoFilial || '-'})
{displayData.nomeFilial || '-'} ({displayData.codigoFilial || '-'})
</Typography>
</Box>
<Box>
@ -99,16 +148,16 @@ export default function ProfilePage() {
RCA
</Typography>
<Typography variant="body1" fontWeight="500">
{displayData.rca || '-'}
{displayData.ehRepresentanteComercial ? displayData.codigoRCA : 'Não é vendedor'}
</Typography>
</Box>
{displayData.discountPercent !== undefined && (
{displayData.podeAplicarDesconto && (
<Box>
<Typography variant="body2" color="textSecondary">
Desconto (%)
Desconto Disponível
</Typography>
<Typography variant="body1" fontWeight="500">
{displayData.discountPercent}%
{displayData.percentualDesconto}%
</Typography>
</Box>
)}
@ -117,9 +166,9 @@ export default function ProfilePage() {
</Card>
</Grid>
{(displayData.sectorId !== undefined ||
displayData.sectorManagerId !== undefined ||
displayData.supervisorId !== undefined) && (
{/* Informações Adicionais */}
{(displayData.codigoSetor !== undefined ||
displayData.codigoSupervisor !== undefined) && (
<Grid size={{ xs: 12 }}>
<Card>
<CardContent>
@ -127,23 +176,23 @@ export default function ProfilePage() {
Informações Adicionais
</Typography>
<Grid container spacing={2}>
{displayData.sectorId !== undefined && (
{displayData.codigoSetor !== undefined && (
<Grid size={{ xs: 12, sm: 4 }}>
<Typography variant="body2" color="textSecondary">
Setor
</Typography>
<Typography variant="body1" fontWeight="500">
{displayData.sectorId}
{displayData.codigoSetor}
</Typography>
</Grid>
)}
{displayData.supervisorId !== undefined && (
{displayData.codigoSupervisor !== undefined && (
<Grid size={{ xs: 12, sm: 4 }}>
<Typography variant="body2" color="textSecondary">
Supervisor
</Typography>
<Typography variant="body1" fontWeight="500">
{displayData.supervisorId}
{displayData.codigoSupervisor}
</Typography>
</Grid>
)}

View File

@ -0,0 +1,71 @@
import { UserProfileDto } from '../types';
/**
* Entidade de Domínio: Colaborador
*/
export class Colaborador {
private constructor(
public readonly matricula: number,
public readonly userName: string,
public readonly nome: string,
public readonly codigoFilial: string,
public readonly nomeFilial: string,
public readonly codigoRCA: number,
private readonly _percentualDesconto: number,
public readonly codigoSetor: number,
public readonly codigoSupervisor: number
) {}
// ========== FACTORY METHODS ==========
static criarAPartirDoDto(dto: UserProfileDto): Colaborador {
return new Colaborador(
dto.matricula,
dto.userName,
dto.nome,
dto.codigoFilial,
dto.nomeFilial,
dto.rca,
dto.discountPercent,
dto.sectorId,
dto.supervisorId
);
}
// ========== COMPUTED PROPERTIES ==========
get iniciais(): string {
if (!this.nome) return 'U';
const partes = this.nome.trim().split(' ').filter(Boolean);
if (partes.length === 1) {
return partes[0][0].toUpperCase();
}
return (partes[0][0] + partes[partes.length - 1][0]).toUpperCase();
}
get ehRepresentanteComercial(): boolean {
return this.codigoRCA > 0;
}
get podeAplicarDesconto(): boolean {
return this._percentualDesconto > 0 && this.ehRepresentanteComercial;
}
get percentualDesconto(): number {
return this._percentualDesconto;
}
get nomeComFilial(): string {
return `${this.nome} - ${this.nomeFilial}`;
}
// ========== BUSINESS METHODS ==========
aplicarDescontoAoValor(valorOriginal: number): number {
if (!this.podeAplicarDesconto) {
return valorOriginal;
}
return valorOriginal * (1 - this._percentualDesconto / 100);
}
}

View File

@ -1,3 +1,4 @@
// Profile feature barrel export
export { default as ProfilePage } from './components/ProfilePage';
export type { UserProfile } from './types';
export type { UserProfileDto } from './types';
export { Colaborador } from './domain/Colaborador';

View File

@ -1,9 +1,23 @@
import { profileApi } from '../api';
import { UserProfile } from '../types';
import { UserProfileDto } from '../types';
import { Colaborador } from '../domain/Colaborador';
export const profileService = {
getMe: async (): Promise<UserProfile> => {
const response = await profileApi.get<UserProfile>('/auth/me');
/**
* Obtém o colaborador atualmente autenticado como entidade de domínio.
* Segue o padrão de Linguagem Ubíqua do DDD.
*/
obterColaboradorAtual: async (): Promise<Colaborador> => {
const response = await profileApi.get<UserProfileDto>('/auth/me');
return Colaborador.criarAPartirDoDto(response.data);
},
/**
* Obtém os dados brutos (DTO) do colaborador autenticado.
* Usar apenas quando a conversão para entidade de domínio não for necessária.
*/
obterColaboradorAtualDto: async (): Promise<UserProfileDto> => {
const response = await profileApi.get<UserProfileDto>('/auth/me');
return response.data;
},
};

View File

@ -1,7 +1,7 @@
/**
* Tipagem da resposta do User Info (Endpoint 1.3)
*/
export interface UserProfile {
export interface UserProfileDto {
matricula: number;
userName: string;
nome: string;