photum/backend/internal/profissionais/service.go
NANDO9322 7536bddacb feat: sistema de validação de conflitos e melhorias na UX da agenda
Implementa validação de horários para evitar conflitos no aceite de eventos, correções na sincronização de dados da agenda e melhorias na interface de gestão de equipe.

Backend:
- handler.go: Correção no retorno do endpoint [UpdateAssignmentStatus](cci:1://file:///c:/Projetos/photum/backend/internal/agenda/handler.go:279:0-313:1) para enviar JSON válido e evitar erros no frontend.
- service.go: Implementação da lógica de validação de conflitos antes de aceitar um evento.
- agenda.sql: Nova query `CheckProfessionalBusyDate` para verificação de sobreposição de horários.

Frontend:
- Dashboard.tsx: Adição de tooltip e texto para exibir o "Motivo da Rejeição" na gestão de equipe (Desktop/Mobile).
- EventScheduler.tsx: Filtro para excluir profissionais com status 'REJEITADO' e correção na label de 'Pendente'.
- EventDetails.tsx: Refatoração para usar estado global ([useData](cci:1://file:///c:/Projetos/photum/frontend/contexts/DataContext.tsx:1156:0-1160:2)), garantindo atualização imediata de datas e locais.
- DataContext.tsx: Mapeamento do campo `local_evento` e melhoria no tratamento de erro otimista.
- Ajustes gerais em ProfessionalDetailsModal, Login e correções de tipos.
2025-12-30 11:24:53 -03:00

278 lines
9.5 KiB
Go

package profissionais
import (
"context"
"errors"
"fmt"
"photum-backend/internal/db/generated"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
type Service struct {
queries *generated.Queries
}
func NewService(queries *generated.Queries) *Service {
return &Service{queries: queries}
}
type CreateProfissionalInput struct {
Nome string `json:"nome"`
FuncaoProfissionalID string `json:"funcao_profissional_id"`
Endereco *string `json:"endereco"`
Cidade *string `json:"cidade"`
Uf *string `json:"uf"`
Whatsapp *string `json:"whatsapp"`
CpfCnpjTitular *string `json:"cpf_cnpj_titular"`
Banco *string `json:"banco"`
Agencia *string `json:"agencia"`
ContaPix *string `json:"conta_pix"`
CarroDisponivel *bool `json:"carro_disponivel"`
TemEstudio *bool `json:"tem_estudio"`
QtdEstudio *int `json:"qtd_estudio"`
TipoCartao *string `json:"tipo_cartao"`
Observacao *string `json:"observacao"`
QualTec *int `json:"qual_tec"`
EducacaoSimpatia *int `json:"educacao_simpatia"`
DesempenhoEvento *int `json:"desempenho_evento"`
DispHorario *int `json:"disp_horario"`
Media *float64 `json:"media"`
TabelaFree *string `json:"tabela_free"`
ExtraPorEquipamento *bool `json:"extra_por_equipamento"`
Equipamentos *string `json:"equipamentos"`
Email *string `json:"email"`
AvatarURL *string `json:"avatar_url"`
TargetUserID *string `json:"target_user_id"` // Optional: For admin creation
}
func (s *Service) Create(ctx context.Context, userID string, input CreateProfissionalInput) (*generated.CadastroProfissionai, error) {
finalUserID := userID
if input.TargetUserID != nil && *input.TargetUserID != "" {
finalUserID = *input.TargetUserID
}
usuarioUUID, err := uuid.Parse(finalUserID)
if err != nil {
return nil, errors.New("invalid usuario_id")
}
var funcaoUUID uuid.UUID
var funcaoValid bool
if input.FuncaoProfissionalID != "" {
parsed, err := uuid.Parse(input.FuncaoProfissionalID)
if err != nil {
return nil, errors.New("invalid funcao_profissional_id")
}
funcaoUUID = parsed
funcaoValid = true
} else {
funcaoValid = false
}
params := generated.CreateProfissionalParams{
UsuarioID: pgtype.UUID{Bytes: usuarioUUID, Valid: true},
Nome: input.Nome,
FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: funcaoValid},
Endereco: toPgText(input.Endereco),
Cidade: toPgText(input.Cidade),
Uf: toPgText(input.Uf),
Whatsapp: toPgText(input.Whatsapp),
CpfCnpjTitular: toPgText(input.CpfCnpjTitular),
Banco: toPgText(input.Banco),
Agencia: toPgText(input.Agencia),
ContaPix: toPgText(input.ContaPix),
CarroDisponivel: toPgBool(input.CarroDisponivel),
TemEstudio: toPgBool(input.TemEstudio),
QtdEstudio: toPgInt4(input.QtdEstudio),
TipoCartao: toPgText(input.TipoCartao),
Observacao: toPgText(input.Observacao),
QualTec: toPgInt4(input.QualTec),
EducacaoSimpatia: toPgInt4(input.EducacaoSimpatia),
DesempenhoEvento: toPgInt4(input.DesempenhoEvento),
DispHorario: toPgInt4(input.DispHorario),
Media: toPgNumeric(input.Media),
TabelaFree: toPgText(input.TabelaFree),
ExtraPorEquipamento: toPgBool(input.ExtraPorEquipamento),
Equipamentos: toPgText(input.Equipamentos),
Email: toPgText(input.Email),
AvatarUrl: toPgText(input.AvatarURL),
}
prof, err := s.queries.CreateProfissional(ctx, params)
if err != nil {
return nil, err
}
return &prof, nil
}
func (s *Service) List(ctx context.Context) ([]generated.ListProfissionaisRow, error) {
return s.queries.ListProfissionais(ctx)
}
func (s *Service) GetByID(ctx context.Context, id string) (*generated.GetProfissionalByIDRow, error) {
uuidVal, err := uuid.Parse(id)
if err != nil {
return nil, errors.New("invalid id")
}
prof, err := s.queries.GetProfissionalByID(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true})
if err != nil {
return nil, err
}
return &prof, nil
}
type UpdateProfissionalInput struct {
Nome string `json:"nome"`
FuncaoProfissionalID string `json:"funcao_profissional_id"`
Endereco *string `json:"endereco"`
Cidade *string `json:"cidade"`
Uf *string `json:"uf"`
Whatsapp *string `json:"whatsapp"`
CpfCnpjTitular *string `json:"cpf_cnpj_titular"`
Banco *string `json:"banco"`
Agencia *string `json:"agencia"`
ContaPix *string `json:"conta_pix"`
CarroDisponivel *bool `json:"carro_disponivel"`
TemEstudio *bool `json:"tem_estudio"`
QtdEstudio *int `json:"qtd_estudio"`
TipoCartao *string `json:"tipo_cartao"`
Observacao *string `json:"observacao"`
QualTec *int `json:"qual_tec"`
EducacaoSimpatia *int `json:"educacao_simpatia"`
DesempenhoEvento *int `json:"desempenho_evento"`
DispHorario *int `json:"disp_horario"`
Media *float64 `json:"media"`
TabelaFree *string `json:"tabela_free"`
ExtraPorEquipamento *bool `json:"extra_por_equipamento"`
Equipamentos *string `json:"equipamentos"`
Email *string `json:"email"`
AvatarURL *string `json:"avatar_url"`
}
func (s *Service) Update(ctx context.Context, id string, input UpdateProfissionalInput) (*generated.CadastroProfissionai, error) {
uuidVal, err := uuid.Parse(id)
if err != nil {
return nil, errors.New("invalid id")
}
funcaoUUID, err := uuid.Parse(input.FuncaoProfissionalID)
if err != nil {
return nil, errors.New("invalid funcao_profissional_id")
}
params := generated.UpdateProfissionalParams{
ID: pgtype.UUID{Bytes: uuidVal, Valid: true},
Nome: input.Nome,
FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: true},
Endereco: toPgText(input.Endereco),
Cidade: toPgText(input.Cidade),
Uf: toPgText(input.Uf),
Whatsapp: toPgText(input.Whatsapp),
CpfCnpjTitular: toPgText(input.CpfCnpjTitular),
Banco: toPgText(input.Banco),
Agencia: toPgText(input.Agencia),
ContaPix: toPgText(input.ContaPix),
CarroDisponivel: toPgBool(input.CarroDisponivel),
TemEstudio: toPgBool(input.TemEstudio),
QtdEstudio: toPgInt4(input.QtdEstudio),
TipoCartao: toPgText(input.TipoCartao),
Observacao: toPgText(input.Observacao),
QualTec: toPgInt4(input.QualTec),
EducacaoSimpatia: toPgInt4(input.EducacaoSimpatia),
DesempenhoEvento: toPgInt4(input.DesempenhoEvento),
DispHorario: toPgInt4(input.DispHorario),
Media: toPgNumeric(input.Media),
TabelaFree: toPgText(input.TabelaFree),
ExtraPorEquipamento: toPgBool(input.ExtraPorEquipamento),
Equipamentos: toPgText(input.Equipamentos),
Email: toPgText(input.Email),
AvatarUrl: toPgText(input.AvatarURL),
}
prof, err := s.queries.UpdateProfissional(ctx, params)
if err != nil {
return nil, err
}
return &prof, nil
}
func (s *Service) Delete(ctx context.Context, id string) error {
fmt.Printf("[DEBUG] Deleting Professional: %s\n", id)
uuidVal, err := uuid.Parse(id)
if err != nil {
return errors.New("invalid id")
}
// Get professional to find associated user
prof, err := s.queries.GetProfissionalByID(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true})
if err != nil {
fmt.Printf("[DEBUG] Failed to get professional %s: %v\n", id, err)
return err
}
fmt.Printf("[DEBUG] Prof Found:: ID=%s, UsuarioID.Valid=%v, UsuarioID=%s\n",
uuid.UUID(prof.ID.Bytes).String(),
prof.UsuarioID.Valid,
uuid.UUID(prof.UsuarioID.Bytes).String())
// Delete associated user first (ensures login access is revoked)
if prof.UsuarioID.Valid {
fmt.Printf("[DEBUG] Attempting to delete User ID: %s\n", uuid.UUID(prof.UsuarioID.Bytes).String())
err = s.queries.DeleteUsuario(ctx, prof.UsuarioID)
if err != nil {
fmt.Printf("[DEBUG] Failed to delete User: %v\n", err)
return err
}
fmt.Println("[DEBUG] User deleted successfully.")
} else {
fmt.Println("[DEBUG] UsuarioID is invalid/null. Skipping User deletion.")
}
// Delete professional profile
fmt.Println("[DEBUG] Deleting Professional profile...")
err = s.queries.DeleteProfissional(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true})
if err != nil {
fmt.Printf("[DEBUG] Failed to delete Professional: %v\n", err)
return err
}
fmt.Println("[DEBUG] Professional deleted successfully.")
return nil
}
// Helpers
func toPgText(s *string) pgtype.Text {
if s == nil {
return pgtype.Text{Valid: false}
}
return pgtype.Text{String: *s, Valid: true}
}
func toPgBool(b *bool) pgtype.Bool {
if b == nil {
return pgtype.Bool{Valid: false}
}
return pgtype.Bool{Bool: *b, Valid: true}
}
func toPgInt4(i *int) pgtype.Int4 {
if i == nil {
return pgtype.Int4{Valid: false}
}
return pgtype.Int4{Int32: int32(*i), Valid: true}
}
func toPgNumeric(f *float64) pgtype.Numeric {
if f == nil {
return pgtype.Numeric{Valid: false}
}
var n pgtype.Numeric
if err := n.Scan(fmt.Sprintf("%f", *f)); err != nil {
return pgtype.Numeric{Valid: false}
}
return n
}