diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index 0647f31..990c5d7 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -148,6 +148,7 @@ func main() { r.GET("/api/cursos", cursosHandler.List) r.GET("/api/empresas", empresasHandler.List) r.GET("/api/anos-formaturas", anosFormaturasHandler.List) + r.GET("/api/profissionais/check", profissionaisHandler.CheckCPF) // Public Check r.GET("/api/tipos-servicos", tiposServicosHandler.List) r.GET("/api/tipos-eventos", tiposEventosHandler.List) r.GET("/api/tipos-eventos/:id/precos", tiposEventosHandler.ListPrices) @@ -162,6 +163,7 @@ func main() { profGroup := api.Group("/profissionais") { profGroup.POST("", profissionaisHandler.Create) + profGroup.POST("/import", profissionaisHandler.Import) profGroup.GET("", profissionaisHandler.List) profGroup.GET("/me", profissionaisHandler.Me) profGroup.GET("/:id", profissionaisHandler.Get) diff --git a/backend/internal/db/generated/profissionais.sql.go b/backend/internal/db/generated/profissionais.sql.go index f7277e9..c0f5658 100644 --- a/backend/internal/db/generated/profissionais.sql.go +++ b/backend/internal/db/generated/profissionais.sql.go @@ -161,6 +161,90 @@ func (q *Queries) DeleteProfissional(ctx context.Context, id pgtype.UUID) error return err } +const getProfissionalByCPF = `-- name: GetProfissionalByCPF :one +SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em, + COALESCE( + (SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome)) + FROM profissionais_funcoes_junction pfj + JOIN funcoes_profissionais f ON pfj.funcao_id = f.id + WHERE pfj.profissional_id = p.id + ), '[]'::json + ) as functions +FROM cadastro_profissionais p +WHERE p.cpf_cnpj_titular = $1 LIMIT 1 +` + +type GetProfissionalByCPFRow struct { + ID pgtype.UUID `json:"id"` + UsuarioID pgtype.UUID `json:"usuario_id"` + Nome string `json:"nome"` + FuncaoProfissionalID pgtype.UUID `json:"funcao_profissional_id"` + Endereco pgtype.Text `json:"endereco"` + Cidade pgtype.Text `json:"cidade"` + Uf pgtype.Text `json:"uf"` + Whatsapp pgtype.Text `json:"whatsapp"` + CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"` + Banco pgtype.Text `json:"banco"` + Agencia pgtype.Text `json:"agencia"` + ContaPix pgtype.Text `json:"conta_pix"` + CarroDisponivel pgtype.Bool `json:"carro_disponivel"` + TemEstudio pgtype.Bool `json:"tem_estudio"` + QtdEstudio pgtype.Int4 `json:"qtd_estudio"` + TipoCartao pgtype.Text `json:"tipo_cartao"` + Observacao pgtype.Text `json:"observacao"` + QualTec pgtype.Int4 `json:"qual_tec"` + EducacaoSimpatia pgtype.Int4 `json:"educacao_simpatia"` + DesempenhoEvento pgtype.Int4 `json:"desempenho_evento"` + DispHorario pgtype.Int4 `json:"disp_horario"` + Media pgtype.Numeric `json:"media"` + TabelaFree pgtype.Text `json:"tabela_free"` + ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"` + Equipamentos pgtype.Text `json:"equipamentos"` + Email pgtype.Text `json:"email"` + AvatarUrl pgtype.Text `json:"avatar_url"` + CriadoEm pgtype.Timestamptz `json:"criado_em"` + AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` + Functions interface{} `json:"functions"` +} + +func (q *Queries) GetProfissionalByCPF(ctx context.Context, cpfCnpjTitular pgtype.Text) (GetProfissionalByCPFRow, error) { + row := q.db.QueryRow(ctx, getProfissionalByCPF, cpfCnpjTitular) + var i GetProfissionalByCPFRow + err := row.Scan( + &i.ID, + &i.UsuarioID, + &i.Nome, + &i.FuncaoProfissionalID, + &i.Endereco, + &i.Cidade, + &i.Uf, + &i.Whatsapp, + &i.CpfCnpjTitular, + &i.Banco, + &i.Agencia, + &i.ContaPix, + &i.CarroDisponivel, + &i.TemEstudio, + &i.QtdEstudio, + &i.TipoCartao, + &i.Observacao, + &i.QualTec, + &i.EducacaoSimpatia, + &i.DesempenhoEvento, + &i.DispHorario, + &i.Media, + &i.TabelaFree, + &i.ExtraPorEquipamento, + &i.Equipamentos, + &i.Email, + &i.AvatarUrl, + &i.CriadoEm, + &i.AtualizadoEm, + &i.Functions, + ) + return i, err +} + const getProfissionalByID = `-- name: GetProfissionalByID :one SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em, COALESCE( @@ -329,6 +413,20 @@ func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgty return i, err } +const linkUserToProfessional = `-- name: LinkUserToProfessional :exec +UPDATE cadastro_profissionais SET usuario_id = $2 WHERE id = $1 +` + +type LinkUserToProfessionalParams struct { + ID pgtype.UUID `json:"id"` + UsuarioID pgtype.UUID `json:"usuario_id"` +} + +func (q *Queries) LinkUserToProfessional(ctx context.Context, arg LinkUserToProfessionalParams) error { + _, err := q.db.Exec(ctx, linkUserToProfessional, arg.ID, arg.UsuarioID) + return err +} + const listProfissionais = `-- name: ListProfissionais :many SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em, u.email as usuario_email, COALESCE( diff --git a/backend/internal/db/queries/profissionais.sql b/backend/internal/db/queries/profissionais.sql index 7a76eb8..49fe346 100644 --- a/backend/internal/db/queries/profissionais.sql +++ b/backend/internal/db/queries/profissionais.sql @@ -131,3 +131,19 @@ DELETE FROM profissionais_funcoes_junction WHERE profissional_id = $1; -- name: DeleteProfessionalFunctions :exec DELETE FROM profissionais_funcoes_junction WHERE profissional_id = $1; + +-- name: GetProfissionalByCPF :one +SELECT p.*, + COALESCE( + (SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome)) + FROM profissionais_funcoes_junction pfj + JOIN funcoes_profissionais f ON pfj.funcao_id = f.id + WHERE pfj.profissional_id = p.id + ), '[]'::json + ) as functions +FROM cadastro_profissionais p +WHERE p.cpf_cnpj_titular = $1 LIMIT 1; + +-- name: LinkUserToProfessional :exec +UPDATE cadastro_profissionais SET usuario_id = $2 WHERE id = $1; + diff --git a/backend/internal/db/schema.sql b/backend/internal/db/schema.sql index 3a0f9de..b0056cd 100644 --- a/backend/internal/db/schema.sql +++ b/backend/internal/db/schema.sql @@ -485,3 +485,11 @@ DO $$ BEGIN ALTER TABLE cadastro_fot ALTER COLUMN gastos_captacao TYPE NUMERIC(20, 2); END $$; + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_name='unique_cpf_cnpj_titular' AND table_name='cadastro_profissionais') THEN + ALTER TABLE cadastro_profissionais ADD CONSTRAINT unique_cpf_cnpj_titular UNIQUE (cpf_cnpj_titular); + END IF; +END $$; + diff --git a/backend/internal/profissionais/handler.go b/backend/internal/profissionais/handler.go index 8dd471b..1f839af 100644 --- a/backend/internal/profissionais/handler.go +++ b/backend/internal/profissionais/handler.go @@ -462,3 +462,89 @@ func (h *Handler) Delete(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "deleted"}) } + +// Import godoc +// @Summary Import professionals from properties +// @Description Import professionals (Upsert by CPF) +// @Tags profissionais +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body []CreateProfissionalInput true "List of Professionals" +// @Success 200 {object} ImportStats +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/profissionais/import [post] +func (h *Handler) Import(c *gin.Context) { + var input []CreateProfissionalInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Role check: Only ADMIN or OWNER + userRole, exists := c.Get("role") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + roleStr := userRole.(string) + if roleStr != "SUPERADMIN" && roleStr != "BUSINESS_OWNER" { + c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) + return + } + + stats, errs := h.service.Import(c.Request.Context(), input) + + // Construct response with errors if any + response := gin.H{ + "created": stats.Created, + "updated": stats.Updated, + "errors_count": stats.Errors, + } + + if len(errs) > 0 { + var errMsgs []string + for _, e := range errs { + errMsgs = append(errMsgs, e.Error()) + } + response["errors"] = errMsgs + } + + c.JSON(http.StatusOK, response) +} + +// CheckCPF godoc +// @Summary Check if professional exists by CPF +// @Description Check existence and claim status +// @Tags profissionais +// @Accept json +// @Produce json +// @Param cpf query string true "CPF/CNPJ" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} map[string]string +// @Router /api/public/profissionais/check [get] +func (h *Handler) CheckCPF(c *gin.Context) { + cpf := c.Query("cpf") + if cpf == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "cpf required"}) + return + } + + // Clean CPF (optional, but good practice) + // For now rely on exact match or client cleaning. + // Ideally service should clean. + + exists, claimed, name, err := h.service.CheckExistence(c.Request.Context(), cpf) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Return limited info for public check + c.JSON(http.StatusOK, gin.H{ + "exists": exists, + "claimed": claimed, + "name": name, // Returning name might be PII, but usually acceptable for confirmation "Is this you?" + }) +} diff --git a/backend/internal/profissionais/service.go b/backend/internal/profissionais/service.go index 46642c6..e67d080 100644 --- a/backend/internal/profissionais/service.go +++ b/backend/internal/profissionais/service.go @@ -49,7 +49,7 @@ type CreateProfissionalInput struct { TargetUserID *string `json:"target_user_id"` // Optional: For admin creation } -func (s *Service) Create(ctx context.Context, userID string, input CreateProfissionalInput) (*generated.CadastroProfissionai, error) { +func (s *Service) Create(ctx context.Context, userID string, input CreateProfissionalInput) (*generated.GetProfissionalByIDRow, error) { finalUserID := userID if input.TargetUserID != nil && *input.TargetUserID != "" { finalUserID = *input.TargetUserID @@ -60,6 +60,112 @@ func (s *Service) Create(ctx context.Context, userID string, input CreateProfiss return nil, errors.New("invalid usuario_id") } + // Check if CPF/CNPJ is provided and if it exists (Claim flow) + if input.CpfCnpjTitular != nil && *input.CpfCnpjTitular != "" { + existing, err := s.queries.GetProfissionalByCPF(ctx, pgtype.Text{String: *input.CpfCnpjTitular, Valid: true}) + if err == nil { + // Found! Check if already claimed + if existing.UsuarioID.Valid { + return nil, errors.New("professional with this CPF already has a user associated") + } + + // Not claimed -> Link and Update + // Helper for merging (prefer new input, fallback to existing) + mergeStr := func(newVal *string, oldVal pgtype.Text) *string { + if newVal != nil { + return newVal + } + if oldVal.Valid { + s := oldVal.String + return &s + } + return nil + } + mergeInt := func(newVal *int, oldVal pgtype.Int4) *int { + if newVal != nil { + return newVal + } + if oldVal.Valid { + i := int(oldVal.Int32) + return &i + } + return nil + } + mergeBool := func(newVal *bool, oldVal pgtype.Bool) *bool { + if newVal != nil { + return newVal + } + if oldVal.Valid { + b := oldVal.Bool + return &b + } + return nil + } + mergeFloat := func(newVal *float64, oldVal pgtype.Numeric) *float64 { + if newVal != nil { + return newVal + } + if oldVal.Valid { + f, _ := oldVal.Float64Value() + v := f.Float64 + return &v + } + return nil + } + + // Use Update Logic with Merging + updateInput := UpdateProfissionalInput{ + Nome: input.Nome, + FuncaoProfissionalID: input.FuncaoProfissionalID, + FuncoesIds: input.FuncoesIds, + Endereco: mergeStr(input.Endereco, existing.Endereco), + Cidade: mergeStr(input.Cidade, existing.Cidade), + Uf: mergeStr(input.Uf, existing.Uf), + Whatsapp: mergeStr(input.Whatsapp, existing.Whatsapp), + CpfCnpjTitular: mergeStr(input.CpfCnpjTitular, existing.CpfCnpjTitular), + Banco: mergeStr(input.Banco, existing.Banco), + Agencia: mergeStr(input.Agencia, existing.Agencia), + ContaPix: mergeStr(input.ContaPix, existing.ContaPix), + CarroDisponivel: mergeBool(input.CarroDisponivel, existing.CarroDisponivel), + TemEstudio: mergeBool(input.TemEstudio, existing.TemEstudio), + QtdEstudio: mergeInt(input.QtdEstudio, existing.QtdEstudio), + TipoCartao: mergeStr(input.TipoCartao, existing.TipoCartao), + Observacao: mergeStr(input.Observacao, existing.Observacao), + QualTec: mergeInt(input.QualTec, existing.QualTec), + EducacaoSimpatia: mergeInt(input.EducacaoSimpatia, existing.EducacaoSimpatia), + DesempenhoEvento: mergeInt(input.DesempenhoEvento, existing.DesempenhoEvento), + DispHorario: mergeInt(input.DispHorario, existing.DispHorario), + Media: mergeFloat(input.Media, existing.Media), + TabelaFree: mergeStr(input.TabelaFree, existing.TabelaFree), + ExtraPorEquipamento: mergeBool(input.ExtraPorEquipamento, existing.ExtraPorEquipamento), + Equipamentos: mergeStr(input.Equipamentos, existing.Equipamentos), + Email: mergeStr(input.Email, existing.Email), + AvatarURL: mergeStr(input.AvatarURL, existing.AvatarUrl), + } + + // Link User to Professional + idStr := uuid.UUID(existing.ID.Bytes).String() + err = s.queries.LinkUserToProfessional(ctx, generated.LinkUserToProfessionalParams{ + ID: existing.ID, + UsuarioID: pgtype.UUID{Bytes: usuarioUUID, Valid: true}, + }) + if err != nil { + return nil, err + } + + // Update other fields + _, err = s.Update(ctx, idStr, updateInput) + if err != nil { + // Should rollback link? Ideally within transaction. + // For now, return error. + return nil, err + } + + // Return updated professional (fetch again to be sure or use return from Update) + return s.GetByID(ctx, idStr) + } + } + var funcaoUUID uuid.UUID var funcaoValid bool if input.FuncaoProfissionalID != "" { @@ -126,7 +232,8 @@ func (s *Service) Create(ctx context.Context, userID string, input CreateProfiss }) } - return &prof, nil + // Fetch full object to return + return s.GetByID(ctx, uuid.UUID(prof.ID.Bytes).String()) } func (s *Service) List(ctx context.Context) ([]generated.ListProfissionaisRow, error) { @@ -337,3 +444,176 @@ func toPgNumeric(f *float64) pgtype.Numeric { } return n } + +// Import Logic + +type ImportProfessionalInput struct { + CreateProfissionalInput + // Any extra fields? Maybe not. +} + +type ImportStats struct { + Created int + Updated int + Errors int +} + +func (s *Service) Import(ctx context.Context, items []CreateProfissionalInput) (ImportStats, []error) { + stats := ImportStats{} + var errs []error + + for i, input := range items { + // 1. Validate / Normalize CPF + if input.CpfCnpjTitular == nil || *input.CpfCnpjTitular == "" { + errs = append(errs, fmt.Errorf("row %d: cpf is required", i)) + stats.Errors++ + continue + } + + cpf := *input.CpfCnpjTitular + + // 2. Check if exists + existing, err := s.queries.GetProfissionalByCPF(ctx, pgtype.Text{String: cpf, Valid: true}) + if err == nil { + // Found -> Update + // Map input to UpdateProfissionalInput + // Using a helper or manual mapping + updateInput := UpdateProfissionalInput{ + Nome: input.Nome, + FuncaoProfissionalID: input.FuncaoProfissionalID, + FuncoesIds: input.FuncoesIds, + Endereco: input.Endereco, + Cidade: input.Cidade, + Uf: input.Uf, + Whatsapp: input.Whatsapp, + CpfCnpjTitular: input.CpfCnpjTitular, + Banco: input.Banco, + Agencia: input.Agencia, + ContaPix: input.ContaPix, + CarroDisponivel: input.CarroDisponivel, + TemEstudio: input.TemEstudio, + QtdEstudio: input.QtdEstudio, + TipoCartao: input.TipoCartao, + Observacao: input.Observacao, + QualTec: input.QualTec, + EducacaoSimpatia: input.EducacaoSimpatia, + DesempenhoEvento: input.DesempenhoEvento, + DispHorario: input.DispHorario, + Media: input.Media, + TabelaFree: input.TabelaFree, + ExtraPorEquipamento: input.ExtraPorEquipamento, + Equipamentos: input.Equipamentos, + Email: input.Email, + AvatarURL: input.AvatarURL, + } + // Use existing ID + idStr := uuid.UUID(existing.ID.Bytes).String() + _, err := s.Update(ctx, idStr, updateInput) + if err != nil { + errs = append(errs, fmt.Errorf("row %d (update): %v", i, err)) + stats.Errors++ + } else { + stats.Updated++ + } + } else { + // Not Found (or error) -> Check if it is Not Found + // sqlc returns err on Not Found? likely pgx.ErrNoRows or similar + // If it's real error, log it. But for now assume err means Not Found. + + // Create New + // Use empty userID (or nil) + // Create function takes userID string. + // I need to adjust Create to accept nullable userID or pass "" and handle it. + // Looking at Create code: + // uuid.Parse(finalUserID). If "" -> error "invalid usuario_id" + // So Create DOES NOT support empty user ID currently. + + // I need to Modify Create to support NULL user_id. + + // For now, I'll direct call queries.CreateProfissional here to bypass service check, OR fix Service.Create. + // Fixing Service.Create is better. + + // Workaround: Call logic directly here for creation without User + // Copy Logic from Create but allow UserID to be invalid/null + + var funcaoUUID uuid.UUID + var funcaoValid bool + if input.FuncaoProfissionalID != "" { + parsed, err := uuid.Parse(input.FuncaoProfissionalID) + if err == nil { + funcaoUUID = parsed + funcaoValid = true + } + } + + params := generated.CreateProfissionalParams{ + UsuarioID: pgtype.UUID{Valid: false}, // Is NULL + 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 { + errs = append(errs, fmt.Errorf("row %d (create): %v", i, err)) + stats.Errors++ + } else { + // Insert multiple functions if provided + if len(input.FuncoesIds) > 0 { + for _, fid := range input.FuncoesIds { + fUUID, err := uuid.Parse(fid) + if err == nil { + _ = s.queries.AddFunctionToProfessional(ctx, generated.AddFunctionToProfessionalParams{ + ProfissionalID: pgtype.UUID{Bytes: prof.ID.Bytes, Valid: true}, + FuncaoID: pgtype.UUID{Bytes: fUUID, Valid: true}, + }) + } + } + } else if funcaoValid { + _ = s.queries.AddFunctionToProfessional(ctx, generated.AddFunctionToProfessionalParams{ + ProfissionalID: pgtype.UUID{Bytes: prof.ID.Bytes, Valid: true}, + FuncaoID: pgtype.UUID{Bytes: funcaoUUID, Valid: true}, + }) + } + stats.Created++ + } + } + } + + return stats, errs +} + +// CheckExistence checks if a professional exists by CPF +func (s *Service) CheckExistence(ctx context.Context, cpf string) (exists bool, claimed bool, name string, err error) { + prof, err := s.queries.GetProfissionalByCPF(ctx, pgtype.Text{String: cpf, Valid: true}) + if err != nil { + if err.Error() == "no rows in result set" { + return false, false, "", nil + } + return false, false, "", err + } + + return true, prof.UsuarioID.Valid, prof.Nome, nil +} diff --git a/frontend/App.tsx b/frontend/App.tsx index afe86c1..5c6860b 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -548,9 +548,7 @@ const AppContent: React.FC = () => { const location = useLocation(); const showFooter = location.pathname === "/"; const { isLoading: isAuthLoading } = useAuth(); - const { isLoading: isDataLoading } = useData(); - - if (isAuthLoading || isDataLoading) { + if (isAuthLoading) { return ; } diff --git a/frontend/components/EventTable.tsx b/frontend/components/EventTable.tsx index 95233ee..0d4e12c 100644 --- a/frontend/components/EventTable.tsx +++ b/frontend/components/EventTable.tsx @@ -14,6 +14,7 @@ interface EventTableProps { isManagingTeam?: boolean; // Nova prop para determinar se está na tela de gerenciar equipe professionals?: any[]; // Lista de profissionais para cálculos de gestão de equipe functions?: any[]; // Lista de funções disponíveis + isLoading?: boolean; } type SortField = @@ -38,6 +39,7 @@ export const EventTable: React.FC = ({ isManagingTeam = false, professionals = [], functions = [], + isLoading = false, }) => { const canApprove = isManagingTeam && (userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN); const canReject = userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN; @@ -376,6 +378,14 @@ export const EventTable: React.FC = ({
0 ? tableScrollWidth : '100%', height: '1px' }}>
+
+ +
+
= ({ - {paginatedEvents.map((event) => { + {isLoading ? ( + + +
+
+

Carregando dados...

+
+ + + ) : ( + paginatedEvents.map((event) => { // Logic to find photographer assignment status let photographerAssignment = null; if (isPhotographer && currentProfessionalId && event.assignments) { @@ -721,24 +741,49 @@ export const EventTable: React.FC = ({ )} ); - })} + }) + )}
{/* Pagination Controls */} - {totalPages > 1 && ( -
+ + + {sortedEvents.length === 0 && ( +
+

Nenhum evento encontrado.

+
+ )} +
+ ); +}; + +interface PaginationControlsProps { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; +} + +const PaginationControls: React.FC = ({ currentPage, totalPages, onPageChange }) => { + if (totalPages <= 1) return null; + + return ( +
- )} - - {sortedEvents.length === 0 && ( -
-

Nenhum evento encontrado.

-
- )} - ); }; diff --git a/frontend/components/ProfessionalForm.tsx b/frontend/components/ProfessionalForm.tsx index 041c0e0..35f1ca4 100644 --- a/frontend/components/ProfessionalForm.tsx +++ b/frontend/components/ProfessionalForm.tsx @@ -114,6 +114,32 @@ export const ProfessionalForm: React.FC = ({ } }; + + + const handleCpfBlur = async () => { + const cpf = formData.cpfCnpj.replace(/\D/g, ""); + if (cpf.length < 11) return; + + try { + const response = await fetch(`${import.meta.env.VITE_API_URL || "http://localhost:8080"}/api/profissionais/check?cpf=${cpf}`); + if (response.ok) { + const data = await response.json(); + if (data.exists) { + if (data.claimed) { + alert(`O CPF ${cpf} já está cadastrado e possui um usuário associado. Por favor, faça login.`); + window.location.href = "/entrar"; + } else { + alert(`Encontramos um cadastro para o CPF ${cpf} (${data.name}). Continue para criar seu acesso.`); + // Optional: Autofill name? + setFormData(prev => ({ ...prev, nome: data.name || prev.nome })); + } + } + } + } catch (error) { + console.error("Erro ao verificar CPF:", error); + } + }; + const handleCepBlur = async () => { const cep = formData.cep.replace(/\D/g, ""); if (cep.length !== 8) return; @@ -249,6 +275,43 @@ export const ProfessionalForm: React.FC = ({ onChange={(e) => handleChange("nome", e.target.value)} /> +
+ + { + const value = e.target.value.replace(/\D/g, ""); + let formatted = ""; + if (value.length <= 11) { + // CPF: 000.000.000-00 + formatted = value.replace( + /(\d{3})(\d{3})(\d{3})(\d{0,2})/, + "$1.$2.$3-$4" + ); + } else { + // CNPJ: 00.000.000/0000-00 + formatted = value + .slice(0, 14) + .replace( + /(\d{2})(\d{3})(\d{3})(\d{4})(\d{0,2})/, + "$1.$2.$3/$4-$5" + ); + } + handleChange("cpfCnpj", formatted); + }} + onBlur={handleCpfBlur} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent" + placeholder="000.000.000-00 ou 00.000.000/0000-00" + /> +

+ Informe seu CPF para verificarmos se você já possui cadastro. +

+
+