From 25aee29acdf6e5ddb3407bd4eab25580a7948779 Mon Sep 17 00:00:00 2001 From: NANDO9322 Date: Tue, 24 Feb 2026 18:42:22 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Implementa=C3=A7=C3=A3o=20Log=C3=ADstic?= =?UTF-8?q?a=20Di=C3=A1ria,=20Novo=20Cargo=20Pesquisador,=20Exporta=C3=A7?= =?UTF-8?q?=C3=A3o=20XLSX=20Financeiro=20e=20Corre=C3=A7=C3=B5es=20no=20Pa?= =?UTF-8?q?inel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cmd/api/main.go | 7 + backend/internal/auth/handler.go | 16 +- backend/internal/auth/service.go | 28 + backend/internal/auth/tokens.go | 5 + .../db/generated/convites_diarios.sql.go | 188 +++ .../db/generated/logistica_disponiveis.sql.go | 164 +++ backend/internal/db/generated/models.go | 11 + .../020_add_convites_diarios.down.sql | 1 + .../020_add_convites_diarios.up.sql | 11 + .../internal/db/queries/convites_diarios.sql | 30 + .../db/queries/logistica_disponiveis.sql | 26 + backend/internal/db/schema.sql | 12 + backend/internal/logistica/handler.go | 152 +++ backend/internal/logistica/service.go | 156 +++ frontend/App.tsx | 11 + frontend/components/Navbar.tsx | 1 + frontend/package-lock.json | 1021 +++++++++++++++++ frontend/package.json | 2 + frontend/pages/DailyLogistics.tsx | 574 +++++++++ frontend/pages/Dashboard.tsx | 276 ++++- frontend/pages/Finance.tsx | 309 ++++- frontend/pages/Team.tsx | 107 +- frontend/services/apiService.ts | 116 ++ 23 files changed, 3103 insertions(+), 121 deletions(-) create mode 100644 backend/internal/db/generated/convites_diarios.sql.go create mode 100644 backend/internal/db/generated/logistica_disponiveis.sql.go create mode 100644 backend/internal/db/migrations/020_add_convites_diarios.down.sql create mode 100644 backend/internal/db/migrations/020_add_convites_diarios.up.sql create mode 100644 backend/internal/db/queries/convites_diarios.sql create mode 100644 backend/internal/db/queries/logistica_disponiveis.sql create mode 100644 frontend/pages/DailyLogistics.tsx diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index 9798a11..97a1b3a 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -270,6 +270,13 @@ func main() { logisticaGroup.POST("/carros/:id/passageiros", logisticaHandler.AddPassenger) logisticaGroup.DELETE("/carros/:id/passageiros/:profID", logisticaHandler.RemovePassenger) logisticaGroup.GET("/carros/:id/passageiros", logisticaHandler.ListPassengers) + + // Daily Invitations and Available Staff + logisticaGroup.GET("/disponiveis", logisticaHandler.ListProfissionaisDisponiveis) + logisticaGroup.POST("/convites", logisticaHandler.CreateConvite) + logisticaGroup.GET("/convites", logisticaHandler.ListConvitesProfissional) + logisticaGroup.PUT("/convites/:id", logisticaHandler.ResponderConvite) + logisticaGroup.GET("/convites-por-data", logisticaHandler.ListConvitesPorData) } codigosGroup := api.Group("/codigos-acesso") diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index 411715a..15a44a5 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -146,6 +146,12 @@ func (h *Handler) Register(c *gin.Context) { MaxAge: 30 * 24 * 60 * 60, }) + // Calculate MaxAge for access token cookie based on role + accessMaxAge := 180 * 60 // Default 3 hours + if req.Role == "RESEARCHER" { + accessMaxAge = 30 * 24 * 60 * 60 // 30 days + } + // Set access_token cookie for fallback http.SetCookie(c.Writer, &http.Cookie{ Name: "access_token", @@ -154,7 +160,7 @@ func (h *Handler) Register(c *gin.Context) { HttpOnly: true, Secure: false, SameSite: http.SameSiteStrictMode, - MaxAge: 180 * 60, // 3 hours + MaxAge: accessMaxAge, }) c.JSON(http.StatusCreated, gin.H{ @@ -235,6 +241,12 @@ func (h *Handler) Login(c *gin.Context) { MaxAge: 30 * 24 * 60 * 60, }) + // Calculate MaxAge for access token cookie based on role + accessMaxAge := 180 * 60 // Default 3 hours + if user.Role == "RESEARCHER" { + accessMaxAge = 30 * 24 * 60 * 60 // 30 days + } + // Set access_token cookie for fallback http.SetCookie(c.Writer, &http.Cookie{ Name: "access_token", @@ -243,7 +255,7 @@ func (h *Handler) Login(c *gin.Context) { HttpOnly: true, Secure: false, SameSite: http.SameSiteStrictMode, - MaxAge: 180 * 60, // 3 hours + MaxAge: accessMaxAge, }) // Handle Nullable Fields diff --git a/backend/internal/auth/service.go b/backend/internal/auth/service.go index efffde9..2d12926 100644 --- a/backend/internal/auth/service.go +++ b/backend/internal/auth/service.go @@ -168,6 +168,34 @@ func (s *Service) Login(ctx context.Context, email, senha string) (*TokenPair, * return nil, nil, nil, err } + // Persist the Refresh Token in the database + refreshHash := sha256.Sum256([]byte(refreshToken)) + refreshHashString := hex.EncodeToString(refreshHash[:]) + + refreshExp := time.Now().Add(time.Duration(s.jwtRefreshTTLDays) * 24 * time.Hour) + // For Researchers, we can ensure the refresh token lives just as long as the extended access token + margin + if user.Role == "RESEARCHER" { + refreshExp = time.Now().Add(35 * 24 * time.Hour) // 35 days + } + + var ip, userAgent string // We could pass them from context if needed, but for now leave blank + + var expTime pgtype.Timestamptz + expTime.Time = refreshExp + expTime.Valid = true + + _, err = s.queries.CreateRefreshToken(ctx, generated.CreateRefreshTokenParams{ + UsuarioID: user.ID, + TokenHash: refreshHashString, + UserAgent: toPgText(&userAgent), + Ip: toPgText(&ip), + ExpiraEm: expTime, + }) + if err != nil { + fmt.Printf("[DEBUG] Failed to save refresh token: %v\n", err) + // We still return true login, but refresh might fail later + } + var profData *generated.GetProfissionalByUsuarioIDRow if user.Role == RolePhotographer || user.Role == RoleBusinessOwner { p, err := s.queries.GetProfissionalByUsuarioID(ctx, user.ID) diff --git a/backend/internal/auth/tokens.go b/backend/internal/auth/tokens.go index dc4f6df..82a4397 100644 --- a/backend/internal/auth/tokens.go +++ b/backend/internal/auth/tokens.go @@ -15,6 +15,11 @@ type Claims struct { } func GenerateAccessToken(userID uuid.UUID, role string, regioes []string, secret string, ttlMinutes int) (string, time.Time, error) { + // Extend TTL to 30 days specifically for RESEARCHER role to prevent session drop + if role == "RESEARCHER" { + ttlMinutes = 30 * 24 * 60 + } + expirationTime := time.Now().Add(time.Duration(ttlMinutes) * time.Minute) claims := &Claims{ UserID: userID, diff --git a/backend/internal/db/generated/convites_diarios.sql.go b/backend/internal/db/generated/convites_diarios.sql.go new file mode 100644 index 0000000..c13ad90 --- /dev/null +++ b/backend/internal/db/generated/convites_diarios.sql.go @@ -0,0 +1,188 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: convites_diarios.sql + +package generated + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const createConviteDiario = `-- name: CreateConviteDiario :one +INSERT INTO convites_diarios ( + profissional_id, data, status, regiao +) VALUES ( + $1, $2, $3, $4 +) +RETURNING id, profissional_id, data, status, motivo_rejeicao, regiao, criado_em, atualizado_em +` + +type CreateConviteDiarioParams struct { + ProfissionalID pgtype.UUID `json:"profissional_id"` + Data pgtype.Date `json:"data"` + Status pgtype.Text `json:"status"` + Regiao pgtype.Text `json:"regiao"` +} + +func (q *Queries) CreateConviteDiario(ctx context.Context, arg CreateConviteDiarioParams) (ConvitesDiario, error) { + row := q.db.QueryRow(ctx, createConviteDiario, + arg.ProfissionalID, + arg.Data, + arg.Status, + arg.Regiao, + ) + var i ConvitesDiario + err := row.Scan( + &i.ID, + &i.ProfissionalID, + &i.Data, + &i.Status, + &i.MotivoRejeicao, + &i.Regiao, + &i.CriadoEm, + &i.AtualizadoEm, + ) + return i, err +} + +const deleteConvite = `-- name: DeleteConvite :exec +DELETE FROM convites_diarios +WHERE id = $1 +` + +func (q *Queries) DeleteConvite(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, deleteConvite, id) + return err +} + +const getConviteByProfissionalAndDate = `-- name: GetConviteByProfissionalAndDate :one +SELECT id, profissional_id, data, status, motivo_rejeicao, regiao, criado_em, atualizado_em FROM convites_diarios +WHERE profissional_id = $1 AND data = $2 +` + +type GetConviteByProfissionalAndDateParams struct { + ProfissionalID pgtype.UUID `json:"profissional_id"` + Data pgtype.Date `json:"data"` +} + +func (q *Queries) GetConviteByProfissionalAndDate(ctx context.Context, arg GetConviteByProfissionalAndDateParams) (ConvitesDiario, error) { + row := q.db.QueryRow(ctx, getConviteByProfissionalAndDate, arg.ProfissionalID, arg.Data) + var i ConvitesDiario + err := row.Scan( + &i.ID, + &i.ProfissionalID, + &i.Data, + &i.Status, + &i.MotivoRejeicao, + &i.Regiao, + &i.CriadoEm, + &i.AtualizadoEm, + ) + return i, err +} + +const getConvitesByDateAndRegion = `-- name: GetConvitesByDateAndRegion :many +SELECT id, profissional_id, data, status, motivo_rejeicao, regiao, criado_em, atualizado_em FROM convites_diarios +WHERE data = $1 AND regiao = $2 +` + +type GetConvitesByDateAndRegionParams struct { + Data pgtype.Date `json:"data"` + Regiao pgtype.Text `json:"regiao"` +} + +func (q *Queries) GetConvitesByDateAndRegion(ctx context.Context, arg GetConvitesByDateAndRegionParams) ([]ConvitesDiario, error) { + rows, err := q.db.Query(ctx, getConvitesByDateAndRegion, arg.Data, arg.Regiao) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ConvitesDiario + for rows.Next() { + var i ConvitesDiario + if err := rows.Scan( + &i.ID, + &i.ProfissionalID, + &i.Data, + &i.Status, + &i.MotivoRejeicao, + &i.Regiao, + &i.CriadoEm, + &i.AtualizadoEm, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getConvitesByProfissional = `-- name: GetConvitesByProfissional :many +SELECT id, profissional_id, data, status, motivo_rejeicao, regiao, criado_em, atualizado_em FROM convites_diarios +WHERE profissional_id = $1 +ORDER BY data ASC +` + +func (q *Queries) GetConvitesByProfissional(ctx context.Context, profissionalID pgtype.UUID) ([]ConvitesDiario, error) { + rows, err := q.db.Query(ctx, getConvitesByProfissional, profissionalID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ConvitesDiario + for rows.Next() { + var i ConvitesDiario + if err := rows.Scan( + &i.ID, + &i.ProfissionalID, + &i.Data, + &i.Status, + &i.MotivoRejeicao, + &i.Regiao, + &i.CriadoEm, + &i.AtualizadoEm, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateConviteStatus = `-- name: UpdateConviteStatus :one +UPDATE convites_diarios +SET status = $2, motivo_rejeicao = $3, atualizado_em = NOW() +WHERE id = $1 +RETURNING id, profissional_id, data, status, motivo_rejeicao, regiao, criado_em, atualizado_em +` + +type UpdateConviteStatusParams struct { + ID pgtype.UUID `json:"id"` + Status pgtype.Text `json:"status"` + MotivoRejeicao pgtype.Text `json:"motivo_rejeicao"` +} + +func (q *Queries) UpdateConviteStatus(ctx context.Context, arg UpdateConviteStatusParams) (ConvitesDiario, error) { + row := q.db.QueryRow(ctx, updateConviteStatus, arg.ID, arg.Status, arg.MotivoRejeicao) + var i ConvitesDiario + err := row.Scan( + &i.ID, + &i.ProfissionalID, + &i.Data, + &i.Status, + &i.MotivoRejeicao, + &i.Regiao, + &i.CriadoEm, + &i.AtualizadoEm, + ) + return i, err +} diff --git a/backend/internal/db/generated/logistica_disponiveis.sql.go b/backend/internal/db/generated/logistica_disponiveis.sql.go new file mode 100644 index 0000000..6f3684f --- /dev/null +++ b/backend/internal/db/generated/logistica_disponiveis.sql.go @@ -0,0 +1,164 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: logistica_disponiveis.sql + +package generated + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const countProfissionaisDisponiveisLogistica = `-- name: CountProfissionaisDisponiveisLogistica :one +SELECT COUNT(p.id) +FROM cadastro_profissionais p +WHERE p.regiao = $1 + AND ($2::text = '' OR p.nome ILIKE '%' || $2 || '%') + AND NOT EXISTS ( + SELECT 1 FROM convites_diarios cd + WHERE cd.profissional_id = p.id + AND cd.data = CAST($3 AS DATE) + AND cd.status IN ('PENDENTE', 'ACEITO') + ) +` + +type CountProfissionaisDisponiveisLogisticaParams struct { + Regiao pgtype.Text `json:"regiao"` + Column2 string `json:"column_2"` + Column3 pgtype.Date `json:"column_3"` +} + +func (q *Queries) CountProfissionaisDisponiveisLogistica(ctx context.Context, arg CountProfissionaisDisponiveisLogisticaParams) (int64, error) { + row := q.db.QueryRow(ctx, countProfissionaisDisponiveisLogistica, arg.Regiao, arg.Column2, arg.Column3) + var count int64 + err := row.Scan(&count) + return count, err +} + +const getProfissionaisDisponiveisLogistica = `-- name: GetProfissionaisDisponiveisLogistica :many +SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cep, p.numero, p.complemento, p.bairro, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta, 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, p.regiao, f.nome as funcao_nome +FROM cadastro_profissionais p +LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id +WHERE p.regiao = $1 + AND ($2::text = '' OR p.nome ILIKE '%' || $2 || '%') + AND NOT EXISTS ( + SELECT 1 FROM convites_diarios cd + WHERE cd.profissional_id = p.id + AND cd.data = CAST($3 AS DATE) + AND cd.status IN ('PENDENTE', 'ACEITO') + ) +ORDER BY p.nome ASC +LIMIT $4 OFFSET $5 +` + +type GetProfissionaisDisponiveisLogisticaParams struct { + Regiao pgtype.Text `json:"regiao"` + Column2 string `json:"column_2"` + Column3 pgtype.Date `json:"column_3"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +type GetProfissionaisDisponiveisLogisticaRow 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"` + Cep pgtype.Text `json:"cep"` + Numero pgtype.Text `json:"numero"` + Complemento pgtype.Text `json:"complemento"` + Bairro pgtype.Text `json:"bairro"` + CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"` + Banco pgtype.Text `json:"banco"` + Agencia pgtype.Text `json:"agencia"` + Conta pgtype.Text `json:"conta"` + 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"` + Regiao pgtype.Text `json:"regiao"` + FuncaoNome pgtype.Text `json:"funcao_nome"` +} + +func (q *Queries) GetProfissionaisDisponiveisLogistica(ctx context.Context, arg GetProfissionaisDisponiveisLogisticaParams) ([]GetProfissionaisDisponiveisLogisticaRow, error) { + rows, err := q.db.Query(ctx, getProfissionaisDisponiveisLogistica, + arg.Regiao, + arg.Column2, + arg.Column3, + arg.Limit, + arg.Offset, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetProfissionaisDisponiveisLogisticaRow + for rows.Next() { + var i GetProfissionaisDisponiveisLogisticaRow + if err := rows.Scan( + &i.ID, + &i.UsuarioID, + &i.Nome, + &i.FuncaoProfissionalID, + &i.Endereco, + &i.Cidade, + &i.Uf, + &i.Whatsapp, + &i.Cep, + &i.Numero, + &i.Complemento, + &i.Bairro, + &i.CpfCnpjTitular, + &i.Banco, + &i.Agencia, + &i.Conta, + &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.Regiao, + &i.FuncaoNome, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/backend/internal/db/generated/models.go b/backend/internal/db/generated/models.go index 0c9957f..e605af2 100644 --- a/backend/internal/db/generated/models.go +++ b/backend/internal/db/generated/models.go @@ -159,6 +159,17 @@ type CodigosAcesso struct { Regiao pgtype.Text `json:"regiao"` } +type ConvitesDiario struct { + ID pgtype.UUID `json:"id"` + ProfissionalID pgtype.UUID `json:"profissional_id"` + Data pgtype.Date `json:"data"` + Status pgtype.Text `json:"status"` + MotivoRejeicao pgtype.Text `json:"motivo_rejeicao"` + Regiao pgtype.Text `json:"regiao"` + CriadoEm pgtype.Timestamptz `json:"criado_em"` + AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` +} + type Curso struct { ID pgtype.UUID `json:"id"` Nome string `json:"nome"` diff --git a/backend/internal/db/migrations/020_add_convites_diarios.down.sql b/backend/internal/db/migrations/020_add_convites_diarios.down.sql new file mode 100644 index 0000000..6981af7 --- /dev/null +++ b/backend/internal/db/migrations/020_add_convites_diarios.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS convites_diarios CASCADE; diff --git a/backend/internal/db/migrations/020_add_convites_diarios.up.sql b/backend/internal/db/migrations/020_add_convites_diarios.up.sql new file mode 100644 index 0000000..3f32bf2 --- /dev/null +++ b/backend/internal/db/migrations/020_add_convites_diarios.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS convites_diarios ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profissional_id UUID NOT NULL REFERENCES cadastro_profissionais(id) ON DELETE CASCADE, + data DATE NOT NULL, + status VARCHAR(20) DEFAULT 'PENDENTE', -- PENDENTE, ACEITO, REJEITADO + motivo_rejeicao TEXT, + regiao CHAR(2) DEFAULT 'SP', + criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), + atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(profissional_id, data) +); diff --git a/backend/internal/db/queries/convites_diarios.sql b/backend/internal/db/queries/convites_diarios.sql new file mode 100644 index 0000000..b1bb230 --- /dev/null +++ b/backend/internal/db/queries/convites_diarios.sql @@ -0,0 +1,30 @@ +-- name: CreateConviteDiario :one +INSERT INTO convites_diarios ( + profissional_id, data, status, regiao +) VALUES ( + $1, $2, $3, $4 +) +RETURNING *; + +-- name: GetConvitesByProfissional :many +SELECT * FROM convites_diarios +WHERE profissional_id = $1 +ORDER BY data ASC; + +-- name: GetConvitesByDateAndRegion :many +SELECT * FROM convites_diarios +WHERE data = $1 AND regiao = $2; + +-- name: GetConviteByProfissionalAndDate :one +SELECT * FROM convites_diarios +WHERE profissional_id = $1 AND data = $2; + +-- name: UpdateConviteStatus :one +UPDATE convites_diarios +SET status = $2, motivo_rejeicao = $3, atualizado_em = NOW() +WHERE id = $1 +RETURNING *; + +-- name: DeleteConvite :exec +DELETE FROM convites_diarios +WHERE id = $1; diff --git a/backend/internal/db/queries/logistica_disponiveis.sql b/backend/internal/db/queries/logistica_disponiveis.sql new file mode 100644 index 0000000..b885b46 --- /dev/null +++ b/backend/internal/db/queries/logistica_disponiveis.sql @@ -0,0 +1,26 @@ +-- name: GetProfissionaisDisponiveisLogistica :many +SELECT p.*, f.nome as funcao_nome +FROM cadastro_profissionais p +LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id +WHERE p.regiao = $1 + AND ($2::text = '' OR p.nome ILIKE '%' || $2 || '%') + AND NOT EXISTS ( + SELECT 1 FROM convites_diarios cd + WHERE cd.profissional_id = p.id + AND cd.data = CAST($3 AS DATE) + AND cd.status IN ('PENDENTE', 'ACEITO') + ) +ORDER BY p.nome ASC +LIMIT $4 OFFSET $5; + +-- name: CountProfissionaisDisponiveisLogistica :one +SELECT COUNT(p.id) +FROM cadastro_profissionais p +WHERE p.regiao = $1 + AND ($2::text = '' OR p.nome ILIKE '%' || $2 || '%') + AND NOT EXISTS ( + SELECT 1 FROM convites_diarios cd + WHERE cd.profissional_id = p.id + AND cd.data = CAST($3 AS DATE) + AND cd.status IN ('PENDENTE', 'ACEITO') + ); diff --git a/backend/internal/db/schema.sql b/backend/internal/db/schema.sql index 7217c5f..5c30282 100644 --- a/backend/internal/db/schema.sql +++ b/backend/internal/db/schema.sql @@ -262,6 +262,18 @@ CREATE TABLE IF NOT EXISTS agenda_profissionais ( UNIQUE(agenda_id, profissional_id) ); +CREATE TABLE IF NOT EXISTS convites_diarios ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + profissional_id UUID NOT NULL REFERENCES cadastro_profissionais(id) ON DELETE CASCADE, + data DATE NOT NULL, + status VARCHAR(20) DEFAULT 'PENDENTE', -- PENDENTE, ACEITO, REJEITADO + motivo_rejeicao TEXT, + regiao CHAR(2) DEFAULT 'SP', + criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), + atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(profissional_id, data) +); + 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, diff --git a/backend/internal/logistica/handler.go b/backend/internal/logistica/handler.go index 8890aff..daa8c5c 100644 --- a/backend/internal/logistica/handler.go +++ b/backend/internal/logistica/handler.go @@ -162,3 +162,155 @@ func (h *Handler) ListPassengers(c *gin.Context) { } c.JSON(http.StatusOK, resp) } + +// ---- LOGISTICA DAILY INVITATIONS & PROFESSIONALS LIST ---- // + +func (h *Handler) ListProfissionaisDisponiveis(c *gin.Context) { + var query ListDisponiveisInput + if err := c.ShouldBindQuery(&query); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + regiao := c.GetString("regiao") + res, err := h.service.ListProfissionaisDisponiveisLogistica(c.Request.Context(), query, regiao) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + formatted := make([]map[string]interface{}, len(res.Data)) + for i, r := range res.Data { + formatted[i] = map[string]interface{}{ + "id": uuid.UUID(r.ID.Bytes).String(), + "nome": r.Nome, + "role": r.FuncaoNome.String, + "avatar_url": r.AvatarUrl.String, + "whatsapp": r.Whatsapp.String, + "cidade": r.Cidade.String, + "carro_disponivel": r.CarroDisponivel.Bool, + } + } + + c.JSON(http.StatusOK, gin.H{ + "data": formatted, + "total": res.Total, + "page": res.Page, + "total_pages": res.TotalPages, + }) +} + +func (h *Handler) CreateConvite(c *gin.Context) { + var req CreateConviteInput + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + regiao := c.GetString("regiao") + convite, err := h.service.CreateConviteDiario(c.Request.Context(), req, regiao) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{"id": uuid.UUID(convite.ID.Bytes).String()}) +} + +// Get Invitations for logged professional +func (h *Handler) ListConvitesProfissional(c *gin.Context) { + // For MVP: Let the frontend send the prof ID or use a dedicated endpoint where frontend provides it. + // Actually frontend can provide ?prof_id=... + + pID := c.Query("prof_id") + if pID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "prof_id is required"}) + return + } + + convites, err := h.service.ListConvitesDoProfissional(c.Request.Context(), pID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Format response nicely + resp := make([]map[string]interface{}, len(convites)) + for i, c := range convites { + // convert time.Time to YYYY-MM-DD + var dateStr string + if c.Data.Valid { + dateStr = c.Data.Time.Format("2006-01-02") + } + + resp[i] = map[string]interface{}{ + "id": uuid.UUID(c.ID.Bytes).String(), + "profissional_id": uuid.UUID(c.ProfissionalID.Bytes).String(), + "data": dateStr, + "status": c.Status.String, + "motivo_rejeicao": c.MotivoRejeicao.String, + "criado_em": c.CriadoEm.Time.Format("2006-01-02T15:04:05Z07:00"), + } + } + + c.JSON(http.StatusOK, resp) +} + +// Respond to an invitation +func (h *Handler) ResponderConvite(c *gin.Context) { + conviteID := c.Param("id") + if conviteID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "id required"}) + return + } + + var req ResponderConviteInput + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + convite, err := h.service.ResponderConvite(c.Request.Context(), conviteID, req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": convite.Status.String}) +} + +// Get Invitations by Date for Logistics Manager +func (h *Handler) ListConvitesPorData(c *gin.Context) { + dataStr := c.Query("data") + if dataStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "data is required"}) + return + } + + regiao := c.GetString("regiao") + convites, err := h.service.ListConvitesPorData(c.Request.Context(), dataStr, regiao) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Format response + resp := make([]map[string]interface{}, len(convites)) + for i, conv := range convites { + var dateStr string + if conv.Data.Valid { + dateStr = conv.Data.Time.Format("2006-01-02") + } + + resp[i] = map[string]interface{}{ + "id": uuid.UUID(conv.ID.Bytes).String(), + "profissional_id": uuid.UUID(conv.ProfissionalID.Bytes).String(), + "data": dateStr, + "status": conv.Status.String, + "motivo_rejeicao": conv.MotivoRejeicao.String, + "criado_em": conv.CriadoEm.Time.Format("2006-01-02T15:04:05Z07:00"), + } + } + + c.JSON(http.StatusOK, resp) +} diff --git a/backend/internal/logistica/service.go b/backend/internal/logistica/service.go index 861615d..397919e 100644 --- a/backend/internal/logistica/service.go +++ b/backend/internal/logistica/service.go @@ -2,6 +2,8 @@ package logistica import ( "context" + "fmt" + "time" "photum-backend/internal/config" "photum-backend/internal/db/generated" @@ -248,3 +250,157 @@ func (s *Service) ListPassageiros(ctx context.Context, carroID string) ([]genera } return s.queries.ListPassageirosByCarroID(ctx, pgtype.UUID{Bytes: cID, Valid: true}) } + +// ---- LOGISTICA DAILY INVITATIONS & PROFESSIONALS LIST ---- // + +type ListDisponiveisInput struct { + Data string `form:"data" binding:"required"` + Search string `form:"search"` + Page int `form:"page,default=1"` + Limit int `form:"limit,default=50"` +} + +type ListDisponiveisResult struct { + Data []generated.GetProfissionaisDisponiveisLogisticaRow `json:"data"` + Total int64 `json:"total"` + Page int `json:"page"` + TotalPages int `json:"total_pages"` +} + +func (s *Service) ListProfissionaisDisponiveisLogistica(ctx context.Context, input ListDisponiveisInput, regiao string) (*ListDisponiveisResult, error) { + offset := (input.Page - 1) * input.Limit + + parsedDate, parsedDateErr := time.Parse("2006-01-02", input.Data) + + arg := generated.GetProfissionaisDisponiveisLogisticaParams{ + Regiao: pgtype.Text{String: regiao, Valid: true}, + Column2: input.Search, + Column3: pgtype.Date{Time: parsedDate, Valid: parsedDateErr == nil}, + Limit: int32(input.Limit), + Offset: int32(offset), + } + + rows, err := s.queries.GetProfissionaisDisponiveisLogistica(ctx, arg) + if err != nil { + return nil, err + } + + countArg := generated.CountProfissionaisDisponiveisLogisticaParams{ + Regiao: pgtype.Text{String: regiao, Valid: true}, + Column2: input.Search, + Column3: pgtype.Date{Time: parsedDate, Valid: parsedDateErr == nil}, + } + total, err := s.queries.CountProfissionaisDisponiveisLogistica(ctx, countArg) + if err != nil { + return nil, err + } + + if rows == nil { + rows = []generated.GetProfissionaisDisponiveisLogisticaRow{} + } + + totalPages := int(total) / input.Limit + if int(total)%input.Limit > 0 { + totalPages++ + } + + return &ListDisponiveisResult{ + Data: rows, + Total: total, + Page: input.Page, + TotalPages: totalPages, + }, nil +} + +type CreateConviteInput struct { + ProfissionalID string `json:"profissional_id" binding:"required"` + Data string `json:"data" binding:"required"` +} + +func (s *Service) CreateConviteDiario(ctx context.Context, input CreateConviteInput, regiao string) (*generated.ConvitesDiario, error) { + profUUID, err := uuid.Parse(input.ProfissionalID) + if err != nil { + return nil, err + } + + arg := generated.CreateConviteDiarioParams{ + ProfissionalID: pgtype.UUID{Bytes: profUUID, Valid: true}, + Status: pgtype.Text{String: "PENDENTE", Valid: true}, + Regiao: pgtype.Text{String: regiao, Valid: true}, + } + + t, err := time.Parse("2006-01-02", input.Data) + if err == nil { + arg.Data = pgtype.Date{Time: t, Valid: true} + } + + convite, err := s.queries.CreateConviteDiario(ctx, arg) + if err != nil { + return nil, err + } + + return &convite, nil +} + +// List Invitations for a Professional +func (s *Service) ListConvitesDoProfissional(ctx context.Context, profID string) ([]generated.ConvitesDiario, error) { + pID, err := uuid.Parse(profID) + if err != nil { + return nil, err + } + return s.queries.GetConvitesByProfissional(ctx, pgtype.UUID{Bytes: pID, Valid: true}) +} + +// Answer Invitation +type ResponderConviteInput struct { + Status string `json:"status" binding:"required"` // ACEITO, REJEITADO + MotivoRejeicao string `json:"motivo_rejeicao"` +} + +func (s *Service) ResponderConvite(ctx context.Context, conviteID string, input ResponderConviteInput) (*generated.ConvitesDiario, error) { + cID, err := uuid.Parse(conviteID) + if err != nil { + return nil, err + } + + validStatus := input.Status == "ACEITO" || input.Status == "REJEITADO" + if !validStatus { + return nil, fmt.Errorf("status inválido") // fmt must be imported, Oh wait.. just use errors.New if fmt is not there. I will just rely on simple return. Wait I can use built-in error. + } + + arg := generated.UpdateConviteStatusParams{ + ID: pgtype.UUID{Bytes: cID, Valid: true}, + Status: pgtype.Text{String: input.Status, Valid: true}, + MotivoRejeicao: pgtype.Text{String: input.MotivoRejeicao, Valid: input.MotivoRejeicao != ""}, + } + + convite, err := s.queries.UpdateConviteStatus(ctx, arg) + if err != nil { + return nil, err + } + + return &convite, nil +} + +// List Invitations by Date and Region (Logistics view) +func (s *Service) ListConvitesPorData(ctx context.Context, dataStr string, regiao string) ([]generated.ConvitesDiario, error) { + parsedDate, err := time.Parse("2006-01-02", dataStr) + if err != nil { + return nil, fmt.Errorf("formato de data inválido, esperado YYYY-MM-DD") + } + + datePg := pgtype.Date{ + Time: parsedDate, + Valid: true, + } + + regPg := pgtype.Text{ + String: regiao, + Valid: regiao != "", + } + + return s.queries.GetConvitesByDateAndRegion(ctx, generated.GetConvitesByDateAndRegionParams{ + Data: datePg, + Regiao: regPg, + }) +} diff --git a/frontend/App.tsx b/frontend/App.tsx index 072b120..a4c5079 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -17,6 +17,7 @@ import { RegistrationSuccess } from "./pages/RegistrationSuccess"; import { TeamPage } from "./pages/Team"; import EventDetails from "./pages/EventDetails"; import Finance from "./pages/Finance"; +import { DailyLogistics } from "./pages/DailyLogistics"; import { SettingsPage } from "./pages/Settings"; import { CourseManagement } from "./pages/CourseManagement"; @@ -641,6 +642,16 @@ const AppContent: React.FC = () => { } /> + + + + + + } + /> = ({ onNavigate, currentPage }) => { return [ { name: "Agenda", path: "painel" }, { name: "Equipe", path: "equipe" }, + { name: "Logística", path: "logistica-diaria" }, { name: "Cadastro de FOT", path: "cursos" }, { name: "Aprovação de Cadastros", path: "aprovacao-cadastros" }, { name: "Códigos de Acesso", path: "codigos-acesso" }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b49599b..5302061 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,8 @@ "dependencies": { "@google/genai": "^1.30.0", "@types/mapbox-gl": "^3.4.1", + "exceljs": "^4.4.0", + "file-saver": "^2.0.5", "lucide-react": "^0.554.0", "mapbox-gl": "^3.16.0", "react": "^19.2.0", @@ -749,6 +751,47 @@ "node": ">=18" } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, "node_modules/@google/genai": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.30.0.tgz", @@ -1384,6 +1427,124 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1420,6 +1581,15 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -1429,6 +1599,36 @@ "node": "*" } }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -1472,12 +1672,62 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001757", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", @@ -1512,6 +1762,18 @@ "node": ">=0.8" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/cheap-ruler": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz", @@ -1545,6 +1807,27 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1565,6 +1848,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -1577,6 +1866,19 @@ "node": ">=0.8" } }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1612,6 +1914,12 @@ "node": ">= 12" } }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1629,6 +1937,45 @@ } } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/earcut": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", @@ -1663,6 +2010,15 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -1715,12 +2071,45 @@ "node": ">=6" } }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1762,6 +2151,12 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -1799,6 +2194,18 @@ "node": ">=0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1814,6 +2221,78 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fstream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fstream/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/gaxios": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", @@ -1921,6 +2400,12 @@ "node": ">=14" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/grid-index": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", @@ -1953,6 +2438,49 @@ "node": ">= 14" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1962,6 +2490,12 @@ "node": ">=8" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2025,6 +2559,48 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -2052,6 +2628,142 @@ "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", "license": "ISC" }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2142,6 +2854,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -2151,6 +2872,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2227,12 +2960,45 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2331,6 +3097,12 @@ "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", "license": "ISC" }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/protocol-buffers-schema": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", @@ -2429,6 +3201,41 @@ "react-dom": ">=18" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.7.tgz", + "integrity": "sha512-FjiwU9HaHW6YB3H4a1sFudnv93lvydNjz2lmyUXR6IwKhGI+bgL3SOZrBGn6kvvX2pJvhEkGSGjyTHN47O4rqA==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/resolve-protobuf-schema": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", @@ -2521,6 +3328,18 @@ ], "license": "MIT" }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2552,6 +3371,12 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2613,6 +3438,15 @@ "node": ">=0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -2718,6 +3552,22 @@ "kdbush": "^4.0.2" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2741,6 +3591,24 @@ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", "license": "ISC" }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -2762,6 +3630,54 @@ "dev": true, "license": "MIT" }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -2793,6 +3709,21 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -3001,6 +3932,12 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -3043,12 +3980,96 @@ "node": ">=0.8" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/zip-stream/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 9a42560..5857167 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,8 @@ "dependencies": { "@google/genai": "^1.30.0", "@types/mapbox-gl": "^3.4.1", + "exceljs": "^4.4.0", + "file-saver": "^2.0.5", "lucide-react": "^0.554.0", "mapbox-gl": "^3.16.0", "react": "^19.2.0", diff --git a/frontend/pages/DailyLogistics.tsx b/frontend/pages/DailyLogistics.tsx new file mode 100644 index 0000000..2fc5a4d --- /dev/null +++ b/frontend/pages/DailyLogistics.tsx @@ -0,0 +1,574 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { useData } from '../contexts/DataContext'; +import { EventData, EventStatus, Professional } from '../types'; +import { RefreshCw, Users, CheckCircle, AlertTriangle, Calendar as CalendarIcon, MapPin, Clock, UserPlus, ChevronLeft, ChevronRight, ChevronDown, ChevronUp } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import { getAvailableProfessionalsLogistics, createDailyInvitation, getDailyInvitationsByDate } from '../services/apiService'; +import { toast } from 'react-hot-toast'; +import { ProfessionalDetailsModal } from '../components/ProfessionalDetailsModal'; +import { Car } from 'lucide-react'; // Added Car icon + +export const DailyLogistics: React.FC = () => { + const { events, professionals, isLoading, refreshEvents } = useData(); + const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); + const navigate = useNavigate(); + + const [searchTerm, setSearchTerm] = useState(""); + const [dailyInvitations, setDailyInvitations] = useState([]); + const [isInvitationsExpanded, setIsInvitationsExpanded] = useState(true); + const [viewingProfessional, setViewingProfessional] = useState(null); + const [isProfModalOpen, setIsProfModalOpen] = useState(false); + + // Utility para cores de cidade baseado na planilha do cliente + cores dinâmicas + const getCityColor = (city: string | undefined) => { + if (!city) return 'bg-gray-100 text-gray-700 border-gray-200'; // Default + const c = city.toLowerCase().trim(); + if (c.includes('campinas')) return 'bg-pink-100 text-pink-700 border-pink-200'; + if (c.includes('piracicaba')) return 'bg-cyan-100 text-cyan-700 border-cyan-200'; + if (c.includes('paulo') || c.includes('sp')) return 'bg-slate-200 text-slate-700 border-slate-300'; + if (c.includes('americana') || c.includes('sbo') || c.includes('barbara') || c.includes('bárbara') || c.includes('santa barbara')) return 'bg-green-100 text-green-700 border-green-200'; + if (c.includes('indaiatuba')) return 'bg-yellow-100 text-yellow-700 border-yellow-200'; + + // Dynamic color for others + const palette = [ + 'bg-purple-100 text-purple-700 border-purple-200', + 'bg-indigo-100 text-indigo-700 border-indigo-200', + 'bg-teal-100 text-teal-700 border-teal-200', + 'bg-orange-100 text-orange-700 border-orange-200', + 'bg-rose-100 text-rose-700 border-rose-200', + 'bg-violet-100 text-violet-700 border-violet-200', + 'bg-emerald-100 text-emerald-700 border-emerald-200', + 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-200' + ]; + let hash = 0; + for (let i = 0; i < c.length; i++) { + hash = c.charCodeAt(i) + ((hash << 5) - hash); + } + return palette[Math.abs(hash) % palette.length]; + }; + + const handleDateChange = (e: React.ChangeEvent) => { + setSelectedDate(e.target.value); + }; + + const handleRefresh = () => { + refreshEvents(); + }; + + // Filter events by the selected date + const eventsInDay = useMemo(() => { + return events.filter(e => { + if (!e.date) return false; + // e.date might be YYYY-MM-DD or full ISO + const eventDateStr = e.date.split('T')[0]; + return eventDateStr === selectedDate; + }); + }, [events, selectedDate]); + + // Aggregate metrics + const metrics = useMemo(() => { + let totalNeeded = 0; + let totalAccepted = 0; + let totalPending = 0; + + // Detailed Role missing counts + let fotMissing = 0; + let recMissing = 0; + let cinMissing = 0; + + // Track professionals already counted in events for this day + const eventAcceptedProfIds = new Set(); + const eventPendingProfIds = new Set(); + + eventsInDay.forEach(event => { + // Required professionals + const fotReq = Number(event.qtdFotografos || 0); + const recReq = Number(event.qtdRecepcionistas || 0); + const cinReq = Number(event.qtdCinegrafistas || 0); + + const eventTotalNeeded = fotReq + recReq + cinReq; + totalNeeded += eventTotalNeeded; + + let fotAc = 0, recAc = 0, cinAc = 0; + let eTotalAc = 0, eTotalPd = 0; + + // Count assignments + if (event.assignments) { + event.assignments.forEach(assignment => { + const status = assignment.status ? String(assignment.status).toUpperCase() : "AGUARDANDO"; + const prof = professionals.find(p => p.id === assignment.professionalId); + const roleName = prof?.role?.toLowerCase() || ""; + + if (status === "ACEITO" || status === "CONFIRMADO") { + eTotalAc++; + eventAcceptedProfIds.add(assignment.professionalId); + if (roleName.includes('fot')) fotAc++; + else if (roleName.includes('rece')) recAc++; + else if (roleName.includes('cin')) cinAc++; + else if (assignment.funcaoId === 'fotografo_id') fotAc++; + } else if (status === "AGUARDANDO" || status === "PENDENTE") { + eTotalPd++; + eventPendingProfIds.add(assignment.professionalId); + } + }); + } + + totalAccepted += eTotalAc; + totalPending += eTotalPd; + + // Calculate missing per role for this event + fotMissing += Math.max(0, fotReq - fotAc); + recMissing += Math.max(0, recReq - recAc); + cinMissing += Math.max(0, cinReq - cinAc); + }); + + // Add Daily Invitations to the generic counters only if not already allocated in an event + const uniqueDailyPending = dailyInvitations.filter(i => i.status === 'PENDENTE' && !eventPendingProfIds.has(i.profissional_id) && !eventAcceptedProfIds.has(i.profissional_id)).length; + const uniqueDailyAccepted = dailyInvitations.filter(i => i.status === 'ACEITO' && !eventAcceptedProfIds.has(i.profissional_id)).length; + + totalPending += uniqueDailyPending; + totalAccepted += uniqueDailyAccepted; + + const totalMissing = Math.max(0, totalNeeded - totalAccepted); + + return { + totalNeeded, totalAccepted, totalPending, totalMissing, + fotMissing, recMissing, cinMissing + }; + }, [eventsInDay, professionals, dailyInvitations]); + + const [paginatedProfessionals, setPaginatedProfessionals] = useState([]); + const [totalPros, setTotalPros] = useState(0); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [isProsLoading, setIsProsLoading] = useState(false); + + // Helper: Fetch available professionals from API + const fetchPros = async () => { + setIsProsLoading(true); + try { + const resp = await getAvailableProfessionalsLogistics(selectedDate, page, 50, searchTerm); + setPaginatedProfessionals(resp.data || []); + setTotalPros(resp.total || 0); + setTotalPages(resp.total_pages || 1); + } catch (err) { + console.error("Failed to fetch professionals", err); + toast.error("Erro ao carregar profissionais"); + } finally { + setIsProsLoading(false); + } + }; + + const fetchDailyInvitations = async () => { + try { + const res = await getDailyInvitationsByDate(selectedDate); + setDailyInvitations(res || []); + } catch(err) { + console.error(err); + } + }; + + useEffect(() => { + fetchPros(); + fetchDailyInvitations(); + }, [selectedDate, page, searchTerm]); + + // Select Event Context Handlers -> Now it's Daily Allocation! + const allocDaily = async (profId: string) => { + try { + await createDailyInvitation(profId, selectedDate); + toast.success("Convite de diária enviado!"); + fetchPros(); // Refresh list to remove from available + fetchDailyInvitations(); // refresh the pending invitations section + } catch (err: any) { + toast.error(err.message || "Erro ao alocar diária"); + } + }; + + // Render individual event card + const renderEventCard = (event: EventData) => { + const fotReq = Number(event.qtdFotografos || 0); + const recReq = Number(event.qtdRecepcionistas || 0); + const cinReq = Number(event.qtdCinegrafistas || 0); + const needed = fotReq + recReq + cinReq; + + // Group accepted/pending counts by role directly from event's assignments + // We assume assignments map back to the professional's functions or are inherently mapped + let accepted = 0; + let pending = 0; + + let fotAc = 0, recAc = 0, cinAc = 0; + + event.assignments?.forEach(a => { + const status = a.status ? String(a.status).toUpperCase() : "AGUARDANDO"; + const prof = professionals.find(p => p.id === a.professionalId); + const roleName = prof?.role?.toLowerCase() || ""; + + if (status === "ACEITO" || status === "CONFIRMADO") { + accepted++; + if (roleName.includes('fot')) fotAc++; + else if (roleName.includes('rece')) recAc++; + else if (roleName.includes('cin')) cinAc++; + else if (a.funcaoId === 'fotografo_id') fotAc++; // Fallback maps + } + else if (status === "AGUARDANDO" || status === "PENDENTE") pending++; + }); + const missing = Math.max(0, needed - accepted); + + return ( +
navigate(`/painel?eventId=${event.id}`)} + className="bg-white rounded-xl shadow-sm border border-gray-100 p-5 hover:shadow-md hover:border-brand-purple/50 transition-all cursor-pointer group" + > +
+
+

+ {event.name} + +

+
+ {event.time} + {event.address?.city || "Cidade N/A"} +
+
+ 0 ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}> + {missing > 0 ? `Faltam ${missing}` : 'Completo'} + +
+ + {/* Roles Breakdown */} +
+
+ Função +
+ A / N +
+
+ + {fotReq > 0 && ( +
+ Fotógrafo +
+ + {fotAc} / {fotReq} + +
+
+ )} + {recReq > 0 && ( +
+ Recepcionista +
+ + {recAc} / {recReq} + +
+
+ )} + {cinReq > 0 && ( +
+ Cinegrafista +
+ + {cinAc} / {cinReq} + +
+
+ )} + {needed === 0 && ( +
Nenhuma vaga registrada
+ )} +
+ + {Array.isArray(event.assignments) && event.assignments.length > 0 && ( +
+
+ Total de Convites: + {event.assignments.length} +
+ {pending > 0 && ( +
+ Aguardando resposta: + {pending} +
+ )} +
+ )} +
+ ); + }; + + + return ( +
+
+
+

Logística Diária

+

Acompanhamento e escala de equipe por data da agenda

+
+ +
+
+
+ +
+ +
+ +
+
+ + {/* Dash/Metrics Panel */} +
+
+
+
+

Necessários

+

{metrics.totalNeeded}

+
+
+
+
+
+

Aceitos

+

{metrics.totalAccepted}

+
+
+
+
+
+

Pendentes

+

{metrics.totalPending}

+
+
+
+
+
+
+

Faltam (Geral)

+

{metrics.totalMissing}

+
+
+ {/* Detailed Missing Roles */} + {metrics.totalMissing > 0 && ( +
+ {metrics.fotMissing > 0 && ( + Fotógrafos: {metrics.fotMissing} + )} + {metrics.recMissing > 0 && ( + Recepcionistas: {metrics.recMissing} + )} + {metrics.cinMissing > 0 && ( + Cinegrafistas: {metrics.cinMissing} + )} +
+ )} +
+
+ + {/* Daily Invitations Section */} + {dailyInvitations.length > 0 && ( +
+
setIsInvitationsExpanded(!isInvitationsExpanded)} + > +

+ Convites Enviados ({dailyInvitations.length}) +

+
+ {isInvitationsExpanded ? : } +
+
+ + {isInvitationsExpanded && ( +
+ {dailyInvitations.map((inv: any) => { + const prof = professionals.find(p => p.id === inv.profissional_id); + return ( +
{ + if (prof) { + setViewingProfessional(prof); + setIsProfModalOpen(true); + } + }} + > +
+ {prof?.nome} +
+
+

{prof?.nome || 'Desconhecido'}

+ {prof?.carro_disponivel && ( + + )} +
+

{prof?.role || 'Profissional'}

+ {prof?.cidade && ( + + {prof.cidade} + + )} +
+
+ + {inv.status} + +
+ ) + })} +
+ )} +
+ )} + + {/* Layout with Events (Left) and Available Professionals Sidebar (Right) */} +
+ {/* Left Column: Events */} +
+ {isLoading ? ( +
+ +
+ ) : eventsInDay.length === 0 ? ( +
+ +

Nenhum evento nesta data

+

Selecione outro dia no calendário acima.

+
+ ) : ( +
+ {eventsInDay.map(renderEventCard)} +
+ )} +
+ + {/* Right Column: Sticky Sidebar for Professionals */} +
+
+
+

+ Lista Geral de Disponíveis +

+

+ Profissionais com status livre no dia {selectedDate.split('-').reverse().join('/')} +

+
+ +
+ setSearchTerm(e.target.value)} + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-purple outline-none" + /> +
+ +
+ {isProsLoading ? ( +
+ +
+ ) : paginatedProfessionals.length === 0 ? ( +
+ +

Nenhum profissional disponível.

+

Todos ocupados ou não encontrados.

+
+ ) : ( + paginatedProfessionals.map(prof => ( +
+
+
{ + setViewingProfessional(prof); + setIsProfModalOpen(true); + }} + > + {prof.nome} +
+
+

{prof.nome}

+ {prof.carro_disponivel && ( + + )} +
+

{prof.role || "Profissional"}

+ {prof.cidade && ( + + {prof.cidade} + + )} +
+
+ +
+
+ )) + )} +
+ + {/* Pagination Context */} + {totalPages > 1 && ( +
+ + Página {page} de {totalPages} + +
+ )} +
+
+
+ + {/* Modal Profissionais */} + {isProfModalOpen && viewingProfessional && ( + { + setIsProfModalOpen(false); + setViewingProfessional(null); + }} + /> + )} +
+ ); +}; diff --git a/frontend/pages/Dashboard.tsx b/frontend/pages/Dashboard.tsx index b1785f1..913d032 100644 --- a/frontend/pages/Dashboard.tsx +++ b/frontend/pages/Dashboard.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useMemo } from "react"; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { UserRole, EventData, EventStatus, EventType, Professional } from "../types"; import { EventTable } from "../components/EventTable"; import { EventFiltersBar, EventFilters } from "../components/EventFiltersBar"; @@ -21,8 +21,9 @@ import { UserX, AlertCircle, Star, + Car, // Added Car icon } from "lucide-react"; -import { setCoordinator, finalizeFOT, getPrice } from "../services/apiService"; +import { setCoordinator, finalizeFOT, getPrice, getDailyInvitationsProfessional, respondDailyInvitation, getDailyInvitationsByDate } from "../services/apiService"; import { useAuth } from "../contexts/AuthContext"; import { useData } from "../contexts/DataContext"; import { STATUS_COLORS } from "../constants"; @@ -37,6 +38,7 @@ export const Dashboard: React.FC = ({ }) => { const { user, token } = useAuth(); const navigate = useNavigate(); + const location = useLocation(); // Extract updateEventDetails from useData const { events, @@ -54,7 +56,37 @@ export const Dashboard: React.FC = ({ refreshEvents, } = useData(); - // ... (inside component) + const [dailyInvitations, setDailyInvitations] = useState([]); + const [isLoadingInvites, setIsLoadingInvites] = useState(false); + + const fetchDailyInvitations = async () => { + if (user.role !== UserRole.PHOTOGRAPHER && user.role !== UserRole.AGENDA_VIEWER) return; + const currentProf = professionals.find(p => p.usuarioId === user.id); + if (!currentProf) return; + + setIsLoadingInvites(true); + try { + const resp = await getDailyInvitationsProfessional(currentProf.id); + setDailyInvitations(resp || []); + } catch (err) { + console.error(err); + } finally { + setIsLoadingInvites(false); + } + }; + + useEffect(() => { + if (professionals.length > 0) fetchDailyInvitations(); + }, [user, professionals]); + + const handleRespondDailyInvitation = async (id: string, status: "ACEITO" | "REJEITADO") => { + try { + await respondDailyInvitation(id, status); + fetchDailyInvitations(); + } catch (err) { + console.error("Error responding to invitation", err); + } + }; const handleSaveEvent = async (data: any) => { const isClient = user.role === UserRole.EVENT_OWNER; @@ -108,12 +140,12 @@ export const Dashboard: React.FC = ({ } }; const [view, setView] = useState<"list" | "create" | "edit" | "details">(() => { - const params = new URLSearchParams(window.location.search); + const params = new URLSearchParams(location.search); return params.get("eventId") ? "details" : initialView; }); const [searchTerm, setSearchTerm] = useState(""); const [selectedEvent, setSelectedEvent] = useState(() => { - const params = new URLSearchParams(window.location.search); + const params = new URLSearchParams(location.search); const eventId = params.get("eventId"); if (eventId && events.length > 0) { return events.find(e => e.id === eventId) || null; @@ -123,17 +155,27 @@ export const Dashboard: React.FC = ({ // Effect to sync selectedEvent if events load LATER than initial render useEffect(() => { - const params = new URLSearchParams(window.location.search); + const params = new URLSearchParams(location.search); const eventId = params.get("eventId"); - if (eventId && events.length > 0 && !selectedEvent) { + if (eventId && events.length > 0) { const found = events.find(e => e.id === eventId); - if (found) { + if (found && (!selectedEvent || selectedEvent.id !== eventId)) { setSelectedEvent(found); // Ensure view is details if we just found it setView("details"); + } else if (!found && !isLoading) { + // Se não encontrou o evento e já acabou de carregar do servidor, volta pra lista e limpa a URL + const newParams = new URLSearchParams(location.search); + newParams.delete("eventId"); + navigate({ search: newParams.toString() }, { replace: true }); + setSelectedEvent(null); + setView("list"); } + } else if (!eventId && selectedEvent) { + setSelectedEvent(null); + setView(initialView); } - }, [events, window.location.search]); + }, [events, location.search, selectedEvent, initialView, isLoading, navigate]); const [activeFilter, setActiveFilter] = useState("all"); const [advancedFilters, setAdvancedFilters] = useState({ date: "", @@ -150,9 +192,39 @@ export const Dashboard: React.FC = ({ const [teamRoleFilter, setTeamRoleFilter] = useState("all"); const [teamStatusFilter, setTeamStatusFilter] = useState("all"); const [teamAvailabilityFilter, setTeamAvailabilityFilter] = useState("all"); + const [showOnlyDailyAccepted, setShowOnlyDailyAccepted] = useState(false); + const [eventDailyInvitations, setEventDailyInvitations] = useState([]); const [roleSelectionProf, setRoleSelectionProf] = useState(null); const [basePrice, setBasePrice] = useState(null); + // Utility para cores de cidade baseado na planilha do cliente + cores dinâmicas + const getCityColor = (city: string | undefined) => { + if (!city) return 'bg-gray-100 text-gray-700 border-gray-200'; // Default + const c = city.toLowerCase().trim(); + if (c.includes('campinas')) return 'bg-pink-100 text-pink-700 border-pink-200'; + if (c.includes('piracicaba')) return 'bg-cyan-100 text-cyan-700 border-cyan-200'; + if (c.includes('paulo') || c.includes('sp')) return 'bg-slate-200 text-slate-700 border-slate-300'; + if (c.includes('americana') || c.includes('sbo') || c.includes('barbara') || c.includes('bárbara') || c.includes('santa barbara')) return 'bg-green-100 text-green-700 border-green-200'; + if (c.includes('indaiatuba')) return 'bg-yellow-100 text-yellow-700 border-yellow-200'; + + // Dynamic color for others + const palette = [ + 'bg-purple-100 text-purple-700 border-purple-200', + 'bg-indigo-100 text-indigo-700 border-indigo-200', + 'bg-teal-100 text-teal-700 border-teal-200', + 'bg-orange-100 text-orange-700 border-orange-200', + 'bg-rose-100 text-rose-700 border-rose-200', + 'bg-violet-100 text-violet-700 border-violet-200', + 'bg-emerald-100 text-emerald-700 border-emerald-200', + 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-200' + ]; + let hash = 0; + for (let i = 0; i < c.length; i++) { + hash = c.charCodeAt(i) + ((hash << 5) - hash); + } + return palette[Math.abs(hash) % palette.length]; + }; + useEffect(() => { const fetchBasePrice = async () => { if (!selectedEvent || !user || !token) { @@ -244,11 +316,12 @@ export const Dashboard: React.FC = ({ useEffect(() => { - if (initialView) { + const params = new URLSearchParams(location.search); + if (initialView && !params.get("eventId")) { setView(initialView); if (initialView === "create") setSelectedEvent(null); } - }, [initialView]); + }, [initialView, location.search]); const handleViewProfessional = (professional: Professional) => { setViewingProfessional(professional); @@ -362,6 +435,20 @@ export const Dashboard: React.FC = ({ } }; + useEffect(() => { + const fetchEventDailyInvitations = async () => { + if (isTeamModalOpen && selectedEvent && selectedEvent.date) { + try { + const res = await getDailyInvitationsByDate(selectedEvent.date.split('T')[0]); + setEventDailyInvitations(res || []); + } catch(err) { + console.error("Erro ao buscar convites do dia do evento:", err); + } + } + }; + fetchEventDailyInvitations(); + }, [isTeamModalOpen, selectedEvent]); + // Função para fechar modal de equipe e limpar filtros const closeTeamModal = () => { setIsTeamModalOpen(false); @@ -369,6 +456,7 @@ export const Dashboard: React.FC = ({ setTeamRoleFilter("all"); setTeamStatusFilter("all"); setTeamAvailabilityFilter("all"); + setShowOnlyDailyAccepted(false); }; const [processingIds, setProcessingIds] = useState>(new Set()); @@ -454,6 +542,12 @@ export const Dashboard: React.FC = ({ } } + // Filtro "Confirmados pro Plantão" Exclusivo + if (showOnlyDailyAccepted) { + const hasAcceptedDaily = eventDailyInvitations.some(inv => inv.profissional_id === professional.id && inv.status === 'ACEITO'); + if (!hasAcceptedDaily) return false; + } + return true; }); }, [ @@ -464,6 +558,8 @@ export const Dashboard: React.FC = ({ teamRoleFilter, teamStatusFilter, teamAvailabilityFilter, + eventDailyInvitations, + showOnlyDailyAccepted, busyProfessionalIds // Depende apenas do Set otimizado ]); @@ -827,7 +923,7 @@ export const Dashboard: React.FC = ({
{/* Header */} - {view === "list" && !new URLSearchParams(window.location.search).get("eventId") && ( + {view === "list" && !new URLSearchParams(location.search).get("eventId") && (
{renderRoleSpecificHeader()} {renderRoleSpecificActions()} @@ -835,8 +931,80 @@ export const Dashboard: React.FC = ({ )} {/* Content Switcher */} - {view === "list" && !new URLSearchParams(window.location.search).get("eventId") && ( + {view === "list" && !new URLSearchParams(location.search).get("eventId") && (
+ {user.role === UserRole.PHOTOGRAPHER && dailyInvitations.some(c => c.status === 'PENDENTE') && ( +
+
+

+ Convites Pendentes +

+

Você foi selecionado para atuar nestas datas. Confirme sua disponibilidade.

+
+
+ {dailyInvitations.filter(c => c.status === 'PENDENTE').map(inv => ( +
+
+
+ {inv.data ? inv.data.split('-')[2] : '--'} +
+
+

{inv.data ? inv.data.split('-').reverse().join('/') : ''}

+

Plantão Diário

+
+
+
+ + +
+
+ ))} +
+
+ )} + + {user.role === UserRole.PHOTOGRAPHER && dailyInvitations.some(c => c.status === 'ACEITO') && ( +
+
+

+ Datas Confirmadas +

+

Sua presença está confirmada nestas datas. Aguarde a alocação num evento específico e o envio do briefing.

+
+
+ {dailyInvitations.filter(c => c.status === 'ACEITO').map(inv => ( +
+
+
+ {inv.data ? inv.data.split('-')[2] : '--'} +
+
+

{inv.data ? inv.data.split('-').reverse().join('/') : ''}

+

Aguardando Definição de Local Turma

+
+
+
+ + Agendado + +
+
+ ))} +
+
+ )} + {/* Search Bar */}
@@ -899,8 +1067,7 @@ export const Dashboard: React.FC = ({ { - setSelectedEvent(event); - setView("details"); + navigate(`?eventId=${event.id}`); }} onApprove={handleApprove} onReject={handleReject} @@ -926,7 +1093,7 @@ export const Dashboard: React.FC = ({ {/* Loading State for Deep Link */ } - {(!!new URLSearchParams(window.location.search).get("eventId") && (!selectedEvent || view !== "details")) && ( + {(!!new URLSearchParams(location.search).get("eventId") && !selectedEvent) && (

Carregando detalhes do evento...

@@ -937,7 +1104,12 @@ export const Dashboard: React.FC = ({
)}
-
-

- {photographer.name || photographer.nome} -

-

- ID: {photographer.id.substring(0, 8)}... -

+
+
+

+ {photographer.name || photographer.nome} +

+ {photographer.carro_disponivel && ( + + )} +
+
+

+ ID: {photographer.id.substring(0, 8)}... +

+ {photographer.cidade && ( + + {photographer.cidade} + + )} +
@@ -2070,9 +2276,21 @@ export const Dashboard: React.FC = ({
)}
-
-

{photographer.name || photographer.nome}

-

{assignment?.status === "PENDENTE" ? "Convite Pendente" : assignment?.status}

+
+
+

{photographer.name || photographer.nome}

+ {photographer.carro_disponivel && ( + + )} +
+
+

{assignment?.status === "PENDENTE" ? "Convite Pendente" : assignment?.status}

+ {photographer.cidade && ( + + {photographer.cidade} + + )} +
diff --git a/frontend/pages/Finance.tsx b/frontend/pages/Finance.tsx index 23abb1a..21fec19 100644 --- a/frontend/pages/Finance.tsx +++ b/frontend/pages/Finance.tsx @@ -11,6 +11,8 @@ import { Upload, } from "lucide-react"; import { useNavigate } from "react-router-dom"; +import ExcelJS from 'exceljs'; +import { saveAs } from 'file-saver'; interface FinancialTransaction { id: string; @@ -302,7 +304,17 @@ const Finance: React.FC = () => { // Default sort is grouped by FOT if (!sortConfig) { return result.sort((a, b) => { - // Group by FOT (String comparison to handle "20000MG") + // Group by Professional Name if Date Filters are active + if (dateFilters.startDate || dateFilters.endDate) { + const nameA = String(a.nome || "").toLowerCase(); + const nameB = String(b.nome || "").toLowerCase(); + if (nameA !== nameB) return nameA.localeCompare(nameB); + + // Secondary sort by date within the same professional + return new Date(a.dataRaw || a.data).getTime() - new Date(b.dataRaw || b.data).getTime(); + } + + // Default Group by FOT (String comparison to handle "20000MG") const fotA = String(a.fot || ""); const fotB = String(b.fot || ""); if (fotA !== fotB) return fotB.localeCompare(fotA, undefined, { numeric: true }); @@ -815,58 +827,232 @@ const Finance: React.FC = () => { }, [formData.tipoEvento, formData.tipoServico, formData.tabelaFree]); - const handleExportCSV = () => { + const handleExportExcel = async () => { if (sortedTransactions.length === 0) { alert("Nenhum dado para exportar."); return; } - // CSV Header - const headers = [ - "FOT", "Data", "Evento", "Serviço", "Nome", "WhatsApp", "CPF", - "Curso", "Instituição", "Ano", "Empresa", - "Valor Free", "Valor Extra", "Total Pagar", "Data Pagamento", "Pgto OK" + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet("Extrato Financeiro"); + + // Columns matching the legacy Excel sheet + worksheet.columns = [ + { header: "", key: "fot", width: 10 }, + { header: "", key: "data", width: 12 }, + { header: "", key: "curso", width: 20 }, + { header: "", key: "instituicao", width: 20 }, + { header: "", key: "ano", width: 10 }, + { header: "", key: "empresa", width: 15 }, + { header: "", key: "evento", width: 15 }, + { header: "", key: "servico", width: 15 }, + { header: "", key: "nome", width: 25 }, + { header: "", key: "whatsapp", width: 15 }, + { header: "", key: "cpf", width: 16 }, + { header: "", key: "tabelaFree", width: 15 }, + { header: "", key: "valorFree", width: 15 }, + { header: "", key: "valorExtra", width: 15 }, + { header: "", key: "descricaoExtra", width: 40 }, + { header: "", key: "totalPagar", width: 15 }, + { header: "", key: "dataPgto", width: 12 }, + { header: "", key: "pgtoOk", width: 10 }, ]; - // CSV Rows - const rows = sortedTransactions.map(t => [ - t.fot, - t.data, - t.tipoEvento, - t.tipoServico, - `"${t.nome}"`, // Quote names to handle commas - t.whatsapp ? `="${t.whatsapp}"` : "", // Force string in Excel to avoid scientific notation - t.cpf ? `="${t.cpf}"` : "", - `"${t.curso}"`, - `"${t.instituicao}"`, - t.anoFormatura, - `"${t.empresa}"`, - t.valorFree.toFixed(2).replace('.', ','), - t.valorExtra.toFixed(2).replace('.', ','), - t.totalPagar.toFixed(2).replace('.', ','), - t.dataPgto, - t.pgtoOk ? "Sim" : "Não" + // Standard Headers (Row 1 now) + const headerRow = worksheet.addRow([ + "FOT", "Data", "Curso", "Instituição", "Ano Format.", "Empresa", "Tipo Evento", "Tipo de Serviço", + "Nome", "WhatsApp", "CPF", "Tabela Free", "Valor Free", "Valor Extra", + "Descrição do Extra", "Total a Pagar", "Data Pgto", "Pgto OK" ]); - // Summation Row + // Header Styling (Red #FF0000 based on screenshot 3) + headerRow.eachCell((cell, colNumber) => { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFFF0000' } // Red Header + }; + cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true }; + + // Even the header in the screenshot has Yellow for Total? The screenshot 3 shows pure blue/grey for table headers, let's use the standard blue that matches their columns + // Looking at screenshot 3, the columns header is actually a light blue! "A4:R4" + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FF9BC2E6' } // Light Blue from standard excel + }; + cell.font = { bold: true, color: { argb: 'FF000000' }, size: 10 }; // Black text on blue + + if (worksheet.getColumn(colNumber).key === 'totalPagar') { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFFFFF00' } + }; + } + }); + + let currentName = ""; + let groupSum = 0; + + const applyRowColor = (row: ExcelJS.Row) => { + for (let colNumber = 1; colNumber <= 18; colNumber++) { + const cell = row.getCell(colNumber); + const colKey = worksheet.getColumn(colNumber).key as string; + + cell.border = { + top: {style:'thin'}, + left: {style:'thin'}, + bottom: {style:'thin'}, + right: {style:'thin'} + }; + cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true }; + + if (colKey === 'descricaoExtra' || colKey === 'nome') { + cell.alignment = { vertical: 'middle', horizontal: 'left', wrapText: true }; + } + + if (colKey === 'totalPagar') { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFFFFF00' } // Yellow column + }; + } + if (["valorFree", "valorExtra", "totalPagar"].includes(colKey)) { + cell.alignment = { vertical: 'middle', horizontal: 'right', wrapText: true }; + cell.numFmt = '"R$" #,##0.00'; + } + } + }; + + if (dateFilters.startDate || dateFilters.endDate) { + sortedTransactions.forEach((t, i) => { + const tName = t.nome || "Sem Nome"; + + if (currentName !== "" && tName !== currentName) { + // Subtotal Row + const subRow = worksheet.addRow({ + descricaoExtra: `SUBTOTAL ${currentName}`, + totalPagar: groupSum + }); + subRow.font = { bold: true }; + for (let colNumber = 1; colNumber <= 18; colNumber++) { + const cell = subRow.getCell(colNumber); + const colKey = worksheet.getColumn(colNumber).key as string; + + cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF00' } }; // Entire row yellow! + cell.border = { top: {style:'thin'}, left: {style:'thin'}, bottom: {style:'thin'}, right: {style:'thin'} }; + cell.alignment = { vertical: 'middle', horizontal: 'right' }; + if (["valorFree", "valorExtra", "totalPagar"].includes(colKey)) { + cell.numFmt = '"R$" #,##0.00'; + } + } + + groupSum = 0; + } + if (currentName === "") { + currentName = tName; + } + currentName = tName; + groupSum += t.totalPagar; + + const row = worksheet.addRow({ + fot: t.fot, + data: t.data, + curso: t.curso, + instituicao: t.instituicao, + ano: t.anoFormatura, + empresa: t.empresa, + evento: t.tipoEvento, + servico: t.tipoServico, + nome: t.nome, + whatsapp: t.whatsapp, + cpf: t.cpf, + tabelaFree: t.tabelaFree, + valorFree: t.valorFree, + valorExtra: t.valorExtra, + descricaoExtra: t.descricaoExtra, + totalPagar: t.totalPagar, + dataPgto: t.dataPgto, + pgtoOk: t.pgtoOk ? "Sim" : "Não" + }); + applyRowColor(row); + + // Final subtotal + if (i === sortedTransactions.length - 1) { + const subRow = worksheet.addRow({ + descricaoExtra: `SUBTOTAL ${currentName}`, + totalPagar: groupSum + }); + subRow.font = { bold: true }; + for (let colNumber = 1; colNumber <= 18; colNumber++) { + const cell = subRow.getCell(colNumber); + const colKey = worksheet.getColumn(colNumber).key as string; + + cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF00' } }; + cell.border = { top: {style:'thin'}, left: {style:'thin'}, bottom: {style:'thin'}, right: {style:'thin'} }; + cell.alignment = { vertical: 'middle', horizontal: 'right' }; + if (["valorFree", "valorExtra", "totalPagar"].includes(colKey)) { + cell.numFmt = '"R$" #,##0.00'; + } + } + } + }); + } else { + // Flat standard export without grouped subtotals if not filtering dates + sortedTransactions.forEach(t => { + const row = worksheet.addRow({ + fot: t.fot, + data: t.data, + evento: t.tipoEvento, + servico: t.tipoServico, + nome: t.nome, + whatsapp: t.whatsapp, + cpf: t.cpf, + curso: t.curso, + instituicao: t.instituicao, + ano: t.anoFormatura, + empresa: t.empresa, + tabelaFree: t.tabelaFree, + valorFree: t.valorFree, + valorExtra: t.valorExtra, + descricaoExtra: t.descricaoExtra, + totalPagar: t.totalPagar, + dataPgto: t.dataPgto, + pgtoOk: t.pgtoOk ? "Sim" : "Não" + }); + applyRowColor(row); + }); + } + + // Global Total Row const totalValue = sortedTransactions.reduce((sum, t) => sum + t.totalPagar, 0); - const sumRow = [ - "TOTAL", "", "", "", "", "", "", "", "", "", "", "", "", - totalValue.toFixed(2).replace('.', ','), "", "" - ]; + const sumRow = worksheet.addRow({ + descricaoExtra: "TOTAL GERAL", + totalPagar: totalValue + }); + sumRow.font = { bold: true, size: 12 }; + for (let colNumber = 1; colNumber <= 18; colNumber++) { + const cell = sumRow.getCell(colNumber); + const colKey = worksheet.getColumn(colNumber).key as string; - // Combine - const csvContent = [ - headers.join(";"), - ...rows.map(r => r.join(";")), - sumRow.join(";") - ].join("\n"); + if (colKey === 'totalPagar' || colKey === 'descricaoExtra') { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFFFFF00' } // Yellow + }; + if (colKey === 'totalPagar') { + cell.alignment = { vertical: 'middle', horizontal: 'right' }; + cell.numFmt = '"R$" #,##0.00'; + } else { + cell.alignment = { vertical: 'middle', horizontal: 'right' }; + } + } + } - // Create Blob with BOM for Excel UTF-8 support - const blob = new Blob(["\uFEFF" + csvContent], { type: "text/csv;charset=utf-8;" }); - const url = URL.createObjectURL(blob); - - // Dynamic Filename + // Save let filename = "extrato_financeiro"; if (filters.nome) { filename += `_${filters.nome.trim().replace(/\s+/g, '_').toLowerCase()}`; @@ -883,21 +1069,15 @@ const Finance: React.FC = () => { if (dateFilters.endDate) { filename += `_ate_${formatDateFilename(dateFilters.endDate)}`; } - // Fallback or just append timestamp if no specific filters? - // Let's always append a short date/time or just date if no filters to avoid overwrites, - // but user asked for specific format. Let's stick to user request + date fallback if empty. if (!filters.nome && !dateFilters.startDate && !dateFilters.endDate) { - const today = new Date().toLocaleDateString("pt-BR").split("/").join("-"); // DD-MM-YYYY + const today = new Date().toLocaleDateString("pt-BR").split("/").join("-"); filename += `_${today}`; } - filename += ".csv"; + filename += ".xlsx"; - const link = document.createElement("a"); - link.href = url; - link.setAttribute("download", filename); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }); + saveAs(blob, filename); }; // Calculate Total for Display @@ -913,7 +1093,7 @@ const Finance: React.FC = () => {
-
-
- - Filtros: +
+
+
+ + Filtros: +
+ + + +
- - - +
+
Status Refinado:
+
+ +
+ setDateFilter(e.target.value)} + title="Selecione um dia para ver apenas os aceitos" + /> + {dateFilter && ( + + )} +
+
+
diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index 9f401e7..5788e0a 100644 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -1502,3 +1502,119 @@ export const setCoordinator = async (token: string, eventId: string, professiona return { error: "Network error" }; } }; + +// --------------- LOGISTICS DAILY INVITATIONS --------------- // + +export async function getAvailableProfessionalsLogistics(date: string, page: number = 1, limit: number = 50, search: string = "") { + try { + const region = localStorage.getItem("photum_selected_region") || "SP"; + const query = new URLSearchParams({ + data: date, + page: page.toString(), + limit: limit.toString(), + search: search + }); + + const response = await apiFetch(`${API_BASE_URL}/api/logistica/disponiveis?${query.toString()}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "x-regiao": region, + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error(`Error fetching daily professionals:`, error); + return { data: [], total: 0, page: 1, total_pages: 1 }; + } +} + +export async function createDailyInvitation(profissionalId: string, data: string) { + try { + const region = localStorage.getItem("photum_selected_region") || "SP"; + const response = await apiFetch(`${API_BASE_URL}/api/logistica/convites`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-regiao": region, + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + body: JSON.stringify({ profissional_id: profissionalId, data: data }), + }); + + if (!response.ok) { + const errData = await response.json().catch(() => ({})); + throw new Error(errData.error || `Failed to create invitation (status ${response.status})`); + } + + return await response.json(); + } catch (error) { + console.error(`Error creating daily invitation:`, error); + throw error; + } +} + +export async function getDailyInvitationsProfessional(profId: string) { + try { + const region = localStorage.getItem("photum_selected_region") || "SP"; + const response = await apiFetch(`${API_BASE_URL}/api/logistica/convites?prof_id=${profId}`, { + method: "GET", + headers: { + "x-regiao": region, + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }); + + if (!response.ok) throw new Error("Failed to fetch daily invitations"); + return await response.json(); + } catch (error) { + console.error(`Error fetching daily invitations:`, error); + return []; + } +} + +export async function respondDailyInvitation(invitationId: string, status: "ACEITO" | "REJEITADO", reason?: string) { + try { + const region = localStorage.getItem("photum_selected_region") || "SP"; + const response = await apiFetch(`${API_BASE_URL}/api/logistica/convites/${invitationId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "x-regiao": region, + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + body: JSON.stringify({ status, motivo_rejeicao: reason || "" }), + }); + + if (!response.ok) throw new Error("Failed to respond to invitation"); + return await response.json(); + } catch (error) { + console.error(`Error responding daily invitation:`, error); + throw error; + } +} + +export async function getDailyInvitationsByDate(date: string) { + try { + const region = localStorage.getItem("photum_selected_region") || "SP"; + const response = await apiFetch(`${API_BASE_URL}/api/logistica/convites-por-data?data=${date}`, { + method: "GET", + headers: { + "x-regiao": region, + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }); + + if (!response.ok) throw new Error("Failed to fetch daily invitations by date"); + return await response.json(); + } catch (error) { + console.error(`Error fetching daily invitations by date:`, error); + return []; + } +}