feat(financeiro): implementação do extrato financeiro do profissional e melhorias na agenda
- Backend: - Adicionado endpoint para extrato financeiro do profissional (/meus-pagamentos). - Atualizada query SQL para incluir nome da empresa e curso nos detalhes da transação. - Adicionado retorno de valores (Free, Extra, Descrição) na API. - Frontend: - Nova página "Meus Pagamentos" com modal de detalhes da transação. - Removido componente antigo PhotographerFinance. - Ajustado filtro de motoristas na Logística para exibir apenas profissionais atribuídos e com carro. - Corrigida exibição da função do profissional na Escala (mostra a função atribuída no evento, ex: Cinegrafista). - Melhoria no botão de voltar na tela de detalhes do evento.
This commit is contained in:
parent
175ee98f2a
commit
943b4f6506
16 changed files with 602 additions and 414 deletions
|
|
@ -167,6 +167,8 @@ func main() {
|
||||||
profGroup.GET("/:id", profissionaisHandler.Get)
|
profGroup.GET("/:id", profissionaisHandler.Get)
|
||||||
profGroup.PUT("/:id", profissionaisHandler.Update)
|
profGroup.PUT("/:id", profissionaisHandler.Update)
|
||||||
profGroup.DELETE("/:id", profissionaisHandler.Delete)
|
profGroup.DELETE("/:id", profissionaisHandler.Delete)
|
||||||
|
// Rota de extrato financeiro (usando agendaHandler por conveniência de serviço)
|
||||||
|
profGroup.GET("/me/financial-statement", agendaHandler.GetProfessionalFinancialStatement)
|
||||||
}
|
}
|
||||||
|
|
||||||
funcoesGroup := api.Group("/funcoes")
|
funcoesGroup := api.Group("/funcoes")
|
||||||
|
|
|
||||||
|
|
@ -389,3 +389,30 @@ func (h *Handler) ListAvailableProfessionals(c *gin.Context) {
|
||||||
|
|
||||||
c.JSON(http.StatusOK, profs)
|
c.JSON(http.StatusOK, profs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetProfessionalFinancialStatement godoc
|
||||||
|
// @Summary Get professional financial statement
|
||||||
|
// @Description Get financial statement for the logged-in professional
|
||||||
|
// @Tags agenda
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} FinancialStatementResponse
|
||||||
|
// @Failure 401 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/professionals/me/financial-statement [get]
|
||||||
|
func (h *Handler) GetProfessionalFinancialStatement(c *gin.Context) {
|
||||||
|
userIDStr := c.GetString("userID")
|
||||||
|
userID, err := uuid.Parse(userIDStr)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Usuário não autenticado"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statement, err := h.service.GetProfessionalFinancialStatement(c.Request.Context(), userID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erro ao buscar extrato: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, statement)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -587,3 +587,124 @@ func (s *Service) UpdateAssignmentPosition(ctx context.Context, agendaID, profes
|
||||||
func (s *Service) ListAvailableProfessionals(ctx context.Context, date time.Time) ([]generated.ListAvailableProfessionalsForDateRow, error) {
|
func (s *Service) ListAvailableProfessionals(ctx context.Context, date time.Time) ([]generated.ListAvailableProfessionalsForDateRow, error) {
|
||||||
return s.queries.ListAvailableProfessionalsForDate(ctx, pgtype.Date{Time: date, Valid: true})
|
return s.queries.ListAvailableProfessionalsForDate(ctx, pgtype.Date{Time: date, Valid: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FinancialStatementResponse struct {
|
||||||
|
TotalRecebido float64 `json:"total_recebido"`
|
||||||
|
PagamentosConfirmados float64 `json:"pagamentos_confirmados"`
|
||||||
|
PagamentosPendentes float64 `json:"pagamentos_pendentes"`
|
||||||
|
Transactions []FinancialTransactionDTO `json:"transactions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FinancialTransactionDTO struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
DataEvento string `json:"data_evento"`
|
||||||
|
NomeEvento string `json:"nome_evento"` // Fot + Tipo
|
||||||
|
TipoEvento string `json:"tipo_evento"`
|
||||||
|
Empresa string `json:"empresa"`
|
||||||
|
ValorRecebido float64 `json:"valor_recebido"`
|
||||||
|
ValorFree float64 `json:"valor_free"`
|
||||||
|
ValorExtra float64 `json:"valor_extra"`
|
||||||
|
DescricaoExtra string `json:"descricao_extra"`
|
||||||
|
DataPagamento string `json:"data_pagamento"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetProfessionalFinancialStatement(ctx context.Context, userID uuid.UUID) (*FinancialStatementResponse, error) {
|
||||||
|
// 1. Identificar o profissional logado
|
||||||
|
prof, err := s.queries.GetProfissionalByUsuarioID(ctx, pgtype.UUID{Bytes: userID, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
return &FinancialStatementResponse{}, fmt.Errorf("profissional não encontrado para este usuário")
|
||||||
|
}
|
||||||
|
|
||||||
|
profID := pgtype.UUID{Bytes: prof.ID.Bytes, Valid: true}
|
||||||
|
cpf := prof.CpfCnpjTitular.String // Fallback para registros antigos
|
||||||
|
|
||||||
|
// 2. Buscar Transações
|
||||||
|
rawTransactions, err := s.queries.ListTransactionsByProfessional(ctx, generated.ListTransactionsByProfessionalParams{
|
||||||
|
ProfissionalID: profID,
|
||||||
|
Column2: cpf,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// Se não houver transações, retorna vazio sem erro
|
||||||
|
return &FinancialStatementResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Processar e Somar
|
||||||
|
var response FinancialStatementResponse
|
||||||
|
var dtoList []FinancialTransactionDTO
|
||||||
|
|
||||||
|
for _, t := range rawTransactions {
|
||||||
|
// Validar valores
|
||||||
|
valor := 0.0
|
||||||
|
valTotal, _ := t.TotalPagar.Float64Value() // pgtype.Numeric
|
||||||
|
if valTotal.Valid {
|
||||||
|
valor = valTotal.Float64
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("DEBUG Transaction %v: Total=%.2f Free=%.2f Extra=%.2f Desc=%s\n",
|
||||||
|
t.ID, valor, getFloat64(t.ValorFree), getFloat64(t.ValorExtra), t.DescricaoExtra.String)
|
||||||
|
|
||||||
|
// Status e Somatórios
|
||||||
|
status := "Pendente"
|
||||||
|
if t.PgtoOk.Valid && t.PgtoOk.Bool {
|
||||||
|
status = "Pago"
|
||||||
|
response.TotalRecebido += valor
|
||||||
|
response.PagamentosConfirmados += valor // Assumindo Recebido = Confirmado neste contexto
|
||||||
|
} else {
|
||||||
|
response.PagamentosPendentes += valor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatar Dados
|
||||||
|
dataEvento := ""
|
||||||
|
if t.DataCobranca.Valid {
|
||||||
|
dataEvento = t.DataCobranca.Time.Format("02/01/2006")
|
||||||
|
}
|
||||||
|
|
||||||
|
dtPagamento := "-"
|
||||||
|
if t.DataPagamento.Valid {
|
||||||
|
dtPagamento = t.DataPagamento.Time.Format("02/01/2006")
|
||||||
|
}
|
||||||
|
|
||||||
|
empresa := "-"
|
||||||
|
if t.EmpresaNome.Valid {
|
||||||
|
empresa = t.EmpresaNome.String
|
||||||
|
}
|
||||||
|
|
||||||
|
nomeEvento := ""
|
||||||
|
if t.FotNumero.Valid {
|
||||||
|
if t.CursoNome.Valid {
|
||||||
|
nomeEvento = fmt.Sprintf("Formatura %s (FOT %d)", t.CursoNome.String, t.FotNumero.Int32)
|
||||||
|
} else {
|
||||||
|
nomeEvento = fmt.Sprintf("Formatura FOT %d", t.FotNumero.Int32)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nomeEvento = t.TipoEvento.String
|
||||||
|
}
|
||||||
|
|
||||||
|
dto := FinancialTransactionDTO{
|
||||||
|
ID: uuid.UUID(t.ID.Bytes),
|
||||||
|
DataEvento: dataEvento,
|
||||||
|
NomeEvento: nomeEvento,
|
||||||
|
TipoEvento: t.TipoEvento.String,
|
||||||
|
Empresa: empresa,
|
||||||
|
ValorRecebido: valor,
|
||||||
|
ValorFree: getFloat64(t.ValorFree),
|
||||||
|
ValorExtra: getFloat64(t.ValorExtra),
|
||||||
|
DescricaoExtra: t.DescricaoExtra.String,
|
||||||
|
DataPagamento: dtPagamento,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
dtoList = append(dtoList, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Transactions = dtoList
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFloat64(n pgtype.Numeric) float64 {
|
||||||
|
v, _ := n.Float64Value()
|
||||||
|
if v.Valid {
|
||||||
|
return v.Float64
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ INSERT INTO financial_transactions (
|
||||||
total_pagar, data_pagamento, pgto_ok
|
total_pagar, data_pagamento, pgto_ok
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
|
||||||
) RETURNING id, fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name, whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra, total_pagar, data_pagamento, pgto_ok, criado_em, atualizado_em
|
) RETURNING id, fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name, whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra, total_pagar, data_pagamento, pgto_ok, criado_em, atualizado_em, profissional_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateTransactionParams struct {
|
type CreateTransactionParams struct {
|
||||||
|
|
@ -74,6 +74,7 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa
|
||||||
&i.PgtoOk,
|
&i.PgtoOk,
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
|
&i.ProfissionalID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +89,7 @@ func (q *Queries) DeleteTransaction(ctx context.Context, id pgtype.UUID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTransaction = `-- name: GetTransaction :one
|
const getTransaction = `-- name: GetTransaction :one
|
||||||
SELECT id, fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name, whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra, total_pagar, data_pagamento, pgto_ok, criado_em, atualizado_em FROM financial_transactions WHERE id = $1
|
SELECT id, fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name, whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra, total_pagar, data_pagamento, pgto_ok, criado_em, atualizado_em, profissional_id FROM financial_transactions WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetTransaction(ctx context.Context, id pgtype.UUID) (FinancialTransaction, error) {
|
func (q *Queries) GetTransaction(ctx context.Context, id pgtype.UUID) (FinancialTransaction, error) {
|
||||||
|
|
@ -112,12 +113,13 @@ func (q *Queries) GetTransaction(ctx context.Context, id pgtype.UUID) (Financial
|
||||||
&i.PgtoOk,
|
&i.PgtoOk,
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
|
&i.ProfissionalID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const listTransactions = `-- name: ListTransactions :many
|
const listTransactions = `-- name: ListTransactions :many
|
||||||
SELECT t.id, t.fot_id, t.data_cobranca, t.tipo_evento, t.tipo_servico, t.professional_name, t.whatsapp, t.cpf, t.tabela_free, t.valor_free, t.valor_extra, t.descricao_extra, t.total_pagar, t.data_pagamento, t.pgto_ok, t.criado_em, t.atualizado_em, f.fot as fot_numero
|
SELECT t.id, t.fot_id, t.data_cobranca, t.tipo_evento, t.tipo_servico, t.professional_name, t.whatsapp, t.cpf, t.tabela_free, t.valor_free, t.valor_extra, t.descricao_extra, t.total_pagar, t.data_pagamento, t.pgto_ok, t.criado_em, t.atualizado_em, t.profissional_id, f.fot as fot_numero
|
||||||
FROM financial_transactions t
|
FROM financial_transactions t
|
||||||
LEFT JOIN cadastro_fot f ON t.fot_id = f.id
|
LEFT JOIN cadastro_fot f ON t.fot_id = f.id
|
||||||
ORDER BY t.data_cobranca DESC
|
ORDER BY t.data_cobranca DESC
|
||||||
|
|
@ -141,6 +143,7 @@ type ListTransactionsRow struct {
|
||||||
PgtoOk pgtype.Bool `json:"pgto_ok"`
|
PgtoOk pgtype.Bool `json:"pgto_ok"`
|
||||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||||
|
ProfissionalID pgtype.UUID `json:"profissional_id"`
|
||||||
FotNumero pgtype.Int4 `json:"fot_numero"`
|
FotNumero pgtype.Int4 `json:"fot_numero"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,6 +174,7 @@ func (q *Queries) ListTransactions(ctx context.Context) ([]ListTransactionsRow,
|
||||||
&i.PgtoOk,
|
&i.PgtoOk,
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
|
&i.ProfissionalID,
|
||||||
&i.FotNumero,
|
&i.FotNumero,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -184,7 +188,7 @@ func (q *Queries) ListTransactions(ctx context.Context) ([]ListTransactionsRow,
|
||||||
}
|
}
|
||||||
|
|
||||||
const listTransactionsByFot = `-- name: ListTransactionsByFot :many
|
const listTransactionsByFot = `-- name: ListTransactionsByFot :many
|
||||||
SELECT id, fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name, whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra, total_pagar, data_pagamento, pgto_ok, criado_em, atualizado_em FROM financial_transactions
|
SELECT id, fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name, whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra, total_pagar, data_pagamento, pgto_ok, criado_em, atualizado_em, profissional_id FROM financial_transactions
|
||||||
WHERE fot_id = $1
|
WHERE fot_id = $1
|
||||||
ORDER BY data_cobranca DESC
|
ORDER BY data_cobranca DESC
|
||||||
`
|
`
|
||||||
|
|
@ -216,6 +220,93 @@ func (q *Queries) ListTransactionsByFot(ctx context.Context, fotID pgtype.UUID)
|
||||||
&i.PgtoOk,
|
&i.PgtoOk,
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
|
&i.ProfissionalID,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listTransactionsByProfessional = `-- name: ListTransactionsByProfessional :many
|
||||||
|
SELECT t.id, t.fot_id, t.data_cobranca, t.tipo_evento, t.tipo_servico, t.professional_name, t.whatsapp, t.cpf, t.tabela_free, t.valor_free, t.valor_extra, t.descricao_extra, t.total_pagar, t.data_pagamento, t.pgto_ok, t.criado_em, t.atualizado_em, t.profissional_id, f.fot as fot_numero, e.nome as empresa_nome, c.nome as curso_nome
|
||||||
|
FROM financial_transactions t
|
||||||
|
LEFT JOIN cadastro_fot f ON t.fot_id = f.id
|
||||||
|
LEFT JOIN empresas e ON f.empresa_id = e.id
|
||||||
|
LEFT JOIN cursos c ON f.curso_id = c.id
|
||||||
|
WHERE
|
||||||
|
t.profissional_id = $1
|
||||||
|
OR (
|
||||||
|
$2::text <> '' AND
|
||||||
|
REGEXP_REPLACE(t.cpf, '\D', '', 'g') = REGEXP_REPLACE($2, '\D', '', 'g')
|
||||||
|
)
|
||||||
|
ORDER BY t.data_cobranca DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListTransactionsByProfessionalParams struct {
|
||||||
|
ProfissionalID pgtype.UUID `json:"profissional_id"`
|
||||||
|
Column2 string `json:"column_2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListTransactionsByProfessionalRow struct {
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
|
FotID pgtype.UUID `json:"fot_id"`
|
||||||
|
DataCobranca pgtype.Date `json:"data_cobranca"`
|
||||||
|
TipoEvento pgtype.Text `json:"tipo_evento"`
|
||||||
|
TipoServico pgtype.Text `json:"tipo_servico"`
|
||||||
|
ProfessionalName pgtype.Text `json:"professional_name"`
|
||||||
|
Whatsapp pgtype.Text `json:"whatsapp"`
|
||||||
|
Cpf pgtype.Text `json:"cpf"`
|
||||||
|
TabelaFree pgtype.Text `json:"tabela_free"`
|
||||||
|
ValorFree pgtype.Numeric `json:"valor_free"`
|
||||||
|
ValorExtra pgtype.Numeric `json:"valor_extra"`
|
||||||
|
DescricaoExtra pgtype.Text `json:"descricao_extra"`
|
||||||
|
TotalPagar pgtype.Numeric `json:"total_pagar"`
|
||||||
|
DataPagamento pgtype.Date `json:"data_pagamento"`
|
||||||
|
PgtoOk pgtype.Bool `json:"pgto_ok"`
|
||||||
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
|
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||||
|
ProfissionalID pgtype.UUID `json:"profissional_id"`
|
||||||
|
FotNumero pgtype.Int4 `json:"fot_numero"`
|
||||||
|
EmpresaNome pgtype.Text `json:"empresa_nome"`
|
||||||
|
CursoNome pgtype.Text `json:"curso_nome"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListTransactionsByProfessional(ctx context.Context, arg ListTransactionsByProfessionalParams) ([]ListTransactionsByProfessionalRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listTransactionsByProfessional, arg.ProfissionalID, arg.Column2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListTransactionsByProfessionalRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListTransactionsByProfessionalRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.FotID,
|
||||||
|
&i.DataCobranca,
|
||||||
|
&i.TipoEvento,
|
||||||
|
&i.TipoServico,
|
||||||
|
&i.ProfessionalName,
|
||||||
|
&i.Whatsapp,
|
||||||
|
&i.Cpf,
|
||||||
|
&i.TabelaFree,
|
||||||
|
&i.ValorFree,
|
||||||
|
&i.ValorExtra,
|
||||||
|
&i.DescricaoExtra,
|
||||||
|
&i.TotalPagar,
|
||||||
|
&i.DataPagamento,
|
||||||
|
&i.PgtoOk,
|
||||||
|
&i.CriadoEm,
|
||||||
|
&i.AtualizadoEm,
|
||||||
|
&i.ProfissionalID,
|
||||||
|
&i.FotNumero,
|
||||||
|
&i.EmpresaNome,
|
||||||
|
&i.CursoNome,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -248,7 +339,7 @@ UPDATE financial_transactions SET
|
||||||
total_pagar = $13, data_pagamento = $14, pgto_ok = $15,
|
total_pagar = $13, data_pagamento = $14, pgto_ok = $15,
|
||||||
atualizado_em = NOW()
|
atualizado_em = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING id, fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name, whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra, total_pagar, data_pagamento, pgto_ok, criado_em, atualizado_em
|
RETURNING id, fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name, whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra, total_pagar, data_pagamento, pgto_ok, criado_em, atualizado_em, profissional_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateTransactionParams struct {
|
type UpdateTransactionParams struct {
|
||||||
|
|
@ -306,6 +397,7 @@ func (q *Queries) UpdateTransaction(ctx context.Context, arg UpdateTransactionPa
|
||||||
&i.PgtoOk,
|
&i.PgtoOk,
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
|
&i.ProfissionalID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@ type FinancialTransaction struct {
|
||||||
PgtoOk pgtype.Bool `json:"pgto_ok"`
|
PgtoOk pgtype.Bool `json:"pgto_ok"`
|
||||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||||
|
ProfissionalID pgtype.UUID `json:"profissional_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FuncoesProfissionai struct {
|
type FuncoesProfissionai struct {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
-- Add profissional_id column to financial_transactions
|
||||||
|
ALTER TABLE financial_transactions ADD COLUMN IF NOT EXISTS profissional_id UUID REFERENCES cadastro_profissionais(id);
|
||||||
|
|
||||||
|
-- Optional: Try to link existing transactions by CPF (removes non-digits for comparison)
|
||||||
|
UPDATE financial_transactions ft
|
||||||
|
SET profissional_id = cp.id
|
||||||
|
FROM cadastro_profissionais cp
|
||||||
|
WHERE ft.profissional_id IS NULL
|
||||||
|
AND REGEXP_REPLACE(ft.cpf, '\D', '', 'g') = REGEXP_REPLACE(cp.cpf_cnpj_titular, '\D', '', 'g')
|
||||||
|
AND ft.cpf IS NOT NULL
|
||||||
|
AND ft.cpf != '';
|
||||||
|
|
@ -39,3 +39,17 @@ DELETE FROM financial_transactions WHERE id = $1;
|
||||||
-- name: GetTransaction :one
|
-- name: GetTransaction :one
|
||||||
SELECT * FROM financial_transactions WHERE id = $1;
|
SELECT * FROM financial_transactions WHERE id = $1;
|
||||||
|
|
||||||
|
|
||||||
|
-- name: ListTransactionsByProfessional :many
|
||||||
|
SELECT t.*, f.fot as fot_numero, e.nome as empresa_nome, c.nome as curso_nome
|
||||||
|
FROM financial_transactions t
|
||||||
|
LEFT JOIN cadastro_fot f ON t.fot_id = f.id
|
||||||
|
LEFT JOIN empresas e ON f.empresa_id = e.id
|
||||||
|
LEFT JOIN cursos c ON f.curso_id = c.id
|
||||||
|
WHERE
|
||||||
|
t.profissional_id = $1
|
||||||
|
OR (
|
||||||
|
$2::text <> '' AND
|
||||||
|
REGEXP_REPLACE(t.cpf, '\D', '', 'g') = REGEXP_REPLACE($2, '\D', '', 'g')
|
||||||
|
)
|
||||||
|
ORDER BY t.data_cobranca DESC;
|
||||||
|
|
|
||||||
|
|
@ -459,7 +459,8 @@ CREATE TABLE IF NOT EXISTS financial_transactions (
|
||||||
data_pagamento DATE,
|
data_pagamento DATE,
|
||||||
pgto_ok BOOLEAN DEFAULT FALSE,
|
pgto_ok BOOLEAN DEFAULT FALSE,
|
||||||
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
profissional_id UUID REFERENCES cadastro_profissionais(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Migration to ensure funcao_id exists (Workaround for primitive migration system)
|
-- Migration to ensure funcao_id exists (Workaround for primitive migration system)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { ProfessionalRegister } from "./pages/ProfessionalRegister";
|
||||||
import { TeamPage } from "./pages/Team";
|
import { TeamPage } from "./pages/Team";
|
||||||
import EventDetails from "./pages/EventDetails";
|
import EventDetails from "./pages/EventDetails";
|
||||||
import Finance from "./pages/Finance";
|
import Finance from "./pages/Finance";
|
||||||
import PhotographerFinance from "./pages/PhotographerFinance";
|
|
||||||
import { SettingsPage } from "./pages/Settings";
|
import { SettingsPage } from "./pages/Settings";
|
||||||
import { CourseManagement } from "./pages/CourseManagement";
|
import { CourseManagement } from "./pages/CourseManagement";
|
||||||
import { InspirationPage } from "./pages/Inspiration";
|
import { InspirationPage } from "./pages/Inspiration";
|
||||||
|
|
@ -32,6 +32,7 @@ import { verifyAccessCode } from "./services/apiService";
|
||||||
import { Button } from "./components/Button";
|
import { Button } from "./components/Button";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { ShieldAlert } from "lucide-react";
|
import { ShieldAlert } from "lucide-react";
|
||||||
|
import ProfessionalStatement from "./pages/ProfessionalStatement";
|
||||||
|
|
||||||
// Componente de acesso negado
|
// Componente de acesso negado
|
||||||
const AccessDenied: React.FC = () => {
|
const AccessDenied: React.FC = () => {
|
||||||
|
|
@ -684,9 +685,9 @@ const AppContent: React.FC = () => {
|
||||||
<Route
|
<Route
|
||||||
path="/meus-pagamentos"
|
path="/meus-pagamentos"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute allowedRoles={[UserRole.PHOTOGRAPHER]}>
|
<ProtectedRoute allowedRoles={[UserRole.PHOTOGRAPHER, UserRole.SUPERADMIN]}>
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<PhotographerFinance />
|
<ProfessionalStatement />
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,11 @@ const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, assignedProfe
|
||||||
>
|
>
|
||||||
<option value="">Selecione ou deixe vazio...</option>
|
<option value="">Selecione ou deixe vazio...</option>
|
||||||
{professionals
|
{professionals
|
||||||
.filter(p => p.carro_disponivel)
|
.filter(p => {
|
||||||
|
const hasCar = p.carro_disponivel;
|
||||||
|
const isAssigned = !assignedProfessionals || assignedProfessionals.includes(p.id);
|
||||||
|
return hasCar && isAssigned;
|
||||||
|
})
|
||||||
.map(p => (
|
.map(p => (
|
||||||
<option key={p.id} value={p.id}>{p.nomeEventos || p.nome}</option>
|
<option key={p.id} value={p.id}>{p.nomeEventos || p.nome}</option>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,7 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
|
||||||
// FILTER: Only show professionals with a valid role (Function), matching "Equipe" page logic.
|
// FILTER: Only show professionals with a valid role (Function), matching "Equipe" page logic.
|
||||||
let availableProfs = professionals.filter(p => roles.some(r => r.id === p.funcao_profissional_id));
|
let availableProfs = professionals.filter(p => roles.some(r => r.id === p.funcao_profissional_id));
|
||||||
const allowedMap = new Map<string, string>(); // ID -> Status
|
const allowedMap = new Map<string, string>(); // ID -> Status
|
||||||
|
const assignedRoleMap = new Map<string, string>(); // ID -> Role Name
|
||||||
|
|
||||||
if (allowedProfessionals) {
|
if (allowedProfessionals) {
|
||||||
// Normalize allowed list
|
// Normalize allowed list
|
||||||
|
|
@ -165,6 +166,11 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
|
||||||
if (pid && status !== 'REJEITADO' && status !== 'Rejeitado') {
|
if (pid && status !== 'REJEITADO' && status !== 'Rejeitado') {
|
||||||
ids.push(pid);
|
ids.push(pid);
|
||||||
allowedMap.set(pid, status);
|
allowedMap.set(pid, status);
|
||||||
|
|
||||||
|
if (p.funcaoId) {
|
||||||
|
const r = roles.find(role => role.id === p.funcaoId);
|
||||||
|
if (r) assignedRoleMap.set(pid, r.nome);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -208,6 +214,9 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
|
||||||
const status = allowedMap.get(p.id);
|
const status = allowedMap.get(p.id);
|
||||||
const isPending = status !== 'Confirmado' && status !== 'ACEITO';
|
const isPending = status !== 'Confirmado' && status !== 'ACEITO';
|
||||||
const isDisabled = isBusy || isPending;
|
const isDisabled = isBusy || isPending;
|
||||||
|
|
||||||
|
const assignedRole = assignedRoleMap.get(p.id);
|
||||||
|
const displayRole = assignedRole || p.role || "Profissional";
|
||||||
|
|
||||||
let label = "";
|
let label = "";
|
||||||
if (isPending) label = "(Pendente de Aceite)";
|
if (isPending) label = "(Pendente de Aceite)";
|
||||||
|
|
@ -215,7 +224,7 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<option key={p.id} value={p.id} disabled={isDisabled} className={isDisabled ? "text-gray-400" : ""}>
|
<option key={p.id} value={p.id} disabled={isDisabled} className={isDisabled ? "text-gray-400" : ""}>
|
||||||
{p.nome} - {p.role || "Profissional"} {label}
|
{p.nome} - {displayRole} {label}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -270,6 +279,8 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
|
||||||
// Find professional data again to show equipment in list if needed
|
// Find professional data again to show equipment in list if needed
|
||||||
// Ideally backend should return it, but for now we look up in global list if available
|
// Ideally backend should return it, but for now we look up in global list if available
|
||||||
const profData = professionals.find(p => p.id === item.profissional_id);
|
const profData = professionals.find(p => p.id === item.profissional_id);
|
||||||
|
const assignedRole = assignedRoleMap.get(item.profissional_id);
|
||||||
|
const displayRole = assignedRole || profData?.role;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.id} className="flex flex-col p-2 hover:bg-gray-50 rounded border-b">
|
<div key={item.id} className="flex flex-col p-2 hover:bg-gray-50 rounded border-b">
|
||||||
|
|
@ -285,6 +296,7 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-800">
|
<p className="font-medium text-gray-800">
|
||||||
{item.profissional_nome}
|
{item.profissional_nome}
|
||||||
|
{displayRole && <span className="ml-1 text-xs text-gray-500 font-normal">({displayRole})</span>}
|
||||||
{item.phone && <span className="ml-2 text-xs text-gray-500 font-normal">({item.phone})</span>}
|
{item.phone && <span className="ml-2 text-xs text-gray-500 font-normal">({item.phone})</span>}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,11 @@ const EventDetails: React.FC = () => {
|
||||||
<div className="max-w-7xl mx-auto space-y-6">
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button onClick={() => navigate('/eventos')} className="p-2 hover:bg-gray-200 rounded-full transition-colors">
|
<button onClick={() => navigate('/eventos')} className="flex items-center gap-2 px-3 py-2 hover:bg-gray-200 rounded-lg transition-colors text-gray-600">
|
||||||
<ArrowLeft className="w-6 h-6 text-gray-600" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
|
<span className="font-medium">Voltar</span>
|
||||||
</button>
|
</button>
|
||||||
|
<div className="w-px h-8 bg-gray-300 mx-2 hidden sm:block"></div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-800 flex items-center gap-2">
|
<h1 className="text-2xl font-bold text-gray-800 flex items-center gap-2">
|
||||||
{event.empresa_nome} - {event.tipo_evento_nome}
|
{event.empresa_nome} - {event.tipo_evento_nome}
|
||||||
|
|
|
||||||
|
|
@ -1,401 +0,0 @@
|
||||||
import React, { useState, useEffect, useMemo } from "react";
|
|
||||||
import {
|
|
||||||
Download,
|
|
||||||
ArrowUpDown,
|
|
||||||
ArrowUp,
|
|
||||||
ArrowDown,
|
|
||||||
AlertCircle,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
interface PhotographerPayment {
|
|
||||||
id: string;
|
|
||||||
data: string;
|
|
||||||
nomeEvento: string;
|
|
||||||
tipoEvento: string;
|
|
||||||
empresa: string;
|
|
||||||
valorRecebido: number;
|
|
||||||
dataPagamento: string;
|
|
||||||
statusPagamento: "Pago" | "Pendente" | "Atrasado";
|
|
||||||
}
|
|
||||||
|
|
||||||
type SortField = keyof PhotographerPayment | null;
|
|
||||||
type SortDirection = "asc" | "desc" | null;
|
|
||||||
|
|
||||||
const PhotographerFinance: React.FC = () => {
|
|
||||||
const [payments, setPayments] = useState<PhotographerPayment[]>([
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
data: "2025-11-15",
|
|
||||||
nomeEvento: "Formatura Medicina UFPR 2025",
|
|
||||||
tipoEvento: "Formatura",
|
|
||||||
empresa: "PhotoPro Studio",
|
|
||||||
valorRecebido: 1500.0,
|
|
||||||
dataPagamento: "2025-11-20",
|
|
||||||
statusPagamento: "Pago",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
data: "2025-11-18",
|
|
||||||
nomeEvento: "Formatura Direito PUC-PR 2025",
|
|
||||||
tipoEvento: "Formatura",
|
|
||||||
empresa: "PhotoPro Studio",
|
|
||||||
valorRecebido: 1200.0,
|
|
||||||
dataPagamento: "2025-11-25",
|
|
||||||
statusPagamento: "Pago",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
data: "2025-12-01",
|
|
||||||
nomeEvento: "Formatura Engenharia UTFPR 2025",
|
|
||||||
tipoEvento: "Formatura",
|
|
||||||
empresa: "Lens & Art",
|
|
||||||
valorRecebido: 1800.0,
|
|
||||||
dataPagamento: "2025-12-15",
|
|
||||||
statusPagamento: "Pendente",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [sortField, setSortField] = useState<SortField>(null);
|
|
||||||
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
|
|
||||||
const [apiError, setApiError] = useState<string>("");
|
|
||||||
|
|
||||||
// Load API data
|
|
||||||
useEffect(() => {
|
|
||||||
loadApiData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadApiData = async () => {
|
|
||||||
try {
|
|
||||||
// TODO: Implementar chamada real da API
|
|
||||||
// const response = await fetch("http://localhost:3000/api/photographer/payments");
|
|
||||||
// const data = await response.json();
|
|
||||||
// setPayments(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro ao carregar pagamentos:", error);
|
|
||||||
setApiError(
|
|
||||||
"Não foi possível carregar os dados da API. Usando dados de exemplo."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sorting logic
|
|
||||||
const handleSort = (field: keyof PhotographerPayment) => {
|
|
||||||
if (sortField === field) {
|
|
||||||
if (sortDirection === "asc") {
|
|
||||||
setSortDirection("desc");
|
|
||||||
} else if (sortDirection === "desc") {
|
|
||||||
setSortDirection(null);
|
|
||||||
setSortField(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setSortField(field);
|
|
||||||
setSortDirection("asc");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSortIcon = (field: keyof PhotographerPayment) => {
|
|
||||||
if (sortField !== field) {
|
|
||||||
return (
|
|
||||||
<ArrowUpDown
|
|
||||||
size={16}
|
|
||||||
className="opacity-0 group-hover:opacity-50 transition-opacity"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (sortDirection === "asc") {
|
|
||||||
return <ArrowUp size={16} className="text-brand-gold" />;
|
|
||||||
}
|
|
||||||
if (sortDirection === "desc") {
|
|
||||||
return <ArrowDown size={16} className="text-brand-gold" />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ArrowUpDown
|
|
||||||
size={16}
|
|
||||||
className="opacity-0 group-hover:opacity-50 transition-opacity"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortedPayments = useMemo(() => {
|
|
||||||
if (!sortField || !sortDirection) return payments;
|
|
||||||
|
|
||||||
return [...payments].sort((a, b) => {
|
|
||||||
const aValue = a[sortField];
|
|
||||||
const bValue = b[sortField];
|
|
||||||
|
|
||||||
if (aValue === null || aValue === undefined) return 1;
|
|
||||||
if (bValue === null || bValue === undefined) return -1;
|
|
||||||
|
|
||||||
let comparison = 0;
|
|
||||||
if (typeof aValue === "string" && typeof bValue === "string") {
|
|
||||||
comparison = aValue.localeCompare(bValue);
|
|
||||||
} else if (typeof aValue === "number" && typeof bValue === "number") {
|
|
||||||
comparison = aValue - bValue;
|
|
||||||
} else if (typeof aValue === "boolean" && typeof bValue === "boolean") {
|
|
||||||
comparison = aValue === bValue ? 0 : aValue ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sortDirection === "asc" ? comparison : -comparison;
|
|
||||||
});
|
|
||||||
}, [payments, sortField, sortDirection]);
|
|
||||||
|
|
||||||
// Export to CSV
|
|
||||||
const handleExport = () => {
|
|
||||||
const headers = [
|
|
||||||
"Data Evento",
|
|
||||||
"Nome Evento",
|
|
||||||
"Tipo Evento",
|
|
||||||
"Empresa",
|
|
||||||
"Valor Recebido",
|
|
||||||
"Data Pagamento",
|
|
||||||
"Status",
|
|
||||||
];
|
|
||||||
|
|
||||||
const csvContent = [
|
|
||||||
headers.join(","),
|
|
||||||
...sortedPayments.map((p) =>
|
|
||||||
[
|
|
||||||
p.data,
|
|
||||||
`"${p.nomeEvento}"`,
|
|
||||||
p.tipoEvento,
|
|
||||||
p.empresa,
|
|
||||||
p.valorRecebido.toFixed(2),
|
|
||||||
p.dataPagamento,
|
|
||||||
p.statusPagamento,
|
|
||||||
].join(",")
|
|
||||||
),
|
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
|
||||||
const link = document.createElement("a");
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
link.setAttribute("href", url);
|
|
||||||
link.setAttribute("download", `meus_pagamentos_${Date.now()}.csv`);
|
|
||||||
link.style.visibility = "hidden";
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate totals
|
|
||||||
const totalRecebido = sortedPayments.reduce(
|
|
||||||
(sum, p) => sum + p.valorRecebido,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const totalPago = sortedPayments
|
|
||||||
.filter((p) => p.statusPagamento === "Pago")
|
|
||||||
.reduce((sum, p) => sum + p.valorRecebido, 0);
|
|
||||||
const totalPendente = sortedPayments
|
|
||||||
.filter((p) => p.statusPagamento === "Pendente")
|
|
||||||
.reduce((sum, p) => sum + p.valorRecebido, 0);
|
|
||||||
|
|
||||||
const getStatusBadge = (status: string) => {
|
|
||||||
const statusColors = {
|
|
||||||
Pago: "bg-green-100 text-green-800",
|
|
||||||
Pendente: "bg-yellow-100 text-yellow-800",
|
|
||||||
Atrasado: "bg-red-100 text-red-800",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`px-2 py-1 rounded-full text-xs font-medium ${
|
|
||||||
statusColors[status as keyof typeof statusColors] ||
|
|
||||||
"bg-gray-100 text-gray-800"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{status}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 pt-20">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Meus Pagamentos</h1>
|
|
||||||
<p className="mt-2 text-gray-600">
|
|
||||||
Visualize todos os pagamentos recebidos pelos eventos fotografados
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Summary Cards */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
|
||||||
<p className="text-sm text-gray-600 mb-1">Total Recebido</p>
|
|
||||||
<p className="text-2xl font-bold text-gray-900">
|
|
||||||
R$ {totalRecebido.toFixed(2)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
|
||||||
<p className="text-sm text-gray-600 mb-1">Pagamentos Confirmados</p>
|
|
||||||
<p className="text-2xl font-bold text-green-600">
|
|
||||||
R$ {totalPago.toFixed(2)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
|
||||||
<p className="text-sm text-gray-600 mb-1">Pagamentos Pendentes</p>
|
|
||||||
<p className="text-2xl font-bold text-yellow-600">
|
|
||||||
R$ {totalPendente.toFixed(2)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Card */}
|
|
||||||
<div className="bg-white rounded-lg shadow">
|
|
||||||
{/* Actions Bar */}
|
|
||||||
<div className="px-6 py-4 border-b border-gray-200">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900">
|
|
||||||
Histórico de Pagamentos
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
onClick={handleExport}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors"
|
|
||||||
>
|
|
||||||
<Download size={20} />
|
|
||||||
Exportar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{apiError && (
|
|
||||||
<div className="mx-6 mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg flex items-start gap-3">
|
|
||||||
<AlertCircle
|
|
||||||
className="text-yellow-600 flex-shrink-0 mt-0.5"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium text-yellow-800">Aviso</p>
|
|
||||||
<p className="text-sm text-yellow-700 mt-1">{apiError}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead className="bg-gray-50 border-b border-gray-200">
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-3 text-left">
|
|
||||||
<button
|
|
||||||
onClick={() => handleSort("data")}
|
|
||||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
|
||||||
>
|
|
||||||
Data Evento
|
|
||||||
{getSortIcon("data")}
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-3 text-left">
|
|
||||||
<button
|
|
||||||
onClick={() => handleSort("nomeEvento")}
|
|
||||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
|
||||||
>
|
|
||||||
Nome Evento
|
|
||||||
{getSortIcon("nomeEvento")}
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-3 text-left">
|
|
||||||
<button
|
|
||||||
onClick={() => handleSort("tipoEvento")}
|
|
||||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
|
||||||
>
|
|
||||||
Tipo Evento
|
|
||||||
{getSortIcon("tipoEvento")}
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-3 text-left">
|
|
||||||
<button
|
|
||||||
onClick={() => handleSort("empresa")}
|
|
||||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
|
||||||
>
|
|
||||||
Empresa
|
|
||||||
{getSortIcon("empresa")}
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-3 text-left">
|
|
||||||
<button
|
|
||||||
onClick={() => handleSort("valorRecebido")}
|
|
||||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
|
||||||
>
|
|
||||||
Valor Recebido
|
|
||||||
{getSortIcon("valorRecebido")}
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-3 text-left">
|
|
||||||
<button
|
|
||||||
onClick={() => handleSort("dataPagamento")}
|
|
||||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
|
||||||
>
|
|
||||||
Data Pagamento
|
|
||||||
{getSortIcon("dataPagamento")}
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-3 text-left">
|
|
||||||
<button
|
|
||||||
onClick={() => handleSort("statusPagamento")}
|
|
||||||
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
|
|
||||||
>
|
|
||||||
Status
|
|
||||||
{getSortIcon("statusPagamento")}
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
|
||||||
{sortedPayments.length === 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={7}
|
|
||||||
className="px-4 py-8 text-center text-gray-500"
|
|
||||||
>
|
|
||||||
Nenhum pagamento encontrado
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
sortedPayments.map((payment) => (
|
|
||||||
<tr
|
|
||||||
key={payment.id}
|
|
||||||
className="hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
|
||||||
{new Date(payment.data).toLocaleDateString("pt-BR")}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-sm">
|
|
||||||
{payment.nomeEvento}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
|
||||||
{payment.tipoEvento}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-sm">{payment.empresa}</td>
|
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-sm font-medium text-green-600">
|
|
||||||
R$ {payment.valorRecebido.toFixed(2)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
|
||||||
{new Date(payment.dataPagamento).toLocaleDateString(
|
|
||||||
"pt-BR"
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
|
||||||
{getStatusBadge(payment.statusPagamento)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50">
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Total de pagamentos: {sortedPayments.length}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PhotographerFinance;
|
|
||||||
260
frontend/pages/ProfessionalStatement.tsx
Normal file
260
frontend/pages/ProfessionalStatement.tsx
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
import { getProfessionalFinancialStatement } from "../services/apiService";
|
||||||
|
import { formatCurrency } from "../utils/format";
|
||||||
|
|
||||||
|
interface FinancialTransactionDTO {
|
||||||
|
id: string;
|
||||||
|
data_evento: string;
|
||||||
|
nome_evento: string;
|
||||||
|
tipo_evento: string;
|
||||||
|
empresa: string;
|
||||||
|
valor_recebido: number;
|
||||||
|
valor_free?: number; // Optional as backend might not have sent it yet if cache/delays
|
||||||
|
valor_extra?: number;
|
||||||
|
descricao_extra?: string;
|
||||||
|
data_pagamento: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FinancialStatementResponse {
|
||||||
|
total_recebido: number;
|
||||||
|
pagamentos_confirmados: number;
|
||||||
|
pagamentos_pendentes: number;
|
||||||
|
transactions: FinancialTransactionDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfessionalStatement: React.FC = () => {
|
||||||
|
const { user, token } = useAuth();
|
||||||
|
const [data, setData] = useState<FinancialStatementResponse | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedTransaction, setSelectedTransaction] = useState<FinancialTransactionDTO | null>(null);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token) {
|
||||||
|
fetchStatement();
|
||||||
|
} else {
|
||||||
|
const storedToken = localStorage.getItem("@Auth:token");
|
||||||
|
if (storedToken) {
|
||||||
|
fetchStatement(storedToken);
|
||||||
|
} else {
|
||||||
|
console.error("No token found");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const fetchStatement = async (overrideToken?: string) => {
|
||||||
|
try {
|
||||||
|
const t = overrideToken || token;
|
||||||
|
if (!t) return;
|
||||||
|
const response = await getProfessionalFinancialStatement(t);
|
||||||
|
if (response.data) {
|
||||||
|
setData(response.data);
|
||||||
|
} else {
|
||||||
|
console.error(response.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao buscar extrato:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowClick = (t: FinancialTransactionDTO) => {
|
||||||
|
setSelectedTransaction(t);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusBadge = ({ status }: { status: string }) => {
|
||||||
|
const isPaid = status === "Pago";
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`px-3 py-1 rounded-full text-xs font-semibold ${
|
||||||
|
isPaid
|
||||||
|
? "bg-green-100 text-green-700"
|
||||||
|
: "bg-yellow-100 text-yellow-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="p-8 text-center text-gray-500">Carregando extrato...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <div className="p-8 text-center text-gray-500">Nenhum dado encontrado.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-50 min-h-screen p-4 sm:p-8 font-sans">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<header className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">Meus Pagamentos</h1>
|
||||||
|
<p className="text-gray-500">
|
||||||
|
Visualize todos os pagamentos recebidos pelos eventos fotografados
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
|
||||||
|
<p className="text-sm text-gray-500 mb-1">Total Recebido</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">
|
||||||
|
{formatCurrency(data.total_recebido)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
|
||||||
|
<p className="text-sm text-gray-500 mb-1">Pagamentos Confirmados</p>
|
||||||
|
<p className="text-2xl font-bold text-green-600">
|
||||||
|
{formatCurrency(data.pagamentos_confirmados)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100">
|
||||||
|
<p className="text-sm text-gray-500 mb-1">Pagamentos Pendentes</p>
|
||||||
|
<p className="text-2xl font-bold text-yellow-600">
|
||||||
|
{formatCurrency(data.pagamentos_pendentes)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Transactions Table */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-100 overflow-hidden">
|
||||||
|
<div className="p-6 border-b border-gray-100 flex justify-between items-center">
|
||||||
|
<h2 className="text-lg font-bold text-gray-900">Histórico de Pagamentos</h2>
|
||||||
|
<button className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-gray-50 hover:bg-gray-100 rounded-md transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||||
|
</svg>
|
||||||
|
Exportar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm text-left">
|
||||||
|
<thead className="bg-gray-50 text-gray-500 font-medium">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-4">Data Evento</th>
|
||||||
|
<th className="px-6 py-4">Nome Evento</th>
|
||||||
|
<th className="px-6 py-4">Tipo Evento</th>
|
||||||
|
<th className="px-6 py-4">Empresa</th>
|
||||||
|
<th className="px-6 py-4">Valor Recebido</th>
|
||||||
|
<th className="px-6 py-4">Data Pagamento</th>
|
||||||
|
<th className="px-6 py-4 text-center">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-100">
|
||||||
|
{(data.transactions || []).length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={7} className="px-6 py-8 text-center text-gray-500">
|
||||||
|
Nenhum pagamento registrado.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
(data.transactions || []).map((t) => (
|
||||||
|
<tr
|
||||||
|
key={t.id}
|
||||||
|
className="hover:bg-gray-50 transition-colors cursor-pointer"
|
||||||
|
onClick={() => handleRowClick(t)}
|
||||||
|
>
|
||||||
|
<td className="px-6 py-4 text-gray-900">{t.data_evento}</td>
|
||||||
|
<td className="px-6 py-4 text-gray-900 font-medium">{t.nome_evento}</td>
|
||||||
|
<td className="px-6 py-4 text-gray-500">{t.tipo_evento}</td>
|
||||||
|
<td className="px-6 py-4 text-gray-500">{t.empresa}</td>
|
||||||
|
<td className={`px-6 py-4 font-medium ${t.status === 'Pago' ? 'text-green-600' : 'text-green-600'}`}>
|
||||||
|
{formatCurrency(t.valor_recebido)}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-gray-500">{t.data_pagamento}</td>
|
||||||
|
<td className="px-6 py-4 text-center">
|
||||||
|
<StatusBadge status={t.status} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
<tfoot className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<td colSpan={7} className="px-6 py-3 text-xs text-gray-500">
|
||||||
|
Total de pagamentos: {(data.transactions || []).length}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Details Modal */}
|
||||||
|
{isModalOpen && selectedTransaction && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||||
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-lg overflow-hidden animate-in fade-in zoom-in duration-200">
|
||||||
|
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">Detalhes do Pagamento</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors p-1 rounded-full hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Header Info */}
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-1">Evento</p>
|
||||||
|
<p className="text-xl font-bold text-gray-900">{selectedTransaction.nome_evento}</p>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">{selectedTransaction.empresa} • {selectedTransaction.data_evento}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Financial Breakdown */}
|
||||||
|
<div className="bg-gray-50 rounded-xl p-4 space-y-3 border border-gray-100">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-gray-600 text-sm">Valor Base (Free)</span>
|
||||||
|
<span className="font-semibold text-gray-900">{formatCurrency(selectedTransaction.valor_free || 0)}</span>
|
||||||
|
</div>
|
||||||
|
{(selectedTransaction.valor_extra || 0) > 0 && (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-yellow-700 text-sm font-medium">Valor Extra</span>
|
||||||
|
<span className="font-semibold text-yellow-700">{formatCurrency(selectedTransaction.valor_extra || 0)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="border-t border-gray-200 my-2"></div>
|
||||||
|
<div className="flex justify-between items-center pt-1">
|
||||||
|
<span className="text-gray-900 font-bold text-base">Total Recebido</span>
|
||||||
|
<span className="text-green-600 font-bold text-lg">{formatCurrency(selectedTransaction.valor_recebido)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Extra Description */}
|
||||||
|
{(selectedTransaction.descricao_extra) && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-2">Descrição do Extra</p>
|
||||||
|
<div className="bg-yellow-50 text-yellow-800 p-3 rounded-lg text-sm border border-yellow-100">
|
||||||
|
{selectedTransaction.descricao_extra}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Status Footer */}
|
||||||
|
<div className="flex justify-between items-center pt-2">
|
||||||
|
<span className="text-sm text-gray-500">Data do Pagamento</span>
|
||||||
|
<div className="flex gap-3 items-center">
|
||||||
|
<span className="font-medium text-gray-900">{selectedTransaction.data_pagamento}</span>
|
||||||
|
<StatusBadge status={selectedTransaction.status} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfessionalStatement;
|
||||||
|
|
@ -1132,3 +1132,37 @@ export async function listPassengers(carroId: string, token: string) {
|
||||||
export async function verifyAccessCode(code: string): Promise<ApiResponse<{ valid: boolean; error?: string; empresa_id?: string; empresa_nome?: string }>> {
|
export async function verifyAccessCode(code: string): Promise<ApiResponse<{ valid: boolean; error?: string; empresa_id?: string; empresa_nome?: string }>> {
|
||||||
return fetchFromBackend<{ valid: boolean; error?: string; empresa_id?: string; empresa_nome?: string }>(`/api/public/codigos-acesso/verificar?code=${encodeURIComponent(code)}`);
|
return fetchFromBackend<{ valid: boolean; error?: string; empresa_id?: string; empresa_nome?: string }>(`/api/public/codigos-acesso/verificar?code=${encodeURIComponent(code)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Busca o extrato financeiro do profissional logado
|
||||||
|
*/
|
||||||
|
export async function getProfessionalFinancialStatement(token: string): Promise<ApiResponse<any>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/profissionais/me/financial-statement`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
error: null,
|
||||||
|
isBackendDown: false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching financial statement:", error);
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||||
|
isBackendDown: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
7
frontend/utils/format.ts
Normal file
7
frontend/utils/format.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const formatCurrency = (value: number | undefined | null): string => {
|
||||||
|
if (value === undefined || value === null) return "R$ 0,00";
|
||||||
|
return new Intl.NumberFormat("pt-BR", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "BRL",
|
||||||
|
}).format(value);
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue