Merge pull request #54 from rede5/ajustar-notificacao-whatsapp-por-regiao-11345353370
Ajustar notificacao whatsapp por regiao 11345353370
This commit is contained in:
commit
cc7c7dccc4
7 changed files with 193 additions and 37 deletions
|
|
@ -71,7 +71,12 @@ func main() {
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
// Initialize services
|
// Initialize services
|
||||||
notificationService := notification.NewService()
|
notificationService := notification.NewService(
|
||||||
|
cfg.EvolutionApiUrl,
|
||||||
|
cfg.EvolutionApiKey,
|
||||||
|
cfg.WhatsappInstanceSP,
|
||||||
|
cfg.WhatsappInstanceMG,
|
||||||
|
)
|
||||||
profissionaisService := profissionais.NewService(queries)
|
profissionaisService := profissionais.NewService(queries)
|
||||||
financeService := finance.NewService(queries, profissionaisService)
|
financeService := finance.NewService(queries, profissionaisService)
|
||||||
authService := auth.NewService(queries, profissionaisService, cfg)
|
authService := auth.NewService(queries, profissionaisService, cfg)
|
||||||
|
|
|
||||||
|
|
@ -448,7 +448,7 @@ func (s *Service) AssignProfessional(ctx context.Context, agendaID uuid.UUID, pr
|
||||||
baseUrl,
|
baseUrl,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := s.notification.SendWhatsApp(prof.Whatsapp.String, msg); err != nil {
|
if err := s.notification.SendWhatsApp(prof.Whatsapp.String, msg, agenda.Regiao.String); err != nil {
|
||||||
log.Printf("[Notification] Falha ao enviar WhatsApp para %s: %v", prof.Nome, err)
|
log.Printf("[Notification] Falha ao enviar WhatsApp para %s: %v", prof.Nome, err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
@ -702,8 +702,8 @@ func (s *Service) NotifyLogistics(ctx context.Context, agendaID uuid.UUID, passe
|
||||||
tipoEventoNome,
|
tipoEventoNome,
|
||||||
logisticaMsg,
|
logisticaMsg,
|
||||||
)
|
)
|
||||||
|
// Passa a agendaRegiao para definir o numero
|
||||||
if err := s.notification.SendWhatsApp(phone, msg); err != nil {
|
if err := s.notification.SendWhatsApp(phone, msg, agenda.Regiao.String); err != nil {
|
||||||
// Não logar erro para todos se for falha de validação de numero, mas logar warning
|
// Não logar erro para todos se for falha de validação de numero, mas logar warning
|
||||||
log.Printf("[Notification] Erro ao enviar para %s: %v", p.Nome, err)
|
log.Printf("[Notification] Erro ao enviar para %s: %v", p.Nome, err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ type Config struct {
|
||||||
S3Bucket string
|
S3Bucket string
|
||||||
S3Region string
|
S3Region string
|
||||||
FrontendURL string
|
FrontendURL string
|
||||||
|
EvolutionApiUrl string
|
||||||
|
EvolutionApiKey string
|
||||||
|
WhatsappInstanceSP string
|
||||||
|
WhatsappInstanceMG string
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig() *Config {
|
func LoadConfig() *Config {
|
||||||
|
|
@ -48,6 +52,10 @@ func LoadConfig() *Config {
|
||||||
S3Bucket: getEnv("S3_BUCKET", ""),
|
S3Bucket: getEnv("S3_BUCKET", ""),
|
||||||
S3Region: getEnv("S3_REGION", "nyc1"),
|
S3Region: getEnv("S3_REGION", "nyc1"),
|
||||||
FrontendURL: getEnv("FRONTEND_URL", "http://localhost:3000"),
|
FrontendURL: getEnv("FRONTEND_URL", "http://localhost:3000"),
|
||||||
|
EvolutionApiUrl: getEnv("EVOLUTION_API_URL", "https://others-evolution-api.nsowe9.easypanel.host"),
|
||||||
|
EvolutionApiKey: getEnv("EVOLUTION_API_KEY", "429683C4C977415CAAFCCE10F7D57E11"),
|
||||||
|
WhatsappInstanceSP: getEnv("WHATSAPP_INSTANCE_SP", "NANDO"),
|
||||||
|
WhatsappInstanceMG: getEnv("WHATSAPP_INSTANCE_MG", "NANDO"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
apiURL string
|
apiURL string
|
||||||
apiKey string
|
apiKey string
|
||||||
instance string
|
instanceSP string
|
||||||
|
instanceMG string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService() *Service {
|
func NewService(apiURL, apiKey, instanceSP, instanceMG string) *Service {
|
||||||
// Hardcoded configuration as per user request
|
|
||||||
return &Service{
|
return &Service{
|
||||||
apiURL: "https://others-evolution-api.nsowe9.easypanel.host",
|
apiURL: apiURL,
|
||||||
apiKey: "429683C4C977415CAAFCCE10F7D57E11",
|
apiKey: apiKey,
|
||||||
instance: "NANDO",
|
instanceSP: instanceSP,
|
||||||
|
instanceMG: instanceMG,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,13 +30,18 @@ type MessageRequest struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SendWhatsApp(number string, message string) error {
|
func (s *Service) SendWhatsApp(number string, message string, regiao string) error {
|
||||||
cleanNumber := cleanPhoneNumber(number)
|
cleanNumber := cleanPhoneNumber(number)
|
||||||
if cleanNumber == "" {
|
if cleanNumber == "" {
|
||||||
return fmt.Errorf("número de telefone inválido ou vazio")
|
return fmt.Errorf("número de telefone inválido ou vazio")
|
||||||
}
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/message/sendText/%s", s.apiURL, s.instance)
|
instance := s.instanceSP // default
|
||||||
|
if regiao == "MG" {
|
||||||
|
instance = s.instanceMG
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/message/sendText/%s", s.apiURL, instance)
|
||||||
|
|
||||||
payload := MessageRequest{
|
payload := MessageRequest{
|
||||||
Number: cleanNumber,
|
Number: cleanNumber,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { createContext, useContext, useState, ReactNode, useEffect } from "react";
|
import React, { createContext, useContext, useState, ReactNode, useEffect } from "react";
|
||||||
import { useAuth } from "./AuthContext";
|
import { useAuth } from "./AuthContext";
|
||||||
import { getPendingUsers, approveUser as apiApproveUser, getProfessionals, assignProfessional as apiAssignProfessional, removeProfessional as apiRemoveProfessional, updateEventStatus as apiUpdateStatus, updateAssignmentStatus as apiUpdateAssignmentStatus, updateAgenda as apiUpdateAgenda } from "../services/apiService";
|
import { getPendingUsers, approveUser as apiApproveUser, getProfessionals, assignProfessional as apiAssignProfessional, removeProfessional as apiRemoveProfessional, updateEventStatus as apiUpdateStatus, updateAssignmentStatus as apiUpdateAssignmentStatus, updateAgenda as apiUpdateAgenda } from "../services/apiService";
|
||||||
|
import { useRegion } from "./RegionContext";
|
||||||
import {
|
import {
|
||||||
EventData,
|
EventData,
|
||||||
EventStatus,
|
EventStatus,
|
||||||
|
|
@ -621,6 +622,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const { token, user } = useAuth(); // Consume Auth Context
|
const { token, user } = useAuth(); // Consume Auth Context
|
||||||
|
const { currentRegion, isRegionReady } = useRegion();
|
||||||
const [events, setEvents] = useState<EventData[]>([]);
|
const [events, setEvents] = useState<EventData[]>([]);
|
||||||
const [institutions, setInstitutions] =
|
const [institutions, setInstitutions] =
|
||||||
useState<Institution[]>(INITIAL_INSTITUTIONS);
|
useState<Institution[]>(INITIAL_INSTITUTIONS);
|
||||||
|
|
@ -639,7 +641,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
// Use token from context or fallback to localStorage if context not ready (though context is preferred sources of truth)
|
// Use token from context or fallback to localStorage if context not ready (though context is preferred sources of truth)
|
||||||
const visibleToken = token || localStorage.getItem("token");
|
const visibleToken = token || localStorage.getItem("token");
|
||||||
|
|
||||||
if (visibleToken) {
|
if (visibleToken && isRegionReady) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
// Import dynamic to avoid circular dependency if any, or just use imported service
|
// Import dynamic to avoid circular dependency if any, or just use imported service
|
||||||
|
|
@ -747,7 +749,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchEvents();
|
fetchEvents();
|
||||||
}, [token, refreshTrigger]); // React to token change and manual refresh
|
}, [token, refreshTrigger, currentRegion, isRegionReady]); // React to context changes
|
||||||
|
|
||||||
const refreshEvents = async () => {
|
const refreshEvents = async () => {
|
||||||
setRefreshTrigger(prev => prev + 1);
|
setRefreshTrigger(prev => prev + 1);
|
||||||
|
|
@ -795,7 +797,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchProfs = async () => {
|
const fetchProfs = async () => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (token) {
|
if (token && isRegionReady) {
|
||||||
try {
|
try {
|
||||||
const result = await getProfessionals(token);
|
const result = await getProfessionals(token);
|
||||||
if (result.data) {
|
if (result.data) {
|
||||||
|
|
@ -850,7 +852,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchProfs();
|
fetchProfs();
|
||||||
}, [token]);
|
}, [token, currentRegion, isRegionReady]);
|
||||||
|
|
||||||
const addEvent = async (event: any) => {
|
const addEvent = async (event: any) => {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,14 @@ interface RegionContextType {
|
||||||
currentRegion: string;
|
currentRegion: string;
|
||||||
setRegion: (region: string) => void;
|
setRegion: (region: string) => void;
|
||||||
availableRegions: string[];
|
availableRegions: string[];
|
||||||
|
isRegionReady: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RegionContext = createContext<RegionContextType>({
|
const RegionContext = createContext<RegionContextType>({
|
||||||
currentRegion: "SP",
|
currentRegion: "SP",
|
||||||
setRegion: () => {},
|
setRegion: () => {},
|
||||||
availableRegions: [],
|
availableRegions: [],
|
||||||
|
isRegionReady: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RegionProvider: React.FC<{ children: React.ReactNode }> = ({
|
export const RegionProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
|
|
@ -32,9 +34,18 @@ export const RegionProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
// Let's assume public = SP only or no switcher.
|
// Let's assume public = SP only or no switcher.
|
||||||
// BUT: If user is logged out, they shouldn't see switcher anyway.
|
// BUT: If user is logged out, they shouldn't see switcher anyway.
|
||||||
const [availableRegions, setAvailableRegions] = useState<string[]>(["SP"]);
|
const [availableRegions, setAvailableRegions] = useState<string[]>(["SP"]);
|
||||||
|
const [isRegionReady, setIsRegionReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("RegionContext Debug:", { user, allowedRegions: user?.allowedRegions });
|
console.log("RegionContext Debug:", { user, allowedRegions: user?.allowedRegions });
|
||||||
|
// If not logged in or user still fetching, wait (but if public page, we could mark ready. For now, mark ready if no token or after user loads)
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (token && !user) {
|
||||||
|
// Wait for user to load to evaluate regions
|
||||||
|
setIsRegionReady(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (user && user.allowedRegions && user.allowedRegions.length > 0) {
|
if (user && user.allowedRegions && user.allowedRegions.length > 0) {
|
||||||
setAvailableRegions(user.allowedRegions);
|
setAvailableRegions(user.allowedRegions);
|
||||||
|
|
||||||
|
|
@ -49,7 +60,9 @@ export const RegionProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
// Fallback or Public
|
// Fallback or Public
|
||||||
setAvailableRegions(["SP"]);
|
setAvailableRegions(["SP"]);
|
||||||
}
|
}
|
||||||
}, [user, user?.allowedRegions, currentRegion]);
|
|
||||||
|
setIsRegionReady(true);
|
||||||
|
}, [user, currentRegion]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(REGION_KEY, currentRegion);
|
localStorage.setItem(REGION_KEY, currentRegion);
|
||||||
|
|
@ -67,7 +80,7 @@ export const RegionProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RegionContext.Provider
|
<RegionContext.Provider
|
||||||
value={{ currentRegion, setRegion, availableRegions }}
|
value={{ currentRegion, setRegion, availableRegions, isRegionReady }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</RegionContext.Provider>
|
</RegionContext.Provider>
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,21 @@ const ProfessionalStatement: React.FC = () => {
|
||||||
const [selectedTransaction, setSelectedTransaction] = useState<FinancialTransactionDTO | null>(null);
|
const [selectedTransaction, setSelectedTransaction] = useState<FinancialTransactionDTO | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// Filter States
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
data: "",
|
||||||
|
nome: "",
|
||||||
|
tipo: "",
|
||||||
|
empresa: "",
|
||||||
|
status: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const [dateFilters, setDateFilters] = useState({
|
||||||
|
startDate: "",
|
||||||
|
endDate: ""
|
||||||
|
});
|
||||||
|
const [showDateFilters, setShowDateFilters] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token) {
|
if (token) {
|
||||||
fetchStatement();
|
fetchStatement();
|
||||||
|
|
@ -82,6 +97,43 @@ const ProfessionalStatement: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// derived filtered state
|
||||||
|
const transactions = data?.transactions || [];
|
||||||
|
const filteredTransactions = transactions.filter(t => {
|
||||||
|
// String Column Filters
|
||||||
|
if (filters.data && !t.data_evento.toLowerCase().includes(filters.data.toLowerCase())) return false;
|
||||||
|
if (filters.nome && !t.nome_evento.toLowerCase().includes(filters.nome.toLowerCase())) return false;
|
||||||
|
if (filters.tipo && !t.tipo_evento.toLowerCase().includes(filters.tipo.toLowerCase())) return false;
|
||||||
|
if (filters.empresa && !t.empresa.toLowerCase().includes(filters.empresa.toLowerCase())) return false;
|
||||||
|
if (filters.status && !t.status.toLowerCase().includes(filters.status.toLowerCase())) return false;
|
||||||
|
|
||||||
|
// Date Range Filter logic
|
||||||
|
if (dateFilters.startDate || dateFilters.endDate) {
|
||||||
|
// Parse DD/MM/YYYY into JS Date if possible
|
||||||
|
const [d, m, y] = t.data_evento.split('/');
|
||||||
|
if (d && m && y) {
|
||||||
|
const eventDateObj = new Date(parseInt(y), parseInt(m) - 1, parseInt(d));
|
||||||
|
|
||||||
|
if (dateFilters.startDate) {
|
||||||
|
const [sy, sm, sd] = dateFilters.startDate.split('-');
|
||||||
|
const startObj = new Date(parseInt(sy), parseInt(sm) - 1, parseInt(sd));
|
||||||
|
if (eventDateObj < startObj) return false;
|
||||||
|
}
|
||||||
|
if (dateFilters.endDate) {
|
||||||
|
const [ey, em, ed] = dateFilters.endDate.split('-');
|
||||||
|
const endObj = new Date(parseInt(ey), parseInt(em) - 1, parseInt(ed));
|
||||||
|
// Set end of day for precise comparison
|
||||||
|
endObj.setHours(23, 59, 59, 999);
|
||||||
|
if (eventDateObj > endObj) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredTotalSum = filteredTransactions.reduce((acc, curr) => acc + curr.valor_recebido, 0);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="p-8 text-center text-gray-500">Carregando extrato...</div>;
|
return <div className="p-8 text-center text-gray-500">Carregando extrato...</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -126,36 +178,103 @@ const ProfessionalStatement: React.FC = () => {
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-100 overflow-hidden">
|
<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">
|
<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>
|
<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">
|
<div className="flex gap-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<button
|
||||||
<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" />
|
onClick={() => setShowDateFilters(!showDateFilters)}
|
||||||
</svg>
|
className="text-sm text-gray-600 hover:text-gray-900 flex items-center gap-2"
|
||||||
Exportar
|
>
|
||||||
</button>
|
{showDateFilters ? "▼" : "▶"} Filtros Avançados de Data
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Advanced Date Filters */}
|
||||||
|
{showDateFilters && (
|
||||||
|
<div className="bg-white border-b border-gray-100 p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">Data Início</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="w-full border rounded px-3 py-2 text-sm"
|
||||||
|
value={dateFilters.startDate}
|
||||||
|
onChange={e => setDateFilters({...dateFilters, startDate: e.target.value})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">Data Final</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="w-full border rounded px-3 py-2 text-sm"
|
||||||
|
value={dateFilters.endDate}
|
||||||
|
onChange={e => setDateFilters({...dateFilters, endDate: e.target.value})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(dateFilters.startDate || dateFilters.endDate) && (
|
||||||
|
<div className="col-span-1 md:col-span-2 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => setDateFilters({ startDate: "", endDate: "" })}
|
||||||
|
className="text-sm text-red-600 hover:text-red-800 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Limpar Filtros de Data
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-sm text-left">
|
<table className="w-full text-sm text-left">
|
||||||
<thead className="bg-gray-50 text-gray-500 font-medium">
|
<thead className="bg-gray-50 text-gray-500 font-medium">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-4">Data Evento</th>
|
<th className="px-6 py-4">
|
||||||
<th className="px-6 py-4">Nome Evento</th>
|
<div className="flex flex-col gap-1">
|
||||||
<th className="px-6 py-4">Tipo Evento</th>
|
<span>Data Evento</span>
|
||||||
<th className="px-6 py-4">Empresa</th>
|
<input className="w-full text-xs box-border border border-gray-200 rounded px-2 py-1 font-normal text-gray-900" placeholder="Filtrar" value={filters.data} onChange={e => setFilters({...filters, data: e.target.value})} />
|
||||||
<th className="px-6 py-4">Valor Recebido</th>
|
</div>
|
||||||
<th className="px-6 py-4">Data Pagamento</th>
|
</th>
|
||||||
<th className="px-6 py-4 text-center">Status</th>
|
<th className="px-6 py-4">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span>Nome Evento</span>
|
||||||
|
<input className="w-full text-xs box-border border border-gray-200 rounded px-2 py-1 font-normal text-gray-900" placeholder="Filtrar" value={filters.nome} onChange={e => setFilters({...filters, nome: e.target.value})} />
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span>Tipo Evento</span>
|
||||||
|
<input className="w-full text-xs box-border border border-gray-200 rounded px-2 py-1 font-normal text-gray-900" placeholder="Filtrar" value={filters.tipo} onChange={e => setFilters({...filters, tipo: e.target.value})} />
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span>Empresa</span>
|
||||||
|
<input className="w-full text-xs box-border border border-gray-200 rounded px-2 py-1 font-normal text-gray-900" placeholder="Filtrar" value={filters.empresa} onChange={e => setFilters({...filters, empresa: e.target.value})} />
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4 align-top">
|
||||||
|
<div className="whitespace-nowrap pt-1">Valor Recebido</div>
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4 align-top">
|
||||||
|
<div className="whitespace-nowrap pt-1">Data Pagamento</div>
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4 text-center">
|
||||||
|
<div className="flex flex-col gap-1 justify-center items-center">
|
||||||
|
<span>Status</span>
|
||||||
|
<input className="w-20 text-xs box-border border border-gray-200 rounded px-2 py-1 font-normal text-gray-900 text-center" placeholder="Pago..." value={filters.status} onChange={e => setFilters({...filters, status: e.target.value})} />
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100">
|
<tbody className="divide-y divide-gray-100">
|
||||||
{(data.transactions || []).length === 0 ? (
|
{filteredTransactions.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={7} className="px-6 py-8 text-center text-gray-500">
|
<td colSpan={7} className="px-6 py-8 text-center text-gray-500">
|
||||||
Nenhum pagamento registrado.
|
Nenhum pagamento encontrado para os filtros.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
(data.transactions || []).map((t) => (
|
filteredTransactions.map((t) => (
|
||||||
<tr
|
<tr
|
||||||
key={t.id}
|
key={t.id}
|
||||||
className="hover:bg-gray-50 transition-colors cursor-pointer"
|
className="hover:bg-gray-50 transition-colors cursor-pointer"
|
||||||
|
|
@ -179,7 +298,10 @@ const ProfessionalStatement: React.FC = () => {
|
||||||
<tfoot className="bg-gray-50">
|
<tfoot className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={7} className="px-6 py-3 text-xs text-gray-500">
|
<td colSpan={7} className="px-6 py-3 text-xs text-gray-500">
|
||||||
Total de pagamentos: {(data.transactions || []).length}
|
<div className="flex justify-between items-center w-full">
|
||||||
|
<span>Total filtrado: {filteredTransactions.length}</span>
|
||||||
|
<span className="font-bold text-gray-900">Soma Agrupada: {formatCurrency(filteredTotalSum)}</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue