- Frontend: Adiciona campos de senha e visibilidade na tela de Equipe.
- Frontend: Implementa criação de usuário prévia ao cadastro do profissional.
- Backend (Auth): Remove criação duplicada de perfil e ativa usuários automaticamente.
- Backend (Auth): Inclui dados do profissional (avatar) na resposta do endpoint /me.
- Backend (Profissionais): Corrige chave de contexto ('role') para permitir vínculo correto de usuário.
- Backend (Profissionais): Sincroniza exclusão para remover conta de usuário ao deletar profissional.
- Docs: Atualização dos arquivos Swagger.
270 lines
9.2 KiB
Go
270 lines
9.2 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 {
|
|
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 {
|
|
return err
|
|
}
|
|
|
|
// Delete professional profile (should be done first or after?)
|
|
// If foreign key is SET NULL, it doesn't strictly matter for FK constraint,
|
|
// but logically deleting the profile first is cleaner if we want to ensure profile is gone.
|
|
// Actually, if we delete User first and it SETS NULL, we still have to delete Profile.
|
|
// So let's delete Profile first.
|
|
err = s.queries.DeleteProfissional(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete associated user if exists
|
|
if prof.UsuarioID.Valid {
|
|
err = s.queries.DeleteUsuario(ctx, prof.UsuarioID)
|
|
if err != nil {
|
|
// Create warning log? For now just return error or ignore?
|
|
// If user deletion fails, it's orphan but harmless-ish (except login).
|
|
// Better to return error.
|
|
return err
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|