fix: correções de email, avatar e filtro de equipe no agendador

Backend:
- Corrigido mapeamento de email na listagem e busca por ID de profissionais. Agora o sistema utiliza o email do usuário vinculado (`usuario_email`) como fallback caso o email do perfil profissional esteja vazio.

Frontend:
- EventScheduler: Implementado filtro estrito para exibir apenas profissionais que foram explicitamente adicionados à equipe do evento ("Gerenciar Equipe"), prevenindo escalações indevidas.
- EventScheduler: Adicionada validação para ocultar cargos administrativos sem função operacional definida.
- ProfessionalDetailsModal: Corrigida a lógica de exibição do avatar para suportar a propriedade `avatar_url` (padrão atual do backend), resolvendo o problema de imagens quebradas ou ícones genéricos.
This commit is contained in:
NANDO9322 2026-01-12 12:20:08 -03:00
parent 9ff55b36bd
commit 7010e8e7d9
4 changed files with 16 additions and 6 deletions

View file

@ -89,6 +89,10 @@ func toResponse(p interface{}) ProfissionalResponse {
AvatarURL: fromPgText(v.AvatarUrl), AvatarURL: fromPgText(v.AvatarUrl),
} }
case generated.ListProfissionaisRow: case generated.ListProfissionaisRow:
email := fromPgText(v.Email)
if email == nil {
email = fromPgText(v.UsuarioEmail)
}
return ProfissionalResponse{ return ProfissionalResponse{
ID: uuid.UUID(v.ID.Bytes).String(), ID: uuid.UUID(v.ID.Bytes).String(),
UsuarioID: uuid.UUID(v.UsuarioID.Bytes).String(), UsuarioID: uuid.UUID(v.UsuarioID.Bytes).String(),
@ -116,7 +120,7 @@ func toResponse(p interface{}) ProfissionalResponse {
TabelaFree: fromPgText(v.TabelaFree), TabelaFree: fromPgText(v.TabelaFree),
ExtraPorEquipamento: fromPgBool(v.ExtraPorEquipamento), ExtraPorEquipamento: fromPgBool(v.ExtraPorEquipamento),
Equipamentos: fromPgText(v.Equipamentos), Equipamentos: fromPgText(v.Equipamentos),
Email: fromPgText(v.Email), Email: email,
AvatarURL: fromPgText(v.AvatarUrl), AvatarURL: fromPgText(v.AvatarUrl),
} }
case generated.GetProfissionalByIDRow: case generated.GetProfissionalByIDRow:
@ -147,6 +151,7 @@ func toResponse(p interface{}) ProfissionalResponse {
TabelaFree: fromPgText(v.TabelaFree), TabelaFree: fromPgText(v.TabelaFree),
ExtraPorEquipamento: fromPgBool(v.ExtraPorEquipamento), ExtraPorEquipamento: fromPgBool(v.ExtraPorEquipamento),
Equipamentos: fromPgText(v.Equipamentos), Equipamentos: fromPgText(v.Equipamentos),
Email: fromPgText(v.Email),
AvatarURL: fromPgText(v.AvatarUrl), AvatarURL: fromPgText(v.AvatarUrl),
} }
default: default:

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Plus, Trash, User, Truck, MapPin } from "lucide-react"; import { Plus, Trash, User, Truck, MapPin } from "lucide-react";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import { listEscalas, createEscala, deleteEscala, EscalaInput } from "../services/apiService"; import { listEscalas, createEscala, deleteEscala, EscalaInput, getFunctions } from "../services/apiService";
import { useData } from "../contexts/DataContext"; import { useData } from "../contexts/DataContext";
import { UserRole } from "../types"; import { UserRole } from "../types";
@ -23,6 +23,7 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
const { token, user } = useAuth(); const { token, user } = useAuth();
const { professionals, events } = useData(); const { professionals, events } = useData();
const [escalas, setEscalas] = useState<any[]>([]); const [escalas, setEscalas] = useState<any[]>([]);
const [roles, setRoles] = useState<{ id: string; nome: string }[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// New entry state // New entry state
@ -78,6 +79,9 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
useEffect(() => { useEffect(() => {
if (agendaId && token) { if (agendaId && token) {
fetchEscalas(); fetchEscalas();
getFunctions().then(res => {
if (res.data) setRoles(res.data);
});
} }
}, [agendaId, token]); }, [agendaId, token]);
@ -143,10 +147,11 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
}; };
// 1. Start with all professionals or just the allowed ones // 1. Start with all professionals or just the allowed ones
let availableProfs = professionals; // FILTER: Only show professionals with a valid role (Function), matching "Equipe" page logic.
let availableProfs = professionals.filter(p => roles.some(r => r.id === p.funcao_profissional_id));
const allowedMap = new Map<string, string>(); // ID -> Status const allowedMap = new Map<string, string>(); // ID -> Status
if (allowedProfessionals && allowedProfessionals.length > 0) { if (allowedProfessionals) {
// Normalize allowed list // Normalize allowed list
const ids: string[] = []; const ids: string[] = [];
allowedProfessionals.forEach((p: any) => { allowedProfessionals.forEach((p: any) => {

View file

@ -80,7 +80,7 @@ export const ProfessionalDetailsModal: React.FC<ProfessionalDetailsModalProps> =
<div <div
className="w-32 h-32 rounded-full border-4 border-white bg-white shadow-lg mb-4 bg-gray-200" className="w-32 h-32 rounded-full border-4 border-white bg-white shadow-lg mb-4 bg-gray-200"
style={{ style={{
backgroundImage: `url(${professional.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(professional.name || professional.nome)}&background=random`})`, backgroundImage: `url(${professional.avatar_url || professional.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(professional.name || professional.nome)}&background=random`})`,
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center' backgroundPosition: 'center'
}} }}

View file

@ -611,7 +611,7 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Nome do Titular da Conta ou Observação Observações
</label> </label>
<textarea <textarea
value={formData.observacao} value={formData.observacao}