Merge pull request #33 from rede5/Front-back-integracao-task11
feat(staffing): implementa sistema de disponibilidade e escalonamento, (frontend): implementa modal de detalhes e updates otimistas na equipe
This commit is contained in:
commit
95d7906c28
17 changed files with 1094 additions and 74 deletions
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"photum-backend/internal/agenda"
|
"photum-backend/internal/agenda"
|
||||||
"photum-backend/internal/anos_formaturas"
|
"photum-backend/internal/anos_formaturas"
|
||||||
"photum-backend/internal/auth"
|
"photum-backend/internal/auth"
|
||||||
|
"photum-backend/internal/availability"
|
||||||
"photum-backend/internal/cadastro_fot"
|
"photum-backend/internal/cadastro_fot"
|
||||||
"photum-backend/internal/config"
|
"photum-backend/internal/config"
|
||||||
"photum-backend/internal/cursos"
|
"photum-backend/internal/cursos"
|
||||||
|
|
@ -71,6 +72,7 @@ func main() {
|
||||||
tiposEventosService := tipos_eventos.NewService(queries)
|
tiposEventosService := tipos_eventos.NewService(queries)
|
||||||
cadastroFotService := cadastro_fot.NewService(queries)
|
cadastroFotService := cadastro_fot.NewService(queries)
|
||||||
agendaService := agenda.NewService(queries)
|
agendaService := agenda.NewService(queries)
|
||||||
|
availabilityService := availability.NewService(queries)
|
||||||
s3Service := storage.NewS3Service(cfg)
|
s3Service := storage.NewS3Service(cfg)
|
||||||
|
|
||||||
// Seed Demo Users
|
// Seed Demo Users
|
||||||
|
|
@ -89,6 +91,7 @@ func main() {
|
||||||
tiposEventosHandler := tipos_eventos.NewHandler(tiposEventosService)
|
tiposEventosHandler := tipos_eventos.NewHandler(tiposEventosService)
|
||||||
cadastroFotHandler := cadastro_fot.NewHandler(cadastroFotService)
|
cadastroFotHandler := cadastro_fot.NewHandler(cadastroFotService)
|
||||||
agendaHandler := agenda.NewHandler(agendaService)
|
agendaHandler := agenda.NewHandler(agendaService)
|
||||||
|
availabilityHandler := availability.NewHandler(availabilityService)
|
||||||
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
|
|
@ -197,8 +200,13 @@ func main() {
|
||||||
api.DELETE("/agenda/:id/professionals/:profId", agendaHandler.RemoveProfessional)
|
api.DELETE("/agenda/:id/professionals/:profId", agendaHandler.RemoveProfessional)
|
||||||
api.GET("/agenda/:id/professionals", agendaHandler.GetProfessionals)
|
api.GET("/agenda/:id/professionals", agendaHandler.GetProfessionals)
|
||||||
api.PATCH("/agenda/:id/professionals/:profId/status", agendaHandler.UpdateAssignmentStatus)
|
api.PATCH("/agenda/:id/professionals/:profId/status", agendaHandler.UpdateAssignmentStatus)
|
||||||
|
api.PATCH("/agenda/:id/professionals/:profId/position", agendaHandler.UpdateAssignmentPosition)
|
||||||
|
api.GET("/agenda/:id/available", agendaHandler.ListAvailableProfessionals)
|
||||||
api.PATCH("/agenda/:id/status", agendaHandler.UpdateStatus)
|
api.PATCH("/agenda/:id/status", agendaHandler.UpdateStatus)
|
||||||
|
|
||||||
|
api.POST("/availability", availabilityHandler.SetAvailability)
|
||||||
|
api.GET("/availability", availabilityHandler.ListAvailability)
|
||||||
|
|
||||||
admin := api.Group("/admin")
|
admin := api.Group("/admin")
|
||||||
{
|
{
|
||||||
admin.GET("/users", authHandler.ListUsers)
|
admin.GET("/users", authHandler.ListUsers)
|
||||||
|
|
|
||||||
|
|
@ -651,6 +651,15 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/agenda/{id}/available": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"agenda"
|
||||||
|
],
|
||||||
|
"summary": "List available professionals for agenda date",
|
||||||
|
"responses": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/agenda/{id}/professionals": {
|
"/api/agenda/{id}/professionals": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -676,6 +685,15 @@ const docTemplate = `{
|
||||||
"responses": {}
|
"responses": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/agenda/{id}/professionals/{profId}/position": {
|
||||||
|
"patch": {
|
||||||
|
"tags": [
|
||||||
|
"agenda"
|
||||||
|
],
|
||||||
|
"summary": "Update professional position in agenda",
|
||||||
|
"responses": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/agenda/{id}/professionals/{profId}/status": {
|
"/api/agenda/{id}/professionals/{profId}/status": {
|
||||||
"patch": {
|
"patch": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -2490,6 +2508,79 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/availability": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"availability"
|
||||||
|
],
|
||||||
|
"summary": "List my availability for a month with ?start=YYYY-MM-DD\u0026end=YYYY-MM-DD",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Start Date",
|
||||||
|
"name": "start",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "End Date",
|
||||||
|
"name": "end",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"availability"
|
||||||
|
],
|
||||||
|
"summary": "Set availability for a date",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Availability Input",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/availability.SetAvailabilityInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
|
@ -2712,6 +2803,19 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"availability.SetAvailabilityInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"date": {
|
||||||
|
"description": "YYYY-MM-DD",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"description": "DISPONIVEL, INDISPONIVEL",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"cadastro_fot.CadastroFotResponse": {
|
"cadastro_fot.CadastroFotResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -645,6 +645,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/agenda/{id}/available": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"agenda"
|
||||||
|
],
|
||||||
|
"summary": "List available professionals for agenda date",
|
||||||
|
"responses": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/agenda/{id}/professionals": {
|
"/api/agenda/{id}/professionals": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -670,6 +679,15 @@
|
||||||
"responses": {}
|
"responses": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/agenda/{id}/professionals/{profId}/position": {
|
||||||
|
"patch": {
|
||||||
|
"tags": [
|
||||||
|
"agenda"
|
||||||
|
],
|
||||||
|
"summary": "Update professional position in agenda",
|
||||||
|
"responses": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/agenda/{id}/professionals/{profId}/status": {
|
"/api/agenda/{id}/professionals/{profId}/status": {
|
||||||
"patch": {
|
"patch": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -2484,6 +2502,79 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/availability": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"availability"
|
||||||
|
],
|
||||||
|
"summary": "List my availability for a month with ?start=YYYY-MM-DD\u0026end=YYYY-MM-DD",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Start Date",
|
||||||
|
"name": "start",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "End Date",
|
||||||
|
"name": "end",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"availability"
|
||||||
|
],
|
||||||
|
"summary": "Set availability for a date",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Availability Input",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/availability.SetAvailabilityInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
|
@ -2706,6 +2797,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"availability.SetAvailabilityInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"date": {
|
||||||
|
"description": "YYYY-MM-DD",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"description": "DISPONIVEL, INDISPONIVEL",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"cadastro_fot.CadastroFotResponse": {
|
"cadastro_fot.CadastroFotResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,15 @@ definitions:
|
||||||
role:
|
role:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
availability.SetAvailabilityInput:
|
||||||
|
properties:
|
||||||
|
date:
|
||||||
|
description: YYYY-MM-DD
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: DISPONIVEL, INDISPONIVEL
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
cadastro_fot.CadastroFotResponse:
|
cadastro_fot.CadastroFotResponse:
|
||||||
properties:
|
properties:
|
||||||
ano_formatura_id:
|
ano_formatura_id:
|
||||||
|
|
@ -879,6 +888,12 @@ paths:
|
||||||
summary: Update agenda event
|
summary: Update agenda event
|
||||||
tags:
|
tags:
|
||||||
- agenda
|
- agenda
|
||||||
|
/api/agenda/{id}/available:
|
||||||
|
get:
|
||||||
|
responses: {}
|
||||||
|
summary: List available professionals for agenda date
|
||||||
|
tags:
|
||||||
|
- agenda
|
||||||
/api/agenda/{id}/professionals:
|
/api/agenda/{id}/professionals:
|
||||||
get:
|
get:
|
||||||
responses: {}
|
responses: {}
|
||||||
|
|
@ -896,6 +911,12 @@ paths:
|
||||||
summary: Remove professional from agenda
|
summary: Remove professional from agenda
|
||||||
tags:
|
tags:
|
||||||
- agenda
|
- agenda
|
||||||
|
/api/agenda/{id}/professionals/{profId}/position:
|
||||||
|
patch:
|
||||||
|
responses: {}
|
||||||
|
summary: Update professional position in agenda
|
||||||
|
tags:
|
||||||
|
- agenda
|
||||||
/api/agenda/{id}/professionals/{profId}/status:
|
/api/agenda/{id}/professionals/{profId}/status:
|
||||||
patch:
|
patch:
|
||||||
responses: {}
|
responses: {}
|
||||||
|
|
@ -2038,6 +2059,54 @@ paths:
|
||||||
summary: Get S3 Presigned URL for upload
|
summary: Get S3 Presigned URL for upload
|
||||||
tags:
|
tags:
|
||||||
- auth
|
- auth
|
||||||
|
/availability:
|
||||||
|
get:
|
||||||
|
parameters:
|
||||||
|
- description: Start Date
|
||||||
|
in: query
|
||||||
|
name: start
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: End Date
|
||||||
|
in: query
|
||||||
|
name: end
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
additionalProperties: true
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
summary: List my availability for a month with ?start=YYYY-MM-DD&end=YYYY-MM-DD
|
||||||
|
tags:
|
||||||
|
- availability
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: Availability Input
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/availability.SetAvailabilityInput'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Set availability for a date
|
||||||
|
tags:
|
||||||
|
- availability
|
||||||
securityDefinitions:
|
securityDefinitions:
|
||||||
BearerAuth:
|
BearerAuth:
|
||||||
in: header
|
in: header
|
||||||
|
|
|
||||||
|
|
@ -308,3 +308,72 @@ func (h *Handler) UpdateAssignmentStatus(c *gin.Context) {
|
||||||
|
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAssignmentPosition godoc
|
||||||
|
// @Summary Update professional position in agenda
|
||||||
|
// @Tags agenda
|
||||||
|
// @Router /api/agenda/{id}/professionals/{profId}/position [patch]
|
||||||
|
func (h *Handler) UpdateAssignmentPosition(c *gin.Context) {
|
||||||
|
idParam := c.Param("id")
|
||||||
|
agendaID, err := uuid.Parse(idParam)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de agenda inválido"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profIdParam := c.Param("profId")
|
||||||
|
profID, err := uuid.Parse(profIdParam)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de profissional inválido"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Posicao string `json:"posicao" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Dados inválidos: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdateAssignmentPosition(c.Request.Context(), agendaID, profID, req.Posicao); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erro ao atualizar posição: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAvailableProfessionals godoc
|
||||||
|
// @Summary List available professionals for agenda date
|
||||||
|
// @Tags agenda
|
||||||
|
// @Router /api/agenda/{id}/available [get]
|
||||||
|
func (h *Handler) ListAvailableProfessionals(c *gin.Context) {
|
||||||
|
idParam := c.Param("id")
|
||||||
|
agendaID, err := uuid.Parse(idParam)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de agenda inválido"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch agenda to get date
|
||||||
|
agenda, err := h.service.Get(c.Request.Context(), agendaID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erro ao buscar agenda: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if date is valid
|
||||||
|
if !agenda.DataEvento.Valid {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Agenda sem data definida"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profs, err := h.service.ListAvailableProfessionals(c.Request.Context(), agenda.DataEvento.Time)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erro ao buscar profissionais disponíveis: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, profs)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -264,3 +264,17 @@ func (s *Service) UpdateAssignmentStatus(ctx context.Context, agendaID, professi
|
||||||
_, err := s.queries.UpdateAssignmentStatus(ctx, params)
|
_, err := s.queries.UpdateAssignmentStatus(ctx, params)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateAssignmentPosition(ctx context.Context, agendaID, professionalID uuid.UUID, posicao string) error {
|
||||||
|
params := generated.UpdateAssignmentPositionParams{
|
||||||
|
AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true},
|
||||||
|
ProfissionalID: pgtype.UUID{Bytes: professionalID, Valid: true},
|
||||||
|
Posicao: pgtype.Text{String: posicao, Valid: true},
|
||||||
|
}
|
||||||
|
_, err := s.queries.UpdateAssignmentPosition(ctx, params)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListAvailableProfessionals(ctx context.Context, date time.Time) ([]generated.ListAvailableProfessionalsForDateRow, error) {
|
||||||
|
return s.queries.ListAvailableProfessionalsForDate(ctx, pgtype.Date{Time: date, Valid: true})
|
||||||
|
}
|
||||||
|
|
|
||||||
83
backend/internal/availability/handler.go
Normal file
83
backend/internal/availability/handler.go
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
package availability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
service *Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(service *Service) *Handler {
|
||||||
|
return &Handler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAvailability godoc
|
||||||
|
// @Summary Set availability for a date
|
||||||
|
// @Tags availability
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body SetAvailabilityInput true "Availability Input"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /availability [post]
|
||||||
|
func (h *Handler) SetAvailability(c *gin.Context) {
|
||||||
|
var req SetAvailabilityInput
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := c.GetString("userID") // From middleware
|
||||||
|
if userID == "" {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.SetAvailability(c.Request.Context(), userID, req); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "availability updated"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAvailability godoc
|
||||||
|
// @Summary List my availability for a month with ?start=YYYY-MM-DD&end=YYYY-MM-DD
|
||||||
|
// @Tags availability
|
||||||
|
// @Produce json
|
||||||
|
// @Param start query string true "Start Date"
|
||||||
|
// @Param end query string true "End Date"
|
||||||
|
// @Success 200 {array} map[string]interface{}
|
||||||
|
// @Router /availability [get]
|
||||||
|
func (h *Handler) ListAvailability(c *gin.Context) {
|
||||||
|
userId := c.GetString("userID")
|
||||||
|
start := c.Query("start")
|
||||||
|
end := c.Query("end")
|
||||||
|
|
||||||
|
if start == "" || end == "" {
|
||||||
|
// Default to current month
|
||||||
|
now := time.Now()
|
||||||
|
start = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC).Format("2006-01-02")
|
||||||
|
end = time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, time.UTC).Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
availabilities, err := h.service.ListMyAvailability(c.Request.Context(), userId, start, end)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map to simplify response
|
||||||
|
var response []map[string]interface{}
|
||||||
|
for _, a := range availabilities {
|
||||||
|
response = append(response, map[string]interface{}{
|
||||||
|
"date": a.Data.Time.Format("2006-01-02"),
|
||||||
|
"status": a.Status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
75
backend/internal/availability/service.go
Normal file
75
backend/internal/availability/service.go
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
package availability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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 SetAvailabilityInput struct {
|
||||||
|
Date string `json:"date"` // YYYY-MM-DD
|
||||||
|
Status string `json:"status"` // DISPONIVEL, INDISPONIVEL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetAvailability(ctx context.Context, userID string, input SetAvailabilityInput) error {
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
date, err := time.Parse("2006-01-02", input.Date)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pgDate := pgtype.Date{Time: date, Valid: true}
|
||||||
|
|
||||||
|
if input.Status == "INDISPONIVEL" {
|
||||||
|
// Remove record if unavailable (or explicit status if we prefer history, but opt-in usually means record exists = available)
|
||||||
|
// Specifically for this implementation, let's follow the schema which has a status column.
|
||||||
|
// However, "availability" usually implies "I am available".
|
||||||
|
// If I set "INDISPONIVEL", I can either delete the record or set status to INDISPONIVEL.
|
||||||
|
// Let's set status.
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.queries.CreateDisponibilidade(ctx, generated.CreateDisponibilidadeParams{
|
||||||
|
UsuarioID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
Data: pgDate,
|
||||||
|
Status: input.Status,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListMyAvailability(ctx context.Context, userID string, startDate, endDate string) ([]generated.DisponibilidadeProfissionai, error) {
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
start, err := time.Parse("2006-01-02", startDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
end, err := time.Parse("2006-01-02", endDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.queries.ListDisponibilidadeByPeriod(ctx, generated.ListDisponibilidadeByPeriodParams{
|
||||||
|
UsuarioID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
Data: pgtype.Date{Time: start, Valid: true},
|
||||||
|
Data_2: pgtype.Date{Time: end, Valid: true},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -541,6 +541,114 @@ func (q *Queries) ListAgendasByUser(ctx context.Context, userID pgtype.UUID) ([]
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const listAvailableProfessionalsForDate = `-- name: ListAvailableProfessionalsForDate :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.avatar_url, p.criado_em, p.atualizado_em,
|
||||||
|
u.email,
|
||||||
|
f.nome as funcao_nome,
|
||||||
|
dp.status as status_disponibilidade
|
||||||
|
FROM cadastro_profissionais p
|
||||||
|
JOIN usuarios u ON p.usuario_id = u.id
|
||||||
|
JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
|
||||||
|
JOIN disponibilidade_profissionais dp ON u.id = dp.usuario_id
|
||||||
|
WHERE dp.data = $1
|
||||||
|
AND dp.status = 'DISPONIVEL'
|
||||||
|
AND p.id NOT IN (
|
||||||
|
SELECT ap.profissional_id
|
||||||
|
FROM agenda_profissionais ap
|
||||||
|
JOIN agenda a ON ap.agenda_id = a.id
|
||||||
|
WHERE a.data_evento = $1
|
||||||
|
AND ap.status = 'ACEITO'
|
||||||
|
)
|
||||||
|
ORDER BY p.nome
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListAvailableProfessionalsForDateRow 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"`
|
||||||
|
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||||
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
|
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
FuncaoNome string `json:"funcao_nome"`
|
||||||
|
StatusDisponibilidade string `json:"status_disponibilidade"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListAvailableProfessionalsForDate(ctx context.Context, data pgtype.Date) ([]ListAvailableProfessionalsForDateRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listAvailableProfessionalsForDate, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListAvailableProfessionalsForDateRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListAvailableProfessionalsForDateRow
|
||||||
|
if err := rows.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.AvatarUrl,
|
||||||
|
&i.CriadoEm,
|
||||||
|
&i.AtualizadoEm,
|
||||||
|
&i.Email,
|
||||||
|
&i.FuncaoNome,
|
||||||
|
&i.StatusDisponibilidade,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const removeProfessional = `-- name: RemoveProfessional :exec
|
const removeProfessional = `-- name: RemoveProfessional :exec
|
||||||
DELETE FROM agenda_profissionais
|
DELETE FROM agenda_profissionais
|
||||||
WHERE agenda_id = $1 AND profissional_id = $2
|
WHERE agenda_id = $1 AND profissional_id = $2
|
||||||
|
|
@ -723,11 +831,39 @@ func (q *Queries) UpdateAgendaStatus(ctx context.Context, arg UpdateAgendaStatus
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateAssignmentPosition = `-- name: UpdateAssignmentPosition :one
|
||||||
|
UPDATE agenda_profissionais
|
||||||
|
SET posicao = $3
|
||||||
|
WHERE agenda_id = $1 AND profissional_id = $2
|
||||||
|
RETURNING id, agenda_id, profissional_id, status, motivo_rejeicao, criado_em, posicao
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateAssignmentPositionParams struct {
|
||||||
|
AgendaID pgtype.UUID `json:"agenda_id"`
|
||||||
|
ProfissionalID pgtype.UUID `json:"profissional_id"`
|
||||||
|
Posicao pgtype.Text `json:"posicao"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateAssignmentPosition(ctx context.Context, arg UpdateAssignmentPositionParams) (AgendaProfissionai, error) {
|
||||||
|
row := q.db.QueryRow(ctx, updateAssignmentPosition, arg.AgendaID, arg.ProfissionalID, arg.Posicao)
|
||||||
|
var i AgendaProfissionai
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.AgendaID,
|
||||||
|
&i.ProfissionalID,
|
||||||
|
&i.Status,
|
||||||
|
&i.MotivoRejeicao,
|
||||||
|
&i.CriadoEm,
|
||||||
|
&i.Posicao,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const updateAssignmentStatus = `-- name: UpdateAssignmentStatus :one
|
const updateAssignmentStatus = `-- name: UpdateAssignmentStatus :one
|
||||||
UPDATE agenda_profissionais
|
UPDATE agenda_profissionais
|
||||||
SET status = $3, motivo_rejeicao = $4
|
SET status = $3, motivo_rejeicao = $4
|
||||||
WHERE agenda_id = $1 AND profissional_id = $2
|
WHERE agenda_id = $1 AND profissional_id = $2
|
||||||
RETURNING id, agenda_id, profissional_id, status, motivo_rejeicao, criado_em
|
RETURNING id, agenda_id, profissional_id, status, motivo_rejeicao, criado_em, posicao
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateAssignmentStatusParams struct {
|
type UpdateAssignmentStatusParams struct {
|
||||||
|
|
@ -752,6 +888,7 @@ func (q *Queries) UpdateAssignmentStatus(ctx context.Context, arg UpdateAssignme
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.MotivoRejeicao,
|
&i.MotivoRejeicao,
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
|
&i.Posicao,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
117
backend/internal/db/generated/availability.sql.go
Normal file
117
backend/internal/db/generated/availability.sql.go
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: availability.sql
|
||||||
|
|
||||||
|
package generated
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createDisponibilidade = `-- name: CreateDisponibilidade :one
|
||||||
|
INSERT INTO disponibilidade_profissionais (usuario_id, data, status)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (usuario_id, data) DO UPDATE
|
||||||
|
SET status = EXCLUDED.status, criado_em = NOW()
|
||||||
|
RETURNING id, usuario_id, data, status, criado_em
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateDisponibilidadeParams struct {
|
||||||
|
UsuarioID pgtype.UUID `json:"usuario_id"`
|
||||||
|
Data pgtype.Date `json:"data"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateDisponibilidade(ctx context.Context, arg CreateDisponibilidadeParams) (DisponibilidadeProfissionai, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createDisponibilidade, arg.UsuarioID, arg.Data, arg.Status)
|
||||||
|
var i DisponibilidadeProfissionai
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UsuarioID,
|
||||||
|
&i.Data,
|
||||||
|
&i.Status,
|
||||||
|
&i.CriadoEm,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteDisponibilidade = `-- name: DeleteDisponibilidade :exec
|
||||||
|
DELETE FROM disponibilidade_profissionais
|
||||||
|
WHERE usuario_id = $1 AND data = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type DeleteDisponibilidadeParams struct {
|
||||||
|
UsuarioID pgtype.UUID `json:"usuario_id"`
|
||||||
|
Data pgtype.Date `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) DeleteDisponibilidade(ctx context.Context, arg DeleteDisponibilidadeParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteDisponibilidade, arg.UsuarioID, arg.Data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDisponibilidadeByDate = `-- name: GetDisponibilidadeByDate :one
|
||||||
|
SELECT id, usuario_id, data, status, criado_em FROM disponibilidade_profissionais
|
||||||
|
WHERE usuario_id = $1 AND data = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetDisponibilidadeByDateParams struct {
|
||||||
|
UsuarioID pgtype.UUID `json:"usuario_id"`
|
||||||
|
Data pgtype.Date `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetDisponibilidadeByDate(ctx context.Context, arg GetDisponibilidadeByDateParams) (DisponibilidadeProfissionai, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getDisponibilidadeByDate, arg.UsuarioID, arg.Data)
|
||||||
|
var i DisponibilidadeProfissionai
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UsuarioID,
|
||||||
|
&i.Data,
|
||||||
|
&i.Status,
|
||||||
|
&i.CriadoEm,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listDisponibilidadeByPeriod = `-- name: ListDisponibilidadeByPeriod :many
|
||||||
|
SELECT id, usuario_id, data, status, criado_em FROM disponibilidade_profissionais
|
||||||
|
WHERE usuario_id = $1
|
||||||
|
AND data >= $2
|
||||||
|
AND data <= $3
|
||||||
|
ORDER BY data
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListDisponibilidadeByPeriodParams struct {
|
||||||
|
UsuarioID pgtype.UUID `json:"usuario_id"`
|
||||||
|
Data pgtype.Date `json:"data"`
|
||||||
|
Data_2 pgtype.Date `json:"data_2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListDisponibilidadeByPeriod(ctx context.Context, arg ListDisponibilidadeByPeriodParams) ([]DisponibilidadeProfissionai, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listDisponibilidadeByPeriod, arg.UsuarioID, arg.Data, arg.Data_2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []DisponibilidadeProfissionai
|
||||||
|
for rows.Next() {
|
||||||
|
var i DisponibilidadeProfissionai
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UsuarioID,
|
||||||
|
&i.Data,
|
||||||
|
&i.Status,
|
||||||
|
&i.CriadoEm,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,7 @@ type AgendaProfissionai struct {
|
||||||
Status pgtype.Text `json:"status"`
|
Status pgtype.Text `json:"status"`
|
||||||
MotivoRejeicao pgtype.Text `json:"motivo_rejeicao"`
|
MotivoRejeicao pgtype.Text `json:"motivo_rejeicao"`
|
||||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
|
Posicao pgtype.Text `json:"posicao"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnosFormatura struct {
|
type AnosFormatura struct {
|
||||||
|
|
@ -117,6 +118,14 @@ type Curso struct {
|
||||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DisponibilidadeProfissionai struct {
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
|
UsuarioID pgtype.UUID `json:"usuario_id"`
|
||||||
|
Data pgtype.Date `json:"data"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
|
}
|
||||||
|
|
||||||
type Empresa struct {
|
type Empresa struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
Nome string `json:"nome"`
|
Nome string `json:"nome"`
|
||||||
|
|
|
||||||
|
|
@ -151,3 +151,30 @@ UPDATE agenda_profissionais
|
||||||
SET status = $3, motivo_rejeicao = $4
|
SET status = $3, motivo_rejeicao = $4
|
||||||
WHERE agenda_id = $1 AND profissional_id = $2
|
WHERE agenda_id = $1 AND profissional_id = $2
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: UpdateAssignmentPosition :one
|
||||||
|
UPDATE agenda_profissionais
|
||||||
|
SET posicao = $3
|
||||||
|
WHERE agenda_id = $1 AND profissional_id = $2
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: ListAvailableProfessionalsForDate :many
|
||||||
|
SELECT
|
||||||
|
p.*,
|
||||||
|
u.email,
|
||||||
|
f.nome as funcao_nome,
|
||||||
|
dp.status as status_disponibilidade
|
||||||
|
FROM cadastro_profissionais p
|
||||||
|
JOIN usuarios u ON p.usuario_id = u.id
|
||||||
|
JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
|
||||||
|
JOIN disponibilidade_profissionais dp ON u.id = dp.usuario_id
|
||||||
|
WHERE dp.data = $1
|
||||||
|
AND dp.status = 'DISPONIVEL'
|
||||||
|
AND p.id NOT IN (
|
||||||
|
SELECT ap.profissional_id
|
||||||
|
FROM agenda_profissionais ap
|
||||||
|
JOIN agenda a ON ap.agenda_id = a.id
|
||||||
|
WHERE a.data_evento = $1
|
||||||
|
AND ap.status = 'ACEITO'
|
||||||
|
)
|
||||||
|
ORDER BY p.nome;
|
||||||
|
|
|
||||||
21
backend/internal/db/queries/availability.sql
Normal file
21
backend/internal/db/queries/availability.sql
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- name: CreateDisponibilidade :one
|
||||||
|
INSERT INTO disponibilidade_profissionais (usuario_id, data, status)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (usuario_id, data) DO UPDATE
|
||||||
|
SET status = EXCLUDED.status, criado_em = NOW()
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: ListDisponibilidadeByPeriod :many
|
||||||
|
SELECT * FROM disponibilidade_profissionais
|
||||||
|
WHERE usuario_id = $1
|
||||||
|
AND data >= $2
|
||||||
|
AND data <= $3
|
||||||
|
ORDER BY data;
|
||||||
|
|
||||||
|
-- name: GetDisponibilidadeByDate :one
|
||||||
|
SELECT * FROM disponibilidade_profissionais
|
||||||
|
WHERE usuario_id = $1 AND data = $2;
|
||||||
|
|
||||||
|
-- name: DeleteDisponibilidade :exec
|
||||||
|
DELETE FROM disponibilidade_profissionais
|
||||||
|
WHERE usuario_id = $1 AND data = $2;
|
||||||
|
|
@ -356,3 +356,14 @@ CREATE TABLE IF NOT EXISTS agenda_profissionais (
|
||||||
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
UNIQUE(agenda_id, profissional_id)
|
UNIQUE(agenda_id, profissional_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ALTER TABLE agenda_profissionais ADD COLUMN IF NOT EXISTS posicao VARCHAR(100);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS disponibilidade_profissionais (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
usuario_id UUID NOT NULL REFERENCES usuarios(id) ON DELETE CASCADE,
|
||||||
|
data DATE NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'DISPONIVEL', -- DISPONIVEL, INDISPONIVEL
|
||||||
|
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE(usuario_id, data)
|
||||||
|
);
|
||||||
|
|
|
||||||
123
frontend/components/ProfessionalDetailsModal.tsx
Normal file
123
frontend/components/ProfessionalDetailsModal.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Professional } from '../types';
|
||||||
|
import { Button } from './Button';
|
||||||
|
import { X, Mail, Phone, MapPin, Building, Star, Camera, DollarSign, Award } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ProfessionalDetailsModalProps {
|
||||||
|
professional: Professional;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProfessionalDetailsModal: React.FC<ProfessionalDetailsModalProps> = ({
|
||||||
|
professional,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 animate-fadeIn">
|
||||||
|
<div className="bg-white rounded-2xl shadow-2xl max-w-2xl w-full overflow-hidden flex flex-col relative animate-slideIn">
|
||||||
|
|
||||||
|
{/* Header com Capa/Avatar Style */}
|
||||||
|
<div className="h-32 bg-gradient-to-r from-brand-purple to-brand-purple/80 relative">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-4 right-4 text-white hover:bg-white/20 p-2 rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Conteúdo Principal */}
|
||||||
|
<div className="px-8 pb-8 -mt-16 flex flex-col items-center sm:items-start relative z-10">
|
||||||
|
|
||||||
|
{/* Avatar Grande */}
|
||||||
|
<div
|
||||||
|
className="w-32 h-32 rounded-full border-4 border-white bg-white shadow-lg mb-4"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${professional.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(professional.name)}&background=random`})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="text-center sm:text-left w-full">
|
||||||
|
<h2 className="text-2xl font-serif font-bold text-brand-black">{professional.name}</h2>
|
||||||
|
<div className="flex flex-wrap justify-center sm:justify-start gap-2 mt-2">
|
||||||
|
<span className="px-3 py-1 bg-brand-gold/10 text-brand-black rounded-full text-sm font-medium border border-brand-gold/20">
|
||||||
|
{professional.role}
|
||||||
|
</span>
|
||||||
|
{/* Mock de Avaliação */}
|
||||||
|
<span className="px-3 py-1 bg-yellow-50 text-yellow-700 rounded-full text-sm font-medium border border-yellow-200 flex items-center gap-1">
|
||||||
|
<Star size={14} className="fill-yellow-500 text-yellow-500" /> 4.9
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-6 mt-8">
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-bold text-gray-900 border-b pb-2 flex items-center gap-2">
|
||||||
|
<Building size={18} className="text-brand-gold" />
|
||||||
|
Dados Pessoais
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3 text-sm">
|
||||||
|
<div className="flex items-center gap-3 text-gray-700">
|
||||||
|
<Mail size={16} className="text-gray-400" />
|
||||||
|
<span>{professional.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 text-gray-700">
|
||||||
|
<Phone size={16} className="text-gray-400" />
|
||||||
|
<span>{professional.phone || "Não informado"}</span>
|
||||||
|
</div>
|
||||||
|
{/* Endereço Mockado se não tiver no tipo, ou usar campos extras do backend se mapeados */}
|
||||||
|
<div className="flex items-center gap-3 text-gray-700">
|
||||||
|
<MapPin size={16} className="text-gray-400" />
|
||||||
|
<span>São Paulo, SP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-bold text-gray-900 border-b pb-2 flex items-center gap-2">
|
||||||
|
<Camera size={18} className="text-brand-gold" />
|
||||||
|
Equipamentos & Habilidades
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Mock de Habilidades / Equipamentos (pois não está no type Professional simples ainda) */}
|
||||||
|
<div className="text-sm text-gray-600 space-y-2">
|
||||||
|
<p>Equipamento Profissional: <span className="text-gray-900">Canon R6, Lentes série L</span></p>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-2">
|
||||||
|
{["Formatura", "Casamento", "Estúdio"].map(tag => (
|
||||||
|
<span key={tag} className="text-xs bg-gray-100 px-2 py-1 rounded text-gray-600">{tag}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full mt-8 bg-gray-50 p-4 rounded-lg border border-gray-100">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Award className="text-brand-gold mt-1" size={20} />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-gray-900 text-sm">Performance</h4>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">Este profissional tem mantido uma taxa de 100% de presença e alta satisfação nos últimos eventos.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full mt-8 flex justify-end">
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
Fechar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -881,12 +881,22 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
prev.map((e) => {
|
prev.map((e) => {
|
||||||
if (e.id === eventId) {
|
if (e.id === eventId) {
|
||||||
const current = e.photographerIds || [];
|
const current = e.photographerIds || [];
|
||||||
|
const currentAssignments = e.assignments || [];
|
||||||
if (current.includes(photographerId)) {
|
if (current.includes(photographerId)) {
|
||||||
// Remove
|
// Remove
|
||||||
return { ...e, photographerIds: current.filter(id => id !== photographerId) };
|
return {
|
||||||
|
...e,
|
||||||
|
photographerIds: current.filter(id => id !== photographerId),
|
||||||
|
assignments: currentAssignments.filter(a => a.professionalId !== photographerId)
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
// Add
|
// Add
|
||||||
return { ...e, photographerIds: [...current, photographerId] };
|
// Import AssignmentStatus if needed or use string "PENDENTE" matching the type
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
photographerIds: [...current, photographerId],
|
||||||
|
assignments: [...currentAssignments, { professionalId: photographerId, status: "PENDENTE" as any }]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,12 @@ import {
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { useData } from "../contexts/DataContext";
|
import { useData } from "../contexts/DataContext";
|
||||||
import { STATUS_COLORS } from "../constants";
|
import { STATUS_COLORS } from "../constants";
|
||||||
|
import { ProfessionalDetailsModal } from "../components/ProfessionalDetailsModal";
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
initialView?: "list" | "create";
|
initialView?: "list" | "create";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const Dashboard: React.FC<DashboardProps> = ({
|
export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
initialView = "list",
|
initialView = "list",
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -56,8 +55,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
type: "",
|
type: "",
|
||||||
});
|
});
|
||||||
const [isTeamModalOpen, setIsTeamModalOpen] = useState(false);
|
const [isTeamModalOpen, setIsTeamModalOpen] = useState(false);
|
||||||
|
const [viewingProfessional, setViewingProfessional] = useState<Professional | null>(null);
|
||||||
|
|
||||||
// Reset view when initialView prop changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialView) {
|
if (initialView) {
|
||||||
setView(initialView);
|
setView(initialView);
|
||||||
|
|
@ -65,6 +64,10 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
}
|
}
|
||||||
}, [initialView]);
|
}, [initialView]);
|
||||||
|
|
||||||
|
const handleViewProfessional = (professional: Professional) => {
|
||||||
|
setViewingProfessional(professional);
|
||||||
|
};
|
||||||
|
|
||||||
// Guard Clause for basic security
|
// Guard Clause for basic security
|
||||||
if (!user)
|
if (!user)
|
||||||
return <div className="p-10 text-center">Acesso Negado. Faça login.</div>;
|
return <div className="p-10 text-center">Acesso Negado. Faça login.</div>;
|
||||||
|
|
@ -730,61 +733,76 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Equipe Designada */}
|
{/* Equipe Designada */}
|
||||||
{(selectedEvent.photographerIds.length > 0 ||
|
{(selectedEvent.assignments && selectedEvent.assignments.filter(a => a.status !== "REJEITADO").length > 0) ||
|
||||||
user.role === UserRole.BUSINESS_OWNER ||
|
((user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) && selectedEvent.photographerIds.length > 0) ? (
|
||||||
user.role === UserRole.SUPERADMIN) && (
|
<div className="border p-5 rounded bg-white">
|
||||||
<div className="border p-5 rounded bg-white">
|
<div className="flex justify-between items-center mb-3">
|
||||||
<div className="flex justify-between items-center mb-3">
|
<h4 className="font-bold text-sm text-gray-700 flex items-center gap-2">
|
||||||
<h4 className="font-bold text-sm text-gray-700 flex items-center gap-2">
|
<Users size={16} className="text-brand-gold" />
|
||||||
<Users size={16} className="text-brand-gold" />
|
Equipe ({(selectedEvent.assignments || []).filter(a => a.status !== "REJEITADO").length})
|
||||||
Equipe ({selectedEvent.photographerIds.length})
|
</h4>
|
||||||
</h4>
|
{(user.role === UserRole.BUSINESS_OWNER ||
|
||||||
{(user.role === UserRole.BUSINESS_OWNER ||
|
user.role === UserRole.SUPERADMIN) && (
|
||||||
user.role === UserRole.SUPERADMIN) && (
|
<button
|
||||||
<button
|
onClick={handleManageTeam}
|
||||||
onClick={handleManageTeam}
|
className="text-brand-gold hover:text-brand-black transition-colors"
|
||||||
className="text-brand-gold hover:text-brand-black transition-colors"
|
title="Adicionar fotógrafo"
|
||||||
title="Adicionar fotógrafo"
|
>
|
||||||
>
|
<PlusCircle size={18} />
|
||||||
<PlusCircle size={18} />
|
</button>
|
||||||
</button>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedEvent.photographerIds.length > 0 ? (
|
{(selectedEvent.assignments || [])
|
||||||
<div className="space-y-2">
|
.filter(a => a.status !== "REJEITADO")
|
||||||
{selectedEvent.photographerIds.map((id) => {
|
.map((assignment) => {
|
||||||
const photographer = professionals.find(
|
const photographer = professionals.find(
|
||||||
(p) => p.id === id
|
(p) => p.id === assignment.professionalId
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
<div
|
||||||
|
key={assignment.professionalId}
|
||||||
|
className="flex items-center justify-between gap-2 text-sm mb-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
onClick={() => handleViewProfessional(photographer!)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
key={id}
|
className="w-8 h-8 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
|
||||||
className="flex items-center gap-2 text-sm"
|
style={{
|
||||||
>
|
backgroundImage: `url(${photographer?.avatar ||
|
||||||
<div
|
`https://i.pravatar.cc/100?u=${assignment.professionalId}`
|
||||||
className="w-8 h-8 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
|
})`,
|
||||||
style={{
|
backgroundSize: "cover",
|
||||||
backgroundImage: `url(${photographer?.avatar ||
|
}}
|
||||||
`https://i.pravatar.cc/100?u=${id}`
|
></div>
|
||||||
})`,
|
<div className="flex flex-col">
|
||||||
backgroundSize: "cover",
|
<span className="text-gray-700 font-medium">
|
||||||
}}
|
{photographer?.name || "Fotógrafo"}
|
||||||
></div>
|
</span>
|
||||||
<span className="text-gray-700">
|
<span className="text-xs text-gray-500">
|
||||||
{photographer?.name || id}
|
{assignment.status === "PENDENTE" ? "Convite Pendente" : "Confirmado"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
{assignment.status === "PENDENTE" && (
|
||||||
</div>
|
<span className="w-2 h-2 rounded-full bg-yellow-400" title="Pendente"></span>
|
||||||
) : (
|
)}
|
||||||
<p className="text-sm text-gray-400 italic">
|
{assignment.status === "ACEITO" && (
|
||||||
Nenhum profissional atribuído
|
<span className="w-2 h-2 rounded-full bg-green-500" title="Aceito"></span>
|
||||||
</p>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
)}
|
})}
|
||||||
|
{(selectedEvent.assignments || []).filter(a => a.status !== "REJEITADO").length === 0 && (
|
||||||
|
<p className="text-sm text-gray-400 italic">
|
||||||
|
Nenhum profissional na equipe.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -855,10 +873,12 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{professionals.map((photographer) => {
|
{professionals.map((photographer) => {
|
||||||
const isAssigned =
|
const assignment = (selectedEvent.assignments || []).find(
|
||||||
selectedEvent.photographerIds.includes(
|
(a) => a.professionalId === photographer.id
|
||||||
photographer.id
|
);
|
||||||
);
|
|
||||||
|
const status = assignment ? assignment.status : null;
|
||||||
|
const isAssigned = !!status && status !== "REJEITADO"; // Consider assigned if not rejected (effectively)
|
||||||
const isAvailable = true;
|
const isAvailable = true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -867,7 +887,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
className="border-b border-gray-100 hover:bg-gray-50 transition-colors"
|
className="border-b border-gray-100 hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
{/* Profissional */}
|
{/* Profissional */}
|
||||||
<td className="p-4">
|
<td className="p-4 cursor-pointer" onClick={() => handleViewProfessional(photographer)}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
className="w-10 h-10 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
|
className="w-10 h-10 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
|
||||||
|
|
@ -901,13 +921,26 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
|
|
||||||
{/* Status */}
|
{/* Status */}
|
||||||
<td className="p-4 text-center">
|
<td className="p-4 text-center">
|
||||||
{isAssigned ? (
|
{status === "ACEITO" && (
|
||||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-brand-gold/20 text-brand-black rounded-full text-xs font-medium">
|
|
||||||
<CheckCircle size={14} />
|
|
||||||
Atribuído
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-green-100 text-green-800 rounded-full text-xs font-medium">
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-green-100 text-green-800 rounded-full text-xs font-medium">
|
||||||
|
<CheckCircle size={14} />
|
||||||
|
Confirmado
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{status === "PENDENTE" && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-yellow-100 text-yellow-800 rounded-full text-xs font-medium">
|
||||||
|
<Clock size={14} />
|
||||||
|
Pendente
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{status === "REJEITADO" && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-red-100 text-red-800 rounded-full text-xs font-medium">
|
||||||
|
<X size={14} />
|
||||||
|
Recusado
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!status && (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-gray-100 text-gray-600 rounded-full text-xs font-medium">
|
||||||
<UserCheck size={14} />
|
<UserCheck size={14} />
|
||||||
Disponível
|
Disponível
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -920,15 +953,13 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
togglePhotographer(photographer.id)
|
togglePhotographer(photographer.id)
|
||||||
}
|
}
|
||||||
disabled={!isAvailable && !isAssigned}
|
disabled={false}
|
||||||
className={`px-4 py-2 rounded-lg font-medium text-sm transition-colors ${isAssigned
|
className={`px-4 py-2 rounded-lg font-medium text-sm transition-colors ${status === "ACEITO" || status === "PENDENTE"
|
||||||
? "bg-red-100 text-red-700 hover:bg-red-200"
|
? "bg-red-100 text-red-700 hover:bg-red-200"
|
||||||
: isAvailable
|
: "bg-brand-gold text-white hover:bg-[#a5bd2e]"
|
||||||
? "bg-brand-gold text-white hover:bg-[#a5bd2e]"
|
|
||||||
: "bg-gray-100 text-gray-400 cursor-not-allowed"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isAssigned ? "Remover" : "Adicionar"}
|
{status === "ACEITO" || status === "PENDENTE" ? "Remover" : "Adicionar"}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -967,6 +998,14 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{viewingProfessional && (
|
||||||
|
<ProfessionalDetailsModal
|
||||||
|
professional={viewingProfessional}
|
||||||
|
isOpen={!!viewingProfessional}
|
||||||
|
onClose={() => setViewingProfessional(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue