feat(backend): implementa criação automática de transação financeira ao aceitar evento
This commit is contained in:
parent
c9e34af619
commit
6382145442
9 changed files with 285 additions and 21 deletions
|
|
@ -73,6 +73,7 @@ func main() {
|
||||||
// Initialize services
|
// Initialize services
|
||||||
notificationService := notification.NewService()
|
notificationService := notification.NewService()
|
||||||
profissionaisService := profissionais.NewService(queries)
|
profissionaisService := profissionais.NewService(queries)
|
||||||
|
financeService := finance.NewService(queries, profissionaisService)
|
||||||
authService := auth.NewService(queries, profissionaisService, cfg)
|
authService := auth.NewService(queries, profissionaisService, cfg)
|
||||||
funcoesService := funcoes.NewService(queries)
|
funcoesService := funcoes.NewService(queries)
|
||||||
cursosService := cursos.NewService(queries)
|
cursosService := cursos.NewService(queries)
|
||||||
|
|
@ -81,7 +82,7 @@ func main() {
|
||||||
tiposServicosService := tipos_servicos.NewService(queries)
|
tiposServicosService := tipos_servicos.NewService(queries)
|
||||||
tiposEventosService := tipos_eventos.NewService(queries)
|
tiposEventosService := tipos_eventos.NewService(queries)
|
||||||
cadastroFotService := cadastro_fot.NewService(queries)
|
cadastroFotService := cadastro_fot.NewService(queries)
|
||||||
agendaService := agenda.NewService(queries, notificationService, cfg)
|
agendaService := agenda.NewService(queries, notificationService, cfg, financeService)
|
||||||
availabilityService := availability.NewService(queries)
|
availabilityService := availability.NewService(queries)
|
||||||
s3Service := storage.NewS3Service(cfg)
|
s3Service := storage.NewS3Service(cfg)
|
||||||
|
|
||||||
|
|
@ -105,7 +106,7 @@ func main() {
|
||||||
escalasHandler := escalas.NewHandler(escalas.NewService(queries))
|
escalasHandler := escalas.NewHandler(escalas.NewService(queries))
|
||||||
logisticaHandler := logistica.NewHandler(logistica.NewService(queries, notificationService, cfg))
|
logisticaHandler := logistica.NewHandler(logistica.NewService(queries, notificationService, cfg))
|
||||||
codigosHandler := codigos.NewHandler(codigos.NewService(queries))
|
codigosHandler := codigos.NewHandler(codigos.NewService(queries))
|
||||||
financeHandler := finance.NewHandler(finance.NewService(queries, profissionaisService))
|
financeHandler := finance.NewHandler(financeService)
|
||||||
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,21 +14,25 @@ import (
|
||||||
"photum-backend/internal/db/generated"
|
"photum-backend/internal/db/generated"
|
||||||
"photum-backend/internal/notification"
|
"photum-backend/internal/notification"
|
||||||
|
|
||||||
|
"photum-backend/internal/finance"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
queries *generated.Queries
|
queries *generated.Queries
|
||||||
notification *notification.Service
|
notification *notification.Service
|
||||||
cfg *config.Config
|
financeService *finance.Service
|
||||||
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(db *generated.Queries, notif *notification.Service, cfg *config.Config) *Service {
|
func NewService(db *generated.Queries, notif *notification.Service, cfg *config.Config, fin *finance.Service) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
queries: db,
|
queries: db,
|
||||||
notification: notif,
|
notification: notif,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
financeService: fin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -751,6 +755,87 @@ func (s *Service) UpdateAssignmentStatus(ctx context.Context, agendaID, professi
|
||||||
return fmt.Errorf("conflito de horário: profissional já confirmou presença em outro evento às %s", c.Horario.String)
|
return fmt.Errorf("conflito de horário: profissional já confirmou presença em outro evento às %s", c.Horario.String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- AUTO-CREATE FINANCIAL TRANSACTION ---
|
||||||
|
// 1. Fetch Assignment to know the Function/Role
|
||||||
|
assign, err := s.queries.GetAssignment(ctx, generated.GetAssignmentParams{
|
||||||
|
AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true},
|
||||||
|
ProfissionalID: pgtype.UUID{Bytes: professionalID, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[AutoFinance] Error fetching assignment: %v", err)
|
||||||
|
} else {
|
||||||
|
// 2. Determine Service Name
|
||||||
|
var serviceName string
|
||||||
|
if assign.FuncaoID.Valid {
|
||||||
|
fn, err := s.queries.GetFuncaoByID(ctx, assign.FuncaoID)
|
||||||
|
if err == nil {
|
||||||
|
serviceName = fn.Nome
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to professional's default function
|
||||||
|
prof, errProf := s.queries.GetProfissionalByID(ctx, generated.GetProfissionalByIDParams{
|
||||||
|
ID: pgtype.UUID{Bytes: professionalID, Valid: true},
|
||||||
|
Regiao: pgtype.Text{String: regiao, Valid: true},
|
||||||
|
})
|
||||||
|
if serviceName == "" && errProf == nil && prof.FuncaoProfissionalID.Valid {
|
||||||
|
fn, err := s.queries.GetFuncaoByID(ctx, prof.FuncaoProfissionalID)
|
||||||
|
if err == nil {
|
||||||
|
serviceName = fn.Nome
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fetch Event Type Name
|
||||||
|
tipoEvento, errType := s.queries.GetTipoEventoByID(ctx, generated.GetTipoEventoByIDParams{
|
||||||
|
ID: agenda.TipoEventoID,
|
||||||
|
Regiao: pgtype.Text{String: regiao, Valid: true},
|
||||||
|
})
|
||||||
|
|
||||||
|
if serviceName != "" && errType == nil && errProf == nil {
|
||||||
|
// 4. Fetch Standard Price
|
||||||
|
price, errPrice := s.financeService.GetStandardPrice(ctx, tipoEvento.Nome, serviceName, regiao)
|
||||||
|
|
||||||
|
baseValue := 0.0
|
||||||
|
var priceNumeric pgtype.Numeric
|
||||||
|
if errPrice == nil {
|
||||||
|
priceNumeric = price
|
||||||
|
if val, err := price.Float64Value(); err == nil && val.Valid {
|
||||||
|
baseValue = val.Float64
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("[AutoFinance] Price not found for %s / %s: %v", tipoEvento.Nome, serviceName, errPrice)
|
||||||
|
// We continue with 0.0 or handle error?
|
||||||
|
// User wants it to appear. If 0, it appears as 0.
|
||||||
|
priceNumeric.Scan("0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Create Transaction
|
||||||
|
// Check if already exists? (Optional, but good practice. For now simpler is better)
|
||||||
|
|
||||||
|
_, errCreate := s.financeService.Create(ctx, generated.CreateTransactionParams{
|
||||||
|
FotID: agenda.FotID,
|
||||||
|
DataCobranca: agenda.DataEvento,
|
||||||
|
TipoEvento: pgtype.Text{String: tipoEvento.Nome, Valid: true},
|
||||||
|
TipoServico: pgtype.Text{String: serviceName, Valid: true},
|
||||||
|
ProfessionalName: pgtype.Text{String: prof.Nome, Valid: true},
|
||||||
|
Whatsapp: prof.Whatsapp,
|
||||||
|
Cpf: prof.CpfCnpjTitular,
|
||||||
|
TotalPagar: priceNumeric, // Base Value
|
||||||
|
ValorFree: priceNumeric, // Populate Free value for frontend calc
|
||||||
|
ProfissionalID: pgtype.UUID{Bytes: professionalID, Valid: true},
|
||||||
|
PgtoOk: pgtype.Bool{Bool: false, Valid: true},
|
||||||
|
// Others default/null
|
||||||
|
}, regiao)
|
||||||
|
|
||||||
|
if errCreate != nil {
|
||||||
|
log.Printf("[AutoFinance] Failed to create transaction: %v", errCreate)
|
||||||
|
} else {
|
||||||
|
log.Printf("[AutoFinance] Transaction created for %s: %.2f", prof.Nome, baseValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -427,6 +427,33 @@ func (q *Queries) GetAgendaProfessionals(ctx context.Context, agendaID pgtype.UU
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAssignment = `-- name: GetAssignment :one
|
||||||
|
SELECT id, agenda_id, profissional_id, status, motivo_rejeicao, funcao_id, posicao, criado_em, is_coordinator FROM agenda_profissionais
|
||||||
|
WHERE agenda_id = $1 AND profissional_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetAssignmentParams struct {
|
||||||
|
AgendaID pgtype.UUID `json:"agenda_id"`
|
||||||
|
ProfissionalID pgtype.UUID `json:"profissional_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAssignment(ctx context.Context, arg GetAssignmentParams) (AgendaProfissionai, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getAssignment, arg.AgendaID, arg.ProfissionalID)
|
||||||
|
var i AgendaProfissionai
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.AgendaID,
|
||||||
|
&i.ProfissionalID,
|
||||||
|
&i.Status,
|
||||||
|
&i.MotivoRejeicao,
|
||||||
|
&i.FuncaoID,
|
||||||
|
&i.Posicao,
|
||||||
|
&i.CriadoEm,
|
||||||
|
&i.IsCoordinator,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const listAgendas = `-- name: ListAgendas :many
|
const listAgendas = `-- name: ListAgendas :many
|
||||||
SELECT
|
SELECT
|
||||||
a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, a.logistica_notificacao_enviada_em, a.regiao, a.contatos,
|
a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, a.logistica_notificacao_enviada_em, a.regiao, a.contatos,
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ SELECT p.valor
|
||||||
FROM precos_tipos_eventos p
|
FROM precos_tipos_eventos p
|
||||||
JOIN tipos_eventos te ON p.tipo_evento_id = te.id
|
JOIN tipos_eventos te ON p.tipo_evento_id = te.id
|
||||||
JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
|
JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
|
||||||
WHERE te.nome = $1 AND f.nome = $2 AND te.regiao = $3
|
WHERE te.nome ILIKE $1 AND f.nome ILIKE $2 AND te.regiao = $3
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -256,3 +256,7 @@ ORDER BY a.data_evento;
|
||||||
UPDATE agenda_profissionais
|
UPDATE agenda_profissionais
|
||||||
SET is_coordinator = $3
|
SET is_coordinator = $3
|
||||||
WHERE agenda_id = $1 AND profissional_id = $2;
|
WHERE agenda_id = $1 AND profissional_id = $2;
|
||||||
|
|
||||||
|
-- name: GetAssignment :one
|
||||||
|
SELECT * FROM agenda_profissionais
|
||||||
|
WHERE agenda_id = $1 AND profissional_id = $2;
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ SELECT p.valor
|
||||||
FROM precos_tipos_eventos p
|
FROM precos_tipos_eventos p
|
||||||
JOIN tipos_eventos te ON p.tipo_evento_id = te.id
|
JOIN tipos_eventos te ON p.tipo_evento_id = te.id
|
||||||
JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
|
JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
|
||||||
WHERE te.nome = $1 AND f.nome = $2 AND te.regiao = @regiao
|
WHERE te.nome ILIKE $1 AND f.nome ILIKE $2 AND te.regiao = @regiao
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
||||||
-- name: GetTipoEventoByNome :one
|
-- name: GetTipoEventoByNome :one
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Star,
|
Star,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { setCoordinator, finalizeFOT } from "../services/apiService";
|
import { setCoordinator, finalizeFOT, getPrice } from "../services/apiService";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { useData } from "../contexts/DataContext";
|
import { useData } from "../contexts/DataContext";
|
||||||
import { STATUS_COLORS } from "../constants";
|
import { STATUS_COLORS } from "../constants";
|
||||||
|
|
@ -151,6 +151,97 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
const [teamStatusFilter, setTeamStatusFilter] = useState("all");
|
const [teamStatusFilter, setTeamStatusFilter] = useState("all");
|
||||||
const [teamAvailabilityFilter, setTeamAvailabilityFilter] = useState("all");
|
const [teamAvailabilityFilter, setTeamAvailabilityFilter] = useState("all");
|
||||||
const [roleSelectionProf, setRoleSelectionProf] = useState<Professional | null>(null);
|
const [roleSelectionProf, setRoleSelectionProf] = useState<Professional | null>(null);
|
||||||
|
const [basePrice, setBasePrice] = useState<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBasePrice = async () => {
|
||||||
|
if (!selectedEvent || !user || !token) {
|
||||||
|
setBasePrice(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only for professionals (skip owners/admins unless they want to see it? User asked for professional view)
|
||||||
|
// If user is admin but viewing as professional context? No, user req specifically: "ao convidar um profissional... no painel dele"
|
||||||
|
if (user.role === UserRole.EVENT_OWNER || user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) {
|
||||||
|
setBasePrice(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentProf = professionals.find(p => p.usuarioId === user.id);
|
||||||
|
if (!currentProf) return;
|
||||||
|
|
||||||
|
// Determine Service Name
|
||||||
|
let serviceName = "";
|
||||||
|
|
||||||
|
// 1. Check assignment
|
||||||
|
const assignment = selectedEvent.assignments?.find(a => a.professionalId === currentProf.id);
|
||||||
|
if (assignment && assignment.funcaoId && functions) {
|
||||||
|
const fn = functions.find(f => f.id === assignment.funcaoId);
|
||||||
|
if (fn) serviceName = fn.nome;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback to professional functions/role
|
||||||
|
if (!serviceName) {
|
||||||
|
if (currentProf.functions && currentProf.functions.length > 0) {
|
||||||
|
// Use first function as default base? Or try to match?
|
||||||
|
// Simple approach: use first.
|
||||||
|
serviceName = currentProf.functions[0].nome;
|
||||||
|
} else {
|
||||||
|
serviceName = currentProf.role;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map legacy roles if needed (Backend expects names matching tipos_servicos)
|
||||||
|
// e.g. "fotografo" -> "Fotografia"? Check backend migration/seeds.
|
||||||
|
// Assuming names match or backend handles fuzzy match.
|
||||||
|
// Backend GetStandardPrice uses ILIKE on tipos_servicos.nome.
|
||||||
|
// "Fotógrafo" might need to map to "Fotografia".
|
||||||
|
// "Cinegrafista" -> "Cinegrafia".
|
||||||
|
// Let's try raw first, but maybe normalize if needed.
|
||||||
|
// Actually, let's map common ones just in case.
|
||||||
|
// Map legacy roles and English enums
|
||||||
|
const serviceLower = serviceName.toLowerCase();
|
||||||
|
if (serviceLower.includes("fot") || serviceLower === "photographer") serviceName = "Fotógrafo";
|
||||||
|
else if (serviceLower.includes("cine") || serviceLower === "videographer") serviceName = "Cinegrafista";
|
||||||
|
else if (serviceLower.includes("recep")) serviceName = "Recepcionista";
|
||||||
|
else if (serviceLower === "drone") serviceName = "Drone"; // Example
|
||||||
|
|
||||||
|
console.log("Fetch Base Price Debug:", {
|
||||||
|
currentProfId: currentProf.id,
|
||||||
|
assignmentFound: !!assignment,
|
||||||
|
rawService: serviceLower,
|
||||||
|
finalService: serviceName,
|
||||||
|
eventType: selectedEvent.type
|
||||||
|
});
|
||||||
|
|
||||||
|
if (serviceName && selectedEvent.type) {
|
||||||
|
try {
|
||||||
|
const res = await getPrice(token, selectedEvent.type, serviceName);
|
||||||
|
console.log("Fetch Base Price Result:", res);
|
||||||
|
if (res.data) {
|
||||||
|
setBasePrice(res.data.valor);
|
||||||
|
} else {
|
||||||
|
console.warn("Base Price returned no data");
|
||||||
|
setBasePrice(0);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching base price:", err);
|
||||||
|
setBasePrice(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Skipping price fetch: Missing serviceName or eventType");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
fetchBasePrice();
|
||||||
|
}, [selectedEvent, user, token, professionals, functions]);
|
||||||
|
|
||||||
|
// ... (existing code) ...
|
||||||
|
|
||||||
|
// Inside render (around line 1150 in original file, need to find the "Time" card)
|
||||||
|
// Look for: <div className="bg-gray-50 p-4 rounded-sm border border-gray-100"> ... Horário ... </div>
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialView) {
|
if (initialView) {
|
||||||
|
|
@ -1023,6 +1114,17 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
{selectedEvent.time}
|
{selectedEvent.time}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{basePrice !== null && (
|
||||||
|
<div className={`p-4 rounded border ${basePrice > 0 ? "bg-green-50 border-green-200" : "bg-gray-50 border-gray-200"}`}>
|
||||||
|
<p className={`text-xs uppercase tracking-wide mb-1 font-bold ${basePrice > 0 ? "text-green-700" : "text-gray-500"}`}>
|
||||||
|
Valor Base
|
||||||
|
</p>
|
||||||
|
<p className={`font-bold ${basePrice > 0 ? "text-green-800" : "text-gray-700"}`}>
|
||||||
|
{basePrice > 0 ? basePrice.toLocaleString("pt-BR", { style: "currency", currency: "BRL" }) : "R$ 0,00"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* FOT Information Table */}
|
{/* FOT Information Table */}
|
||||||
|
|
@ -1309,6 +1411,18 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Base Value Card for Professionals */}
|
||||||
|
{basePrice !== null && basePrice > 0 && (
|
||||||
|
<tr className="hover:bg-gray-50">
|
||||||
|
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
|
||||||
|
Valor Base
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-gray-900 font-bold text-green-700">
|
||||||
|
{basePrice.toLocaleString("pt-BR", { style: "currency", currency: "BRL" })}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
<tr className="hover:bg-gray-50">
|
<tr className="hover:bg-gray-50">
|
||||||
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
|
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
|
||||||
Horário
|
Horário
|
||||||
|
|
|
||||||
|
|
@ -544,7 +544,7 @@ const Finance: React.FC = () => {
|
||||||
whatsapp: t.whatsapp,
|
whatsapp: t.whatsapp,
|
||||||
cpf: t.cpf,
|
cpf: t.cpf,
|
||||||
tabelaFree: t.tabelaFree,
|
tabelaFree: t.tabelaFree,
|
||||||
valorFree: t.valorFree,
|
valorFree: t.valorFree ?? t.totalPagar, // Use Total as fallback if Free is null (legacy/auto records)
|
||||||
valorExtra: t.valorExtra,
|
valorExtra: t.valorExtra,
|
||||||
descricaoExtra: t.descricaoExtra,
|
descricaoExtra: t.descricaoExtra,
|
||||||
dataPgto: t.dataPgto,
|
dataPgto: t.dataPgto,
|
||||||
|
|
@ -1074,8 +1074,8 @@ const Finance: React.FC = () => {
|
||||||
<td className="px-3 py-2">{t.whatsapp}</td>
|
<td className="px-3 py-2">{t.whatsapp}</td>
|
||||||
<td className="px-3 py-2">{t.cpf}</td>
|
<td className="px-3 py-2">{t.cpf}</td>
|
||||||
<td className="px-3 py-2">{t.tabelaFree}</td>
|
<td className="px-3 py-2">{t.tabelaFree}</td>
|
||||||
<td className="px-3 py-2 text-right">{t.valorFree?.toFixed(2)}</td>
|
<td className="px-3 py-2 text-right">{t.valorFree != null ? t.valorFree.toFixed(2) : "-"}</td>
|
||||||
<td className="px-3 py-2 text-right">{t.valorExtra?.toFixed(2)}</td>
|
<td className="px-3 py-2 text-right">{t.valorExtra != null ? t.valorExtra.toFixed(2) : "-"}</td>
|
||||||
<td className="px-3 py-2 max-w-[150px] truncate" title={t.descricaoExtra}>{t.descricaoExtra}</td>
|
<td className="px-3 py-2 max-w-[150px] truncate" title={t.descricaoExtra}>{t.descricaoExtra}</td>
|
||||||
<td className="px-3 py-2 text-right font-bold text-green-700">{t.totalPagar?.toFixed(2)}</td>
|
<td className="px-3 py-2 text-right font-bold text-green-700">{t.totalPagar?.toFixed(2)}</td>
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2">
|
||||||
|
|
@ -1292,13 +1292,13 @@ const Finance: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Values */}
|
{/* Values */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700">Tabela Free</label>
|
<label className="block text-xs font-medium text-gray-700">Tabela Free</label>
|
||||||
{proFunctions.length > 0 ? (
|
{proFunctions.length > 0 ? (
|
||||||
<select
|
<select
|
||||||
className="w-full border rounded px-2 py-1.5 mt-1"
|
className="w-full border rounded px-2 py-1.5 mt-1"
|
||||||
value={formData.tabelaFree}
|
value={formData.tabelaFree || ""}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setFormData({...formData, tabelaFree: e.target.value});
|
setFormData({...formData, tabelaFree: e.target.value});
|
||||||
// Also set Tipo Servico to match? Or keep them separate?
|
// Also set Tipo Servico to match? Or keep them separate?
|
||||||
|
|
@ -1311,20 +1311,28 @@ const Finance: React.FC = () => {
|
||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
<input type="text" className="w-full border rounded px-2 py-1.5 mt-1"
|
<input type="text" className="w-full border rounded px-2 py-1.5 mt-1"
|
||||||
value={formData.tabelaFree} onChange={e => setFormData({...formData, tabelaFree: e.target.value})}
|
value={formData.tabelaFree || ""} onChange={e => setFormData({...formData, tabelaFree: e.target.value})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700">Valor Free (R$)</label>
|
<label className="block text-xs font-medium text-gray-700">Valor Free (R$)</label>
|
||||||
<input type="number" step="0.01" className="w-full border rounded px-2 py-1.5 mt-1"
|
<input type="number" step="0.01" className="w-full border rounded px-2 py-1.5 mt-1"
|
||||||
value={formData.valorFree} onChange={e => setFormData({...formData, valorFree: parseFloat(e.target.value)})}
|
value={formData.valorFree ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setFormData({...formData, valorFree: val === "" ? null : parseFloat(val)});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-700">Valor Extra (R$)</label>
|
<label className="block text-xs font-medium text-gray-700">Valor Extra (R$)</label>
|
||||||
<input type="number" step="0.01" className="w-full border rounded px-2 py-1.5 mt-1"
|
<input type="number" step="0.01" className="w-full border rounded px-2 py-1.5 mt-1"
|
||||||
value={formData.valorExtra} onChange={e => setFormData({...formData, valorExtra: parseFloat(e.target.value)})}
|
value={formData.valorExtra ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setFormData({...formData, valorExtra: val === "" ? null : parseFloat(val)});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1391,6 +1391,31 @@ export async function getProfessionalFinancialStatement(token: string): Promise<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Busca o preço base para um dado evento e serviço
|
||||||
|
*/
|
||||||
|
export async function getPrice(token: string, eventName: string, serviceName: string): Promise<ApiResponse<{ valor: number }>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/finance/price?event=${encodeURIComponent(eventName)}&service=${encodeURIComponent(serviceName)}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
"x-regiao": localStorage.getItem("photum_selected_region") || "SP"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return { data, error: null, isBackendDown: false };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching price:", error);
|
||||||
|
return { data: null, error: error instanceof Error ? error.message : "Erro desconhecido", isBackendDown: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const setCoordinator = async (token: string, eventId: string, professionalId: string, isCoordinator: boolean) => {
|
export const setCoordinator = async (token: string, eventId: string, professionalId: string, isCoordinator: boolean) => {
|
||||||
try {
|
try {
|
||||||
const region = localStorage.getItem("photum_selected_region") || "SP";
|
const region = localStorage.getItem("photum_selected_region") || "SP";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue