fix: correção de duplicidade de preços e melhorias na UX financeira

- Backend:
  - Ajustada query `GetStandardPrice` para filtrar por região e ordenar por data.
  - Corrigido `SetPrice` para usar o contexto de região, evitando duplicatas.
  - Script de limpeza executado para remover entradas duplicadas no banco.

- Frontend (Financeiro):
  - Reset completo do formulário ao abrir "Nova Transação" (limpa busca FOT e eventos).
  - Preenchimento automático da "Data Evento" ao selecionar um evento encontrado pela busca FOT.
  - Correção na lógica de busca de preço para usar nome da Função (`tabelaFree`).
This commit is contained in:
NANDO9322 2026-02-11 10:20:46 -03:00
parent 6382145442
commit 95a4e441c1
5 changed files with 69 additions and 10 deletions

View file

@ -118,7 +118,11 @@ 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 ILIKE $1 AND f.nome ILIKE $2 AND te.regiao = $3 WHERE te.nome ILIKE $1
AND f.nome ILIKE $2
AND te.regiao = $3
AND p.regiao = $3
ORDER BY p.criado_em DESC
LIMIT 1 LIMIT 1
` `

View file

@ -37,7 +37,11 @@ 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 ILIKE $1 AND f.nome ILIKE $2 AND te.regiao = @regiao WHERE te.nome ILIKE $1
AND f.nome ILIKE $2
AND te.regiao = @regiao
AND p.regiao = @regiao
ORDER BY p.criado_em DESC
LIMIT 1; LIMIT 1;
-- name: GetTipoEventoByNome :one -- name: GetTipoEventoByNome :one

View file

@ -137,7 +137,8 @@ func (h *Handler) SetPrice(c *gin.Context) {
return return
} }
_, err := h.service.SetPrice(c.Request.Context(), req) regiao := c.GetString("regiao")
_, err := h.service.SetPrice(c.Request.Context(), req, regiao)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return

View file

@ -86,7 +86,7 @@ type PriceInput struct {
Valor float64 `json:"valor"` Valor float64 `json:"valor"`
} }
func (s *Service) SetPrice(ctx context.Context, input PriceInput) (*generated.PrecosTiposEvento, error) { func (s *Service) SetPrice(ctx context.Context, input PriceInput, regiao string) (*generated.PrecosTiposEvento, error) {
eventoUUID, err := uuid.Parse(input.TipoEventoID) eventoUUID, err := uuid.Parse(input.TipoEventoID)
if err != nil { if err != nil {
return nil, errors.New("invalid tipo_evento_id") return nil, errors.New("invalid tipo_evento_id")
@ -100,6 +100,7 @@ func (s *Service) SetPrice(ctx context.Context, input PriceInput) (*generated.Pr
TipoEventoID: pgtype.UUID{Bytes: eventoUUID, Valid: true}, TipoEventoID: pgtype.UUID{Bytes: eventoUUID, Valid: true},
FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: true}, FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: true},
Valor: toPgNumeric(input.Valor), Valor: toPgNumeric(input.Valor),
Regiao: pgtype.Text{String: regiao, Valid: true},
}) })
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -526,7 +526,7 @@ const Finance: React.FC = () => {
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
tipoEvento: ev.tipo_evento_nome, tipoEvento: ev.tipo_evento_nome,
// If event has date, we could pre-fill? User request suggests keeping it flexible or maybe they didn't ask explicitly. data: ev.data_evento ? ev.data_evento.split("T")[0] : prev.data,
})); }));
setShowEventSelector(false); setShowEventSelector(false);
}; };
@ -740,6 +740,53 @@ const Finance: React.FC = () => {
setFormData((prev) => ({ ...prev, totalPagar: vFree + vExtra })); setFormData((prev) => ({ ...prev, totalPagar: vFree + vExtra }));
}, [formData.valorFree, formData.valorExtra]); }, [formData.valorFree, formData.valorExtra]);
// Fetch Price on Event/Service/Function Change
useEffect(() => {
if (isEditInitializing.current) {
isEditInitializing.current = false;
return;
}
// Use Tabela Free (Function) if available, otherwise Tipo Servico
const serviceParam = formData.tabelaFree || formData.tipoServico;
if (!formData.tipoEvento || !serviceParam) return;
const fetchPrice = async () => {
const token = localStorage.getItem("token");
if (!token) return;
try {
// Use the new endpoint
const res = await fetch(`${API_BASE_URL}/api/finance/price?event=${encodeURIComponent(formData.tipoEvento!)}&service=${encodeURIComponent(serviceParam)}`, {
headers: {
"Authorization": `Bearer ${token}`,
"x-regiao": localStorage.getItem("photum_selected_region") || "SP"
}
});
if (res.ok) {
const data = await res.json();
// Update Valor Free if different
// Check if data.valor is a number
const price = Number(data.valor);
if (!isNaN(price) && price !== formData.valorFree) {
setFormData(prev => ({ ...prev, valorFree: price }));
}
}
} catch (err) {
console.error("Error fetching price:", err);
}
};
// Debounce slightly or just call
const timeoutId = setTimeout(() => {
fetchPrice();
}, 300);
return () => clearTimeout(timeoutId);
}, [formData.tipoEvento, formData.tipoServico, formData.tabelaFree]);
const handleExportCSV = () => { const handleExportCSV = () => {
if (sortedTransactions.length === 0) { if (sortedTransactions.length === 0) {
alert("Nenhum dado para exportar."); alert("Nenhum dado para exportar.");
@ -866,6 +913,8 @@ const Finance: React.FC = () => {
pgtoOk: false, pgtoOk: false,
}); });
setFotFound(false); setFotFound(false);
setFotQuery(""); // Clear FOT search query
setFotEvents([]); // Clear found events
setProFunctions([]); // Clear professional functions setProFunctions([]); // Clear professional functions
setSelectedTransaction(null); // Ensure no transaction is selected for add setSelectedTransaction(null); // Ensure no transaction is selected for add
setShowAddModal(true); setShowAddModal(true);