From da3754068b5d38323d91867f5e61784bb63a1cb2 Mon Sep 17 00:00:00 2001 From: NANDO9322 Date: Thu, 12 Feb 2026 09:51:27 -0300 Subject: [PATCH] feat:(finance) Implementado sistema de edicao em massa para gastos extras --- backend/cmd/api/main.go | 1 + backend/internal/db/generated/finance.sql.go | 35 ++++ backend/internal/db/queries/finance.sql | 10 + backend/internal/finance/handler.go | 23 +++ backend/internal/finance/service.go | 26 +++ frontend/pages/Finance.tsx | 186 +++++++++++++++---- 6 files changed, 240 insertions(+), 41 deletions(-) create mode 100644 backend/internal/db/generated/finance.sql.go create mode 100644 backend/internal/db/queries/finance.sql diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index 8c08998..d37e113 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -284,6 +284,7 @@ func main() { financeGroup.GET("/fot-search", financeHandler.SearchFot) financeGroup.GET("/professionals", financeHandler.SearchProfessionals) financeGroup.GET("/price", financeHandler.GetPrice) + financeGroup.POST("/bulk/extras", financeHandler.BulkUpdate) financeGroup.PUT("/:id", financeHandler.Update) financeGroup.DELETE("/:id", financeHandler.Delete) } diff --git a/backend/internal/db/generated/finance.sql.go b/backend/internal/db/generated/finance.sql.go new file mode 100644 index 0000000..a7d5b5b --- /dev/null +++ b/backend/internal/db/generated/finance.sql.go @@ -0,0 +1,35 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: finance.sql + +package generated + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const bulkUpdateExtras = `-- name: BulkUpdateExtras :exec +UPDATE financial_transactions +SET + valor_extra = COALESCE(valor_extra, 0) + $1, + descricao_extra = CASE + WHEN descricao_extra IS NULL OR descricao_extra = '' THEN $2 + ELSE descricao_extra || ' + ' || $2 + END, + total_pagar = valor_free + (COALESCE(valor_extra, 0) + $1) +WHERE id = ANY($3::uuid[]) +` + +type BulkUpdateExtrasParams struct { + ValorExtra pgtype.Numeric `json:"valor_extra"` + DescricaoExtra pgtype.Text `json:"descricao_extra"` + Ids []pgtype.UUID `json:"ids"` +} + +func (q *Queries) BulkUpdateExtras(ctx context.Context, arg BulkUpdateExtrasParams) error { + _, err := q.db.Exec(ctx, bulkUpdateExtras, arg.ValorExtra, arg.DescricaoExtra, arg.Ids) + return err +} diff --git a/backend/internal/db/queries/finance.sql b/backend/internal/db/queries/finance.sql new file mode 100644 index 0000000..13a1abf --- /dev/null +++ b/backend/internal/db/queries/finance.sql @@ -0,0 +1,10 @@ +-- name: BulkUpdateExtras :exec +UPDATE financial_transactions +SET + valor_extra = COALESCE(valor_extra, 0) + @valor_extra, + descricao_extra = CASE + WHEN descricao_extra IS NULL OR descricao_extra = '' THEN @descricao_extra + ELSE descricao_extra || ' + ' || @descricao_extra + END, + total_pagar = valor_free + (COALESCE(valor_extra, 0) + @valor_extra) +WHERE id = ANY(@ids::uuid[]); diff --git a/backend/internal/finance/handler.go b/backend/internal/finance/handler.go index 3c2deaa..b4c1dac 100644 --- a/backend/internal/finance/handler.go +++ b/backend/internal/finance/handler.go @@ -35,6 +35,29 @@ type TransactionRequest struct { PgtoOk bool `json:"pgto_ok"` } +type BulkUpdateRequest struct { + IDs []string `json:"ids"` + ValorExtra float64 `json:"valor_extra"` + DescricaoExtra string `json:"descricao_extra"` +} + +func (h *Handler) BulkUpdate(c *gin.Context) { + var req BulkUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + regiao := c.GetString("regiao") + err := h.service.BulkUpdate(c.Request.Context(), req.IDs, req.ValorExtra, req.DescricaoExtra, regiao) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "updated successfully"}) +} + // Helper: Parse Date String to pgtype.Date func parseDate(dateStr string) pgtype.Date { if dateStr == "" { diff --git a/backend/internal/finance/service.go b/backend/internal/finance/service.go index bfad04d..efd2901 100644 --- a/backend/internal/finance/service.go +++ b/backend/internal/finance/service.go @@ -49,6 +49,32 @@ func (s *Service) Update(ctx context.Context, params generated.UpdateTransaction return txn, nil } +func (s *Service) BulkUpdate(ctx context.Context, ids []string, valorExtra float64, descricaoExtra string, regiao string) error { + // Convert string IDs to pgtype.UUID + var pgIDs []pgtype.UUID + for _, id := range ids { + var u pgtype.UUID + if err := u.Scan(id); err == nil { + pgIDs = append(pgIDs, u) + } + } + + if len(pgIDs) == 0 { + return nil + } + + err := s.queries.BulkUpdateExtras(ctx, generated.BulkUpdateExtrasParams{ + ValorExtra: toNumeric(valorExtra), + DescricaoExtra: pgtype.Text{String: descricaoExtra, Valid: true}, // Allow empty description? Yes. + Ids: pgIDs, + }) + if err != nil { + return err + } + + return nil +} + func (s *Service) Delete(ctx context.Context, id pgtype.UUID, regiao string) error { // First fetch to get FotID txn, err := s.queries.GetTransaction(ctx, generated.GetTransactionParams{ diff --git a/frontend/pages/Finance.tsx b/frontend/pages/Finance.tsx index 0c685ac..23abb1a 100644 --- a/frontend/pages/Finance.tsx +++ b/frontend/pages/Finance.tsx @@ -130,6 +130,14 @@ const Finance: React.FC = () => { const [proFunctions, setProFunctions] = useState([]); // Functions of selected professional const [selectedProId, setSelectedProId] = useState(null); + // Bulk Edit State + const [selectedIds, setSelectedIds] = useState>(new Set()); + const [showBulkEditModal, setShowBulkEditModal] = useState(false); + const [bulkFormData, setBulkFormData] = useState({ + valorExtra: 0, + descricaoExtra: "", + }); + // Validations const validateCpf = (cpf: string) => { // Simple length check for now, can be enhanced @@ -428,46 +436,7 @@ const Finance: React.FC = () => { } }; - // Auto-Pricing Effect - useEffect(() => { - const fetchPrice = async () => { - if (!formData.tipoEvento || !formData.tipoServico) return; - - // Skip auto-pricing if we are initializing the edit form - if (isEditInitializing.current) { - isEditInitializing.current = false; - return; - } - - // Don't overwrite if we are editing and the types haven't changed from the original - if (selectedTransaction && - formData.tipoEvento === selectedTransaction.tipoEvento && - formData.tipoServico === selectedTransaction.tipoServico) { - return; - } - const token = localStorage.getItem("token"); - if (!token) return; - - try { - const res = await fetch(`${API_BASE_URL}/api/finance/price?event=${encodeURIComponent(formData.tipoEvento)}&service=${encodeURIComponent(formData.tipoServico)}`, { - headers: { - "Authorization": `Bearer ${token}`, - "x-regiao": localStorage.getItem("photum_selected_region") || "SP" - } - }); - if (res.ok) { - const data = await res.json(); - if (data.valor !== undefined) { - setFormData(prev => ({ ...prev, valorFree: data.valor })); - } - } - } catch (e) { - console.error(e); - } - }; - fetchPrice(); - }, [formData.tipoEvento, formData.tipoServico, selectedTransaction]); const handleProSearch = async (query: string) => { setProQuery(query); @@ -499,6 +468,64 @@ const Finance: React.FC = () => { } }; + // Bulk Selection Logic + const toggleSelection = (id: string) => { + const newSet = new Set(selectedIds); + if (newSet.has(id)) { + newSet.delete(id); + } else { + newSet.add(id); + } + setSelectedIds(newSet); + }; + + const toggleSelectAll = () => { + if (selectedIds.size === sortedTransactions.length && sortedTransactions.length > 0) { + setSelectedIds(new Set()); + } else { + const newSet = new Set(sortedTransactions.map(t => t.id).filter(id => id)); // ensure valid IDs + setSelectedIds(newSet); + } + }; + + const handleBulkUpdate = async () => { + if (selectedIds.size === 0) return; + if (!confirm(`Confirma atualização de ${selectedIds.size} itens?`)) return; + + const token = localStorage.getItem("token"); + if (!token) return; + + try { + const res = await fetch(`${API_BASE_URL}/api/finance/bulk/extras`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, + "x-regiao": localStorage.getItem("photum_selected_region") || "SP" + }, + body: JSON.stringify({ + ids: Array.from(selectedIds), + valor_extra: bulkFormData.valorExtra, + descricao_extra: bulkFormData.descricaoExtra + }) + }); + + if (res.ok) { + alert("Itens atualizados com sucesso!"); + setShowBulkEditModal(false); + setSelectedIds(new Set()); + setBulkFormData({ valorExtra: 0, descricaoExtra: "" }); + loadTransactions(); // Refresh list + } else { + const err = await res.json(); + alert(`Erro ao atualizar: ${err.error || "Erro desconhecido"}`); + } + } catch (e) { + console.error(e); + alert("Erro de conexão ao atualizar."); + } + }; + const selectProfessional = (pro: any) => { // Parse functions let funcs = []; @@ -582,8 +609,9 @@ const Finance: React.FC = () => { console.error("Error fetching FOT details for edit:", err); } } else if (t.fot) { - // Fallback if no ID but we have the number + // Fallback if no ID but we have the number (Legacy records) setFotQuery(String(t.fot)); + handleAutoFill(t.fot); // Fetch details using FOT number } // Fetch professional functions if professional name is present @@ -1018,6 +1046,19 @@ const Finance: React.FC = () => {
+ {/* Bulk Actions Bar */} + {selectedIds.size > 0 && ( +
+ {selectedIds.size} itens selecionados + +
+ )} +
handleScroll('table')} @@ -1026,6 +1067,14 @@ const Finance: React.FC = () => { + {[ { label: "FOT", key: "fot" }, { label: "Data Evento", key: "data" }, @@ -1108,9 +1157,17 @@ const Finance: React.FC = () => { return ( handleEdit(t)} > + @@ -1433,6 +1490,53 @@ const Finance: React.FC = () => { )} + {/* Bulk Edit Modal */} + {showBulkEditModal && ( +
+
+
+

Editar {selectedIds.size} Itens

+ +
+
+
+ Atenção: Os valores inseridos serão adicionados aos valores existentes. A descrição será concatenada. +
+ +
+ + setBulkFormData({...bulkFormData, valorExtra: parseFloat(e.target.value) || 0})} + placeholder="Ex: 50.00" + /> +
+ +
+ + setBulkFormData({...bulkFormData, descricaoExtra: e.target.value})} + placeholder="Ex: + Ajuda de Custo" + /> +
+
+
+ + +
+
+
+ )} ); };
+ 0 && selectedIds.size === sortedTransactions.length} + onChange={toggleSelectAll} + className="rounded text-blue-600 focus:ring-blue-500" + /> +
e.stopPropagation()}> + { e.stopPropagation(); toggleSelection(t.id!); }} + className="rounded text-blue-600 focus:ring-blue-500" + /> + {t.fot || "?"} {t.data} {t.curso}