diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index c2f1965..35eb47a 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -167,6 +167,8 @@ func main() { profGroup.GET("/:id", profissionaisHandler.Get) profGroup.PUT("/:id", profissionaisHandler.Update) 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") diff --git a/backend/internal/agenda/handler.go b/backend/internal/agenda/handler.go index c392e30..caa4514 100644 --- a/backend/internal/agenda/handler.go +++ b/backend/internal/agenda/handler.go @@ -389,3 +389,30 @@ func (h *Handler) ListAvailableProfessionals(c *gin.Context) { 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) +} diff --git a/backend/internal/agenda/service.go b/backend/internal/agenda/service.go index df1782d..6ef38d9 100644 --- a/backend/internal/agenda/service.go +++ b/backend/internal/agenda/service.go @@ -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) { 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 +} diff --git a/backend/internal/db/generated/financial_transactions.sql.go b/backend/internal/db/generated/financial_transactions.sql.go index 9190ba2..b66f651 100644 --- a/backend/internal/db/generated/financial_transactions.sql.go +++ b/backend/internal/db/generated/financial_transactions.sql.go @@ -18,7 +18,7 @@ INSERT INTO financial_transactions ( total_pagar, data_pagamento, pgto_ok ) VALUES ( $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 { @@ -74,6 +74,7 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa &i.PgtoOk, &i.CriadoEm, &i.AtualizadoEm, + &i.ProfissionalID, ) return i, err } @@ -88,7 +89,7 @@ func (q *Queries) DeleteTransaction(ctx context.Context, id pgtype.UUID) error { } 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) { @@ -112,12 +113,13 @@ func (q *Queries) GetTransaction(ctx context.Context, id pgtype.UUID) (Financial &i.PgtoOk, &i.CriadoEm, &i.AtualizadoEm, + &i.ProfissionalID, ) return i, err } 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 LEFT JOIN cadastro_fot f ON t.fot_id = f.id ORDER BY t.data_cobranca DESC @@ -141,6 +143,7 @@ type ListTransactionsRow struct { 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"` } @@ -171,6 +174,7 @@ func (q *Queries) ListTransactions(ctx context.Context) ([]ListTransactionsRow, &i.PgtoOk, &i.CriadoEm, &i.AtualizadoEm, + &i.ProfissionalID, &i.FotNumero, ); err != nil { return nil, err @@ -184,7 +188,7 @@ func (q *Queries) ListTransactions(ctx context.Context) ([]ListTransactionsRow, } 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 ORDER BY data_cobranca DESC ` @@ -216,6 +220,93 @@ func (q *Queries) ListTransactionsByFot(ctx context.Context, fotID pgtype.UUID) &i.PgtoOk, &i.CriadoEm, &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 { return nil, err } @@ -248,7 +339,7 @@ UPDATE financial_transactions SET total_pagar = $13, data_pagamento = $14, pgto_ok = $15, atualizado_em = NOW() 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 { @@ -306,6 +397,7 @@ func (q *Queries) UpdateTransaction(ctx context.Context, arg UpdateTransactionPa &i.PgtoOk, &i.CriadoEm, &i.AtualizadoEm, + &i.ProfissionalID, ) return i, err } diff --git a/backend/internal/db/generated/models.go b/backend/internal/db/generated/models.go index 1dece9a..9bf106b 100644 --- a/backend/internal/db/generated/models.go +++ b/backend/internal/db/generated/models.go @@ -174,6 +174,7 @@ type FinancialTransaction struct { PgtoOk pgtype.Bool `json:"pgto_ok"` CriadoEm pgtype.Timestamptz `json:"criado_em"` AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` + ProfissionalID pgtype.UUID `json:"profissional_id"` } type FuncoesProfissionai struct { diff --git a/backend/internal/db/migrations/005_link_financial_transactions_professional.sql b/backend/internal/db/migrations/005_link_financial_transactions_professional.sql new file mode 100644 index 0000000..b612add --- /dev/null +++ b/backend/internal/db/migrations/005_link_financial_transactions_professional.sql @@ -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 != ''; diff --git a/backend/internal/db/queries/financial_transactions.sql b/backend/internal/db/queries/financial_transactions.sql index 3ddb56f..2d604ee 100644 --- a/backend/internal/db/queries/financial_transactions.sql +++ b/backend/internal/db/queries/financial_transactions.sql @@ -39,3 +39,17 @@ DELETE FROM financial_transactions WHERE id = $1; -- name: GetTransaction :one 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; diff --git a/backend/internal/db/schema.sql b/backend/internal/db/schema.sql index d8e8bd6..1ff120e 100644 --- a/backend/internal/db/schema.sql +++ b/backend/internal/db/schema.sql @@ -459,7 +459,8 @@ CREATE TABLE IF NOT EXISTS financial_transactions ( data_pagamento DATE, pgto_ok BOOLEAN DEFAULT FALSE, 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) diff --git a/frontend/App.tsx b/frontend/App.tsx index f3e11cd..cabe841 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -16,7 +16,7 @@ import { ProfessionalRegister } from "./pages/ProfessionalRegister"; import { TeamPage } from "./pages/Team"; import EventDetails from "./pages/EventDetails"; import Finance from "./pages/Finance"; -import PhotographerFinance from "./pages/PhotographerFinance"; + import { SettingsPage } from "./pages/Settings"; import { CourseManagement } from "./pages/CourseManagement"; import { InspirationPage } from "./pages/Inspiration"; @@ -32,6 +32,7 @@ import { verifyAccessCode } from "./services/apiService"; import { Button } from "./components/Button"; import { X } from "lucide-react"; import { ShieldAlert } from "lucide-react"; +import ProfessionalStatement from "./pages/ProfessionalStatement"; // Componente de acesso negado const AccessDenied: React.FC = () => { @@ -684,9 +685,9 @@ const AppContent: React.FC = () => { + - + } diff --git a/frontend/components/EventLogistics.tsx b/frontend/components/EventLogistics.tsx index d9afe9d..ddc2b0a 100644 --- a/frontend/components/EventLogistics.tsx +++ b/frontend/components/EventLogistics.tsx @@ -114,7 +114,11 @@ const EventLogistics: React.FC = ({ agendaId, assignedProfe > {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 => ( ))} diff --git a/frontend/components/EventScheduler.tsx b/frontend/components/EventScheduler.tsx index cc1ff54..179cec0 100644 --- a/frontend/components/EventScheduler.tsx +++ b/frontend/components/EventScheduler.tsx @@ -150,6 +150,7 @@ const EventScheduler: React.FC = ({ agendaId, dataEvento, a // 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)); const allowedMap = new Map(); // ID -> Status + const assignedRoleMap = new Map(); // ID -> Role Name if (allowedProfessionals) { // Normalize allowed list @@ -165,6 +166,11 @@ const EventScheduler: React.FC = ({ agendaId, dataEvento, a if (pid && status !== 'REJEITADO' && status !== 'Rejeitado') { ids.push(pid); 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 = ({ agendaId, dataEvento, a const status = allowedMap.get(p.id); const isPending = status !== 'Confirmado' && status !== 'ACEITO'; const isDisabled = isBusy || isPending; + + const assignedRole = assignedRoleMap.get(p.id); + const displayRole = assignedRole || p.role || "Profissional"; let label = ""; if (isPending) label = "(Pendente de Aceite)"; @@ -215,7 +224,7 @@ const EventScheduler: React.FC = ({ agendaId, dataEvento, a return ( ); })} @@ -270,6 +279,8 @@ const EventScheduler: React.FC = ({ agendaId, dataEvento, a // 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 const profData = professionals.find(p => p.id === item.profissional_id); + const assignedRole = assignedRoleMap.get(item.profissional_id); + const displayRole = assignedRole || profData?.role; return (
@@ -285,6 +296,7 @@ const EventScheduler: React.FC = ({ agendaId, dataEvento, a

{item.profissional_nome} + {displayRole && ({displayRole})} {item.phone && ({item.phone})}

diff --git a/frontend/pages/EventDetails.tsx b/frontend/pages/EventDetails.tsx index 03a51cb..0c84c14 100644 --- a/frontend/pages/EventDetails.tsx +++ b/frontend/pages/EventDetails.tsx @@ -28,9 +28,11 @@ const EventDetails: React.FC = () => {

{/* Header */}
- +

{event.empresa_nome} - {event.tipo_evento_nome} diff --git a/frontend/pages/PhotographerFinance.tsx b/frontend/pages/PhotographerFinance.tsx deleted file mode 100644 index c1d5a10..0000000 --- a/frontend/pages/PhotographerFinance.tsx +++ /dev/null @@ -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([ - { - 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(null); - const [sortDirection, setSortDirection] = useState(null); - const [apiError, setApiError] = useState(""); - - // 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 ( - - ); - } - if (sortDirection === "asc") { - return ; - } - if (sortDirection === "desc") { - return ; - } - return ( - - ); - }; - - 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 ( - - {status} - - ); - }; - - return ( -
-
- {/* Header */} -
-

Meus Pagamentos

-

- Visualize todos os pagamentos recebidos pelos eventos fotografados -

-
- - {/* Summary Cards */} -
-
-

Total Recebido

-

- R$ {totalRecebido.toFixed(2)} -

-
-
-

Pagamentos Confirmados

-

- R$ {totalPago.toFixed(2)} -

-
-
-

Pagamentos Pendentes

-

- R$ {totalPendente.toFixed(2)} -

-
-
- - {/* Main Card */} -
- {/* Actions Bar */} -
-
-

- Histórico de Pagamentos -

- -
-
- - {apiError && ( -
- -
-

Aviso

-

{apiError}

-
-
- )} - - {/* Table */} -
- - - - - - - - - - - - - - {sortedPayments.length === 0 ? ( - - - - ) : ( - sortedPayments.map((payment) => ( - - - - - - - - - - )) - )} - -
- - - - - - - - - - - - - -
- Nenhum pagamento encontrado -
- {new Date(payment.data).toLocaleDateString("pt-BR")} - - {payment.nomeEvento} - - {payment.tipoEvento} - {payment.empresa} - R$ {payment.valorRecebido.toFixed(2)} - - {new Date(payment.dataPagamento).toLocaleDateString( - "pt-BR" - )} - - {getStatusBadge(payment.statusPagamento)} -
-
- - {/* Footer */} -
-

- Total de pagamentos: {sortedPayments.length} -

-
-
-
-
- ); -}; - -export default PhotographerFinance; diff --git a/frontend/pages/ProfessionalStatement.tsx b/frontend/pages/ProfessionalStatement.tsx new file mode 100644 index 0000000..0879e64 --- /dev/null +++ b/frontend/pages/ProfessionalStatement.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [selectedTransaction, setSelectedTransaction] = useState(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 ( + + {status} + + ); + }; + + if (loading) { + return
Carregando extrato...
; + } + + if (!data) { + return
Nenhum dado encontrado.
; + } + + return ( +
+
+
+

Meus Pagamentos

+

+ Visualize todos os pagamentos recebidos pelos eventos fotografados +

+
+ + {/* Summary Cards */} +
+
+

Total Recebido

+

+ {formatCurrency(data.total_recebido)} +

+
+
+

Pagamentos Confirmados

+

+ {formatCurrency(data.pagamentos_confirmados)} +

+
+
+

Pagamentos Pendentes

+

+ {formatCurrency(data.pagamentos_pendentes)} +

+
+
+ + {/* Transactions Table */} +
+
+

Histórico de Pagamentos

+ +
+ +
+ + + + + + + + + + + + + + {(data.transactions || []).length === 0 ? ( + + + + ) : ( + (data.transactions || []).map((t) => ( + handleRowClick(t)} + > + + + + + + + + + )) + )} + + + + + + +
Data EventoNome EventoTipo EventoEmpresaValor RecebidoData PagamentoStatus
+ Nenhum pagamento registrado. +
{t.data_evento}{t.nome_evento}{t.tipo_evento}{t.empresa} + {formatCurrency(t.valor_recebido)} + {t.data_pagamento} + +
+ Total de pagamentos: {(data.transactions || []).length} +
+
+
+
+ + {/* Details Modal */} + {isModalOpen && selectedTransaction && ( +
+
+
+

Detalhes do Pagamento

+ +
+
+ {/* Header Info */} +
+

Evento

+

{selectedTransaction.nome_evento}

+

{selectedTransaction.empresa} • {selectedTransaction.data_evento}

+
+ + {/* Financial Breakdown */} +
+
+ Valor Base (Free) + {formatCurrency(selectedTransaction.valor_free || 0)} +
+ {(selectedTransaction.valor_extra || 0) > 0 && ( +
+ Valor Extra + {formatCurrency(selectedTransaction.valor_extra || 0)} +
+ )} + {/* Divider */} +
+
+ Total Recebido + {formatCurrency(selectedTransaction.valor_recebido)} +
+
+ + {/* Extra Description */} + {(selectedTransaction.descricao_extra) && ( +
+

Descrição do Extra

+
+ {selectedTransaction.descricao_extra} +
+
+ )} + + {/* Status Footer */} +
+ Data do Pagamento +
+ {selectedTransaction.data_pagamento} + +
+
+
+
+
+ )} +
+ ); +}; + +export default ProfessionalStatement; diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index d0bf4b8..0619b15 100644 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -1132,3 +1132,37 @@ export async function listPassengers(carroId: string, token: string) { export async function verifyAccessCode(code: string): Promise> { 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> { + 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, + }; + } +} diff --git a/frontend/utils/format.ts b/frontend/utils/format.ts new file mode 100644 index 0000000..41727b4 --- /dev/null +++ b/frontend/utils/format.ts @@ -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); +};