1561 lines
52 KiB
TypeScript
1561 lines
52 KiB
TypeScript
'use client';
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
Drawer,
|
|
DrawerClose,
|
|
DrawerContent,
|
|
DrawerDescription,
|
|
DrawerHeader,
|
|
DrawerTitle,
|
|
DrawerTrigger,
|
|
} from '@/components/ui/drawer';
|
|
import { Input } from '@/components/ui/input';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import {
|
|
DataGridPremium,
|
|
GridColDef,
|
|
GridToolbar,
|
|
} from '@mui/x-data-grid-premium';
|
|
import { LicenseInfo } from '@mui/x-license-pro';
|
|
import {
|
|
ArrowDown,
|
|
ArrowUp,
|
|
ArrowUpDown,
|
|
Download,
|
|
Maximize2,
|
|
Minimize2,
|
|
Search,
|
|
X,
|
|
} from 'lucide-react';
|
|
import * as React from 'react';
|
|
import * as XLSX from 'xlsx';
|
|
|
|
// Garantir que a licença seja aplicada no componente atualização de licença
|
|
if (typeof window !== 'undefined') {
|
|
try {
|
|
const PERPETUAL_LICENSE_KEY =
|
|
'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y';
|
|
LicenseInfo.setLicenseKey(PERPETUAL_LICENSE_KEY);
|
|
console.log('✅ Licença MUI X aplicada no componente Analítico');
|
|
} catch (error) {
|
|
console.warn('⚠️ Erro ao aplicar licença no componente:', error);
|
|
}
|
|
}
|
|
|
|
interface AnaliticoItem {
|
|
codigo_grupo: string;
|
|
codigo_subgrupo: string;
|
|
codigo_fornecedor: string;
|
|
nome_fornecedor: string;
|
|
id: string | number; // Pode ser string (gerado pela API) ou number (legado)
|
|
codfilial: string;
|
|
recnum: number;
|
|
data_competencia: string;
|
|
data_vencimento: string;
|
|
data_pagamento: string;
|
|
data_caixa: string;
|
|
codigo_conta: string;
|
|
conta: string;
|
|
codigo_centrocusto: string;
|
|
centro_custo?: string;
|
|
valor: number;
|
|
historico: string;
|
|
historico2: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
// Campos adicionais do Oracle
|
|
entidade?: string;
|
|
tipo_parceiro?: string;
|
|
valor_previsto?: number;
|
|
valor_confirmado?: number;
|
|
valor_pago?: number;
|
|
numero_lancamento?: number;
|
|
ano_mes_comp?: string;
|
|
codgrupo?: string;
|
|
// Novos campos
|
|
data_lancamento?: string;
|
|
data_compensacao?: string;
|
|
data_pagto?: string;
|
|
}
|
|
|
|
interface AnaliticoProps {
|
|
filtros: {
|
|
dataInicio: string;
|
|
dataFim: string;
|
|
centroCusto?: string;
|
|
codigoGrupo?: string;
|
|
codigoSubgrupo?: string;
|
|
codigoConta?: string;
|
|
linhaSelecionada?: string;
|
|
excluirCentroCusto?: string;
|
|
excluirCodigoConta?: string;
|
|
codigosCentrosCustoSelecionados?: string;
|
|
codigosContasSelecionadas?: string;
|
|
};
|
|
}
|
|
|
|
// Componente de filtro customizado estilo Excel
|
|
interface ExcelFilterProps {
|
|
column: GridColDef;
|
|
data: any[];
|
|
filteredData: any[]; // Dados filtrados para mostrar apenas valores disponíveis
|
|
onFilterChange: (field: string, values: string[]) => void;
|
|
onSortChange: (field: string, direction: 'asc' | 'desc' | null) => void;
|
|
currentFilter?: string[];
|
|
currentSort?: 'asc' | 'desc' | null;
|
|
}
|
|
|
|
const ExcelFilter: React.FC<ExcelFilterProps> = ({
|
|
column,
|
|
data,
|
|
filteredData,
|
|
onFilterChange,
|
|
onSortChange,
|
|
currentFilter = [],
|
|
currentSort = null,
|
|
}) => {
|
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
const [searchTerm, setSearchTerm] = React.useState('');
|
|
const [selectedValues, setSelectedValues] =
|
|
React.useState<string[]>(currentFilter);
|
|
const [selectAll, setSelectAll] = React.useState(false);
|
|
|
|
// Sincronizar selectedValues com currentFilter quando ele mudar
|
|
React.useEffect(() => {
|
|
setSelectedValues(currentFilter);
|
|
}, [currentFilter]);
|
|
|
|
// Obter valores únicos da coluna baseado nos dados filtrados
|
|
const uniqueValues = React.useMemo(() => {
|
|
const values = filteredData
|
|
.map((row) => {
|
|
const value = row[column.field];
|
|
if (value === null || value === undefined) return '';
|
|
return String(value);
|
|
})
|
|
.filter(
|
|
(value, index, self) => self.indexOf(value) === index && value !== ''
|
|
)
|
|
.sort();
|
|
|
|
return values;
|
|
}, [filteredData, column.field]);
|
|
|
|
// Filtrar valores baseado na busca
|
|
const filteredValues = React.useMemo(() => {
|
|
if (!searchTerm) return uniqueValues;
|
|
return uniqueValues.filter((value) =>
|
|
value.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
}, [uniqueValues, searchTerm]);
|
|
|
|
const handleSelectAll = (checked: boolean) => {
|
|
if (checked) {
|
|
setSelectedValues(filteredValues);
|
|
setSelectAll(true);
|
|
} else {
|
|
setSelectedValues([]);
|
|
setSelectAll(false);
|
|
}
|
|
};
|
|
|
|
const handleValueToggle = (value: string, checked: boolean) => {
|
|
let newValues: string[];
|
|
if (checked) {
|
|
newValues = [...selectedValues, value];
|
|
} else {
|
|
newValues = selectedValues.filter((v) => v !== value);
|
|
}
|
|
setSelectedValues(newValues);
|
|
setSelectAll(
|
|
newValues.length === filteredValues.length && filteredValues.length > 0
|
|
);
|
|
};
|
|
|
|
const handleApply = () => {
|
|
onFilterChange(column.field, selectedValues);
|
|
setIsOpen(false);
|
|
};
|
|
|
|
const handleClear = () => {
|
|
setSelectedValues([]);
|
|
setSelectAll(false);
|
|
onFilterChange(column.field, []);
|
|
setIsOpen(false);
|
|
};
|
|
|
|
const handleSort = (direction: 'asc' | 'desc') => {
|
|
onSortChange(column.field, direction);
|
|
setIsOpen(false);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0 hover:bg-gray-200 rounded-sm"
|
|
onClick={() => setIsOpen(true)}
|
|
>
|
|
<ArrowUpDown className="h-3 w-3 text-gray-600" />
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-sm font-medium">
|
|
Filtrar por "{column.headerName}"
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{/* Opções de ordenação */}
|
|
<div className="space-y-2">
|
|
<div className="text-xs font-medium text-gray-600">Ordenar</div>
|
|
<div className="flex space-x-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 text-xs"
|
|
onClick={() => handleSort('asc')}
|
|
>
|
|
<ArrowUp className="h-3 w-3 mr-1" />A a Z
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 text-xs"
|
|
onClick={() => handleSort('desc')}
|
|
>
|
|
<ArrowDown className="h-3 w-3 mr-1" />Z a A
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t pt-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 text-xs text-red-600 hover:text-red-700"
|
|
onClick={handleClear}
|
|
>
|
|
<X className="h-3 w-3 mr-1" />
|
|
Limpar Filtro
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Barra de pesquisa */}
|
|
<div className="relative">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-400" />
|
|
<Input
|
|
placeholder="Pesquisar"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-8 h-8 text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* Lista de valores com checkboxes */}
|
|
<div className="max-h-60 overflow-y-auto border rounded-md">
|
|
<div className="p-2">
|
|
<div className="flex items-center space-x-2 py-1">
|
|
<Checkbox
|
|
id="select-all"
|
|
checked={selectAll}
|
|
onCheckedChange={handleSelectAll}
|
|
/>
|
|
<label htmlFor="select-all" className="text-sm font-medium">
|
|
(Selecionar Tudo)
|
|
</label>
|
|
</div>
|
|
|
|
{filteredValues.map((value) => (
|
|
<div key={value} className="flex items-center space-x-2 py-1">
|
|
<Checkbox
|
|
id={`value-${value}`}
|
|
checked={selectedValues.includes(value)}
|
|
onCheckedChange={(checked: boolean) =>
|
|
handleValueToggle(value, checked)
|
|
}
|
|
/>
|
|
<label htmlFor={`value-${value}`} className="text-sm">
|
|
{value}
|
|
</label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Botões de ação */}
|
|
<DialogFooter className="flex space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setIsOpen(false)}
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
<Button size="sm" onClick={handleApply}>
|
|
OK
|
|
</Button>
|
|
</DialogFooter>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default function AnaliticoComponent({ filtros }: AnaliticoProps) {
|
|
const [data, setData] = React.useState<AnaliticoItem[]>([]);
|
|
const [loading, setLoading] = React.useState(false);
|
|
const [globalFilter, setGlobalFilter] = React.useState('');
|
|
const [open, setOpen] = React.useState(false);
|
|
const [drawerOpen, setDrawerOpen] = React.useState(false);
|
|
const [columnFilters, setColumnFilters] = React.useState<
|
|
Record<string, string[]>
|
|
>({});
|
|
const [columnSorts, setColumnSorts] = React.useState<
|
|
Record<string, 'asc' | 'desc' | null>
|
|
>({});
|
|
const [conditions, setConditions] = React.useState([
|
|
{ column: '', operator: 'contains', value: '' },
|
|
]);
|
|
|
|
// Estado para armazenar filtros externos (vindos do teste.tsx)
|
|
const [filtrosExternos, setFiltrosExternos] = React.useState(filtros);
|
|
|
|
// Funções para gerenciar filtros customizados
|
|
const handleColumnFilterChange = React.useCallback(
|
|
(field: string, values: string[]) => {
|
|
setColumnFilters((prev) => ({
|
|
...prev,
|
|
[field]: values,
|
|
}));
|
|
},
|
|
[]
|
|
);
|
|
|
|
const handleColumnSortChange = React.useCallback(
|
|
(field: string, direction: 'asc' | 'desc' | null) => {
|
|
setColumnSorts((prev) => ({
|
|
...prev,
|
|
[field]: direction,
|
|
}));
|
|
},
|
|
[]
|
|
);
|
|
|
|
// Função para contar filtros aplicados (apenas filtros internos do modal customizado)
|
|
const getFilterCount = React.useCallback(() => {
|
|
let count = 0;
|
|
|
|
// Contar filtros de coluna (filtros do modal customizado)
|
|
count += Object.keys(columnFilters).length;
|
|
|
|
// Contar filtro global (se aplicável)
|
|
if (globalFilter && globalFilter.trim() !== '') {
|
|
count += 1;
|
|
}
|
|
|
|
return count;
|
|
}, [columnFilters, globalFilter]);
|
|
|
|
// Função para limpar todos os filtros internos (mantém filtros externos)
|
|
const clearAllFilters = React.useCallback(() => {
|
|
setColumnFilters({});
|
|
setColumnSorts({});
|
|
setGlobalFilter('');
|
|
}, []);
|
|
|
|
// Atualizar filtros externos quando os props mudarem
|
|
React.useEffect(() => {
|
|
console.log('🔄 Analítico - useEffect dos filtros chamado');
|
|
console.log('📋 Filtros recebidos via props:', filtros);
|
|
setFiltrosExternos(filtros);
|
|
}, [filtros]);
|
|
|
|
const fetchData = React.useCallback(async () => {
|
|
console.log('🔄 Analítico - fetchData chamado');
|
|
console.log('📋 Filtros externos recebidos:', filtrosExternos);
|
|
|
|
if (!filtrosExternos.dataInicio || !filtrosExternos.dataFim) {
|
|
console.log('⚠️ Sem dataInicio ou dataFim, limpando dados');
|
|
setData([]);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const params = new URLSearchParams();
|
|
|
|
if (filtrosExternos.dataInicio) {
|
|
params.append('dataInicio', filtrosExternos.dataInicio);
|
|
}
|
|
if (filtrosExternos.dataFim) {
|
|
params.append('dataFim', filtrosExternos.dataFim);
|
|
}
|
|
if (filtrosExternos.centroCusto) {
|
|
params.append('centroCusto', filtrosExternos.centroCusto);
|
|
console.log(
|
|
'🏢 Analítico - Adicionando parâmetro centroCusto:',
|
|
filtrosExternos.centroCusto
|
|
);
|
|
} else {
|
|
console.log(
|
|
'⚠️ Analítico - centroCusto não está presente nos filtros externos'
|
|
);
|
|
}
|
|
if (filtrosExternos.codigoGrupo) {
|
|
params.append('codigoGrupo', filtrosExternos.codigoGrupo);
|
|
}
|
|
if (filtrosExternos.codigoSubgrupo) {
|
|
params.append('codigoSubgrupo', filtrosExternos.codigoSubgrupo);
|
|
}
|
|
if (filtrosExternos.codigoConta) {
|
|
params.append('codigoConta', filtrosExternos.codigoConta);
|
|
}
|
|
if (filtrosExternos.excluirCentroCusto) {
|
|
params.append('excluirCentroCusto', filtrosExternos.excluirCentroCusto);
|
|
}
|
|
if (filtrosExternos.excluirCodigoConta) {
|
|
params.append('excluirCodigoConta', filtrosExternos.excluirCodigoConta);
|
|
}
|
|
if (filtrosExternos.codigosCentrosCustoSelecionados) {
|
|
params.append(
|
|
'codigosCentrosCustoSelecionados',
|
|
filtrosExternos.codigosCentrosCustoSelecionados
|
|
);
|
|
}
|
|
if (filtrosExternos.codigosContasSelecionadas) {
|
|
params.append(
|
|
'codigosContasSelecionadas',
|
|
filtrosExternos.codigosContasSelecionadas
|
|
);
|
|
}
|
|
|
|
const url = `/api/analitico-entidade-oracle?${params.toString()}`;
|
|
console.log('🌐 Fazendo requisição para:', url);
|
|
console.log('📋 Parâmetros enviados:', {
|
|
dataInicio: filtrosExternos.dataInicio,
|
|
dataFim: filtrosExternos.dataFim,
|
|
centroCusto: filtrosExternos.centroCusto,
|
|
codigoGrupo: filtrosExternos.codigoGrupo,
|
|
codigoSubgrupo: filtrosExternos.codigoSubgrupo,
|
|
codigoConta: filtrosExternos.codigoConta,
|
|
codigosCentrosCustoSelecionados:
|
|
filtrosExternos.codigosCentrosCustoSelecionados,
|
|
codigosContasSelecionadas: filtrosExternos.codigosContasSelecionadas,
|
|
});
|
|
|
|
const response = await fetch(url);
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
console.log('✅ Resposta da API recebida:', result.length, 'registros');
|
|
console.log('📝 Primeiros 2 registros:', result.slice(0, 2));
|
|
console.log('🔍 Verificando campos específicos:', {
|
|
data_lancamento: result[0]?.data_lancamento,
|
|
data_compensacao: result[0]?.data_compensacao,
|
|
data_vencimento: result[0]?.data_vencimento,
|
|
data_caixa: result[0]?.data_caixa,
|
|
data_pagto: result[0]?.data_pagto,
|
|
entidade: result[0]?.entidade,
|
|
tipo_parceiro: result[0]?.tipo_parceiro,
|
|
centro_custo: result[0]?.centro_custo,
|
|
valor: result[0]?.valor,
|
|
tipo_valor: typeof result[0]?.valor,
|
|
});
|
|
setData(result as AnaliticoItem[]);
|
|
} else {
|
|
console.error('❌ Erro ao buscar dados:', await response.text());
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Erro ao buscar dados:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [filtrosExternos]);
|
|
|
|
React.useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
// Filtrar dados baseado nos filtros de coluna
|
|
const filteredData = React.useMemo(() => {
|
|
if (!data || data.length === 0) return data;
|
|
|
|
return data
|
|
.filter((row) => {
|
|
return Object.entries(columnFilters).every(([field, filterValues]) => {
|
|
if (!filterValues || filterValues.length === 0) return true;
|
|
|
|
const cellValue = (row as any)[field];
|
|
const stringValue =
|
|
cellValue === null || cellValue === undefined
|
|
? ''
|
|
: String(cellValue);
|
|
|
|
return filterValues.includes(stringValue);
|
|
});
|
|
})
|
|
.map((row, index) => {
|
|
// Garantir ID único: usar o id original se existir, senão criar baseado em recnum e índice
|
|
const originalId =
|
|
row.id !== null && row.id !== undefined
|
|
? typeof row.id === 'string' && row.id !== ''
|
|
? row.id
|
|
: String(row.id)
|
|
: null;
|
|
const recnumValue =
|
|
row.recnum !== null && row.recnum !== undefined ? row.recnum : null;
|
|
const uniqueId = originalId
|
|
? `filtered-${originalId}`
|
|
: `filtered-${recnumValue !== null ? recnumValue : 'idx'}-${index}`;
|
|
|
|
return {
|
|
...row,
|
|
id: uniqueId, // Garantir ID único e estável (agora é string)
|
|
};
|
|
});
|
|
}, [data, columnFilters]);
|
|
|
|
// Função para renderizar header com filtro Excel
|
|
const renderHeaderWithFilter = React.useCallback(
|
|
(column: GridColDef) => {
|
|
return (params: any) => (
|
|
<div className="flex items-center justify-between w-full">
|
|
<span className="text-sm font-medium">{column.headerName}</span>
|
|
<div className="flex items-center">
|
|
<ExcelFilter
|
|
column={column}
|
|
data={data}
|
|
filteredData={filteredData}
|
|
onFilterChange={handleColumnFilterChange}
|
|
onSortChange={handleColumnSortChange}
|
|
currentFilter={columnFilters[column.field] || []}
|
|
currentSort={columnSorts[column.field] || null}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
[
|
|
data,
|
|
filteredData,
|
|
columnFilters,
|
|
columnSorts,
|
|
handleColumnFilterChange,
|
|
handleColumnSortChange,
|
|
]
|
|
);
|
|
|
|
// Definir colunas do DataGridPro na ordem solicitada
|
|
const columns = React.useMemo(() => {
|
|
const dateCellRenderer = (params: any) => {
|
|
if (!params.value) return '-';
|
|
try {
|
|
return new Date(params.value).toLocaleDateString('pt-BR');
|
|
} catch (error) {
|
|
return params.value;
|
|
}
|
|
};
|
|
|
|
const currencyCellRenderer = (params: any, showZero: boolean = false) => {
|
|
const value = params.value;
|
|
if (value === null || value === undefined || value === '') return '-';
|
|
if (!showZero && value === 0) return '-';
|
|
const numValue =
|
|
typeof value === 'string' ? parseFloat(value) : Number(value);
|
|
if (isNaN(numValue)) return '-';
|
|
const formatted = new Intl.NumberFormat('pt-BR', {
|
|
style: 'currency',
|
|
currency: 'BRL',
|
|
}).format(numValue);
|
|
return (
|
|
<span
|
|
className={`text-sm font-semibold ${
|
|
numValue < 0 ? 'text-red-600' : 'text-gray-900'
|
|
}`}
|
|
>
|
|
{formatted}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const baseColumns = [
|
|
{
|
|
field: 'ano_mes_comp',
|
|
headerName: 'Ano/Mês Comp',
|
|
width: 110,
|
|
sortable: true,
|
|
resizable: true,
|
|
renderCell: (params: any) => params.value || '-',
|
|
},
|
|
{
|
|
field: 'numero_lancamento',
|
|
headerName: 'NumLanc',
|
|
width: 100,
|
|
sortable: true,
|
|
resizable: true,
|
|
renderCell: (params: any) => params.value || '-',
|
|
},
|
|
{
|
|
field: 'data_lancamento',
|
|
headerName: 'Dt Lanc',
|
|
width: 95,
|
|
sortable: true,
|
|
resizable: true,
|
|
renderCell: dateCellRenderer,
|
|
},
|
|
{
|
|
field: 'data_vencimento',
|
|
headerName: 'Dt Venc',
|
|
width: 95,
|
|
sortable: true,
|
|
resizable: true,
|
|
renderCell: dateCellRenderer,
|
|
},
|
|
{
|
|
field: 'data_pagto',
|
|
headerName: 'Dt Pagto',
|
|
width: 95,
|
|
sortable: true,
|
|
resizable: true,
|
|
renderCell: dateCellRenderer,
|
|
},
|
|
{
|
|
field: 'tipo_parceiro',
|
|
headerName: 'Tipo Parc',
|
|
width: 95,
|
|
sortable: true,
|
|
resizable: true,
|
|
renderCell: (params: any) => params.value || '-',
|
|
},
|
|
{
|
|
field: 'codigo_fornecedor',
|
|
headerName: 'Cod. Fornec',
|
|
width: 100,
|
|
sortable: true,
|
|
resizable: true,
|
|
},
|
|
{
|
|
field: 'nome_fornecedor',
|
|
headerName: 'Fornecedor',
|
|
width: 200,
|
|
sortable: true,
|
|
resizable: true,
|
|
},
|
|
{
|
|
field: 'valor',
|
|
headerName: 'Vl.Realizado',
|
|
type: 'number' as const,
|
|
width: 120,
|
|
sortable: true,
|
|
resizable: true,
|
|
renderCell: (params: any) => currencyCellRenderer(params, true),
|
|
},
|
|
{
|
|
field: 'valor_pago',
|
|
headerName: 'Vl.Pago',
|
|
type: 'number' as const,
|
|
width: 100,
|
|
sortable: true,
|
|
resizable: true,
|
|
renderCell: (params: any) => currencyCellRenderer(params, false),
|
|
},
|
|
{
|
|
field: 'valor_previsto',
|
|
headerName: 'Vl.Pr',
|
|
type: 'number' as const,
|
|
width: 85,
|
|
sortable: true,
|
|
resizable: true,
|
|
renderCell: (params: any) => currencyCellRenderer(params, false),
|
|
},
|
|
{
|
|
field: 'valor_confirmado',
|
|
headerName: 'Vl.Confirmado',
|
|
type: 'number' as const,
|
|
width: 125,
|
|
sortable: true,
|
|
resizable: true,
|
|
renderCell: (params: any) => currencyCellRenderer(params, false),
|
|
},
|
|
{
|
|
field: 'entidade',
|
|
headerName: 'Entidade',
|
|
width: 90,
|
|
sortable: true,
|
|
resizable: true,
|
|
renderCell: (params: any) => params.value || '-',
|
|
},
|
|
{
|
|
field: 'codigo_conta',
|
|
headerName: 'Cod.Conta',
|
|
width: 100,
|
|
sortable: true,
|
|
resizable: true,
|
|
},
|
|
{
|
|
field: 'conta',
|
|
headerName: 'Conta',
|
|
width: 200,
|
|
sortable: true,
|
|
resizable: true,
|
|
},
|
|
{
|
|
field: 'codigo_centrocusto',
|
|
headerName: 'Cod.CC',
|
|
width: 90,
|
|
sortable: true,
|
|
resizable: true,
|
|
},
|
|
{
|
|
field: 'centro_custo',
|
|
headerName: 'Centro Custo',
|
|
width: 180,
|
|
sortable: true,
|
|
resizable: true,
|
|
renderCell: (params: any) => params.value || '-',
|
|
},
|
|
{
|
|
field: 'historico',
|
|
headerName: 'Histórico',
|
|
width: 250,
|
|
sortable: true,
|
|
resizable: true,
|
|
},
|
|
{
|
|
field: 'historico2',
|
|
headerName: 'Histórico 2',
|
|
width: 250,
|
|
sortable: true,
|
|
resizable: true,
|
|
},
|
|
];
|
|
|
|
// Adicionar renderHeader com filtro Excel para todas as colunas
|
|
return baseColumns.map((col) => ({
|
|
...col,
|
|
renderHeader: renderHeaderWithFilter(col),
|
|
}));
|
|
}, [renderHeaderWithFilter]);
|
|
|
|
// Ordenar dados baseado na ordenação de coluna
|
|
const sortedAndFilteredData = React.useMemo(() => {
|
|
if (!filteredData || filteredData.length === 0) return filteredData;
|
|
|
|
const sortField = Object.keys(columnSorts).find(
|
|
(field) => columnSorts[field] !== null
|
|
);
|
|
if (!sortField || !columnSorts[sortField]) return filteredData;
|
|
|
|
return [...filteredData].sort((a, b) => {
|
|
const aValue = (a as any)[sortField];
|
|
const bValue = (b as any)[sortField];
|
|
|
|
// Converter para string para comparação
|
|
const aString =
|
|
aValue === null || aValue === undefined ? '' : String(aValue);
|
|
const bString =
|
|
bValue === null || bValue === undefined ? '' : String(bValue);
|
|
|
|
if (columnSorts[sortField] === 'asc') {
|
|
return aString.localeCompare(bString);
|
|
} else {
|
|
return bString.localeCompare(aString);
|
|
}
|
|
});
|
|
}, [filteredData, columnSorts]);
|
|
|
|
// Calcular valor total dos dados filtrados
|
|
const valorTotal = React.useMemo(() => {
|
|
return sortedAndFilteredData.reduce(
|
|
(sum, item) => sum + (Number(item.valor) || 0),
|
|
0
|
|
);
|
|
}, [sortedAndFilteredData]);
|
|
|
|
// Limpar filtros de colunas que não têm mais valores disponíveis
|
|
React.useEffect(() => {
|
|
const updatedFilters = { ...columnFilters };
|
|
let hasChanges = false;
|
|
|
|
Object.keys(columnFilters).forEach((field) => {
|
|
const currentFilterValues = columnFilters[field] || [];
|
|
if (currentFilterValues.length === 0) return;
|
|
|
|
// Obter valores únicos disponíveis para esta coluna nos dados filtrados
|
|
const availableValues = filteredData
|
|
.map((row) => {
|
|
const value = (row as any)[field];
|
|
return value === null || value === undefined ? '' : String(value);
|
|
})
|
|
.filter(
|
|
(value, index, self) => self.indexOf(value) === index && value !== ''
|
|
);
|
|
|
|
// Filtrar apenas os valores que ainda estão disponíveis
|
|
const validFilterValues = currentFilterValues.filter((value) =>
|
|
availableValues.includes(value)
|
|
);
|
|
|
|
if (validFilterValues.length !== currentFilterValues.length) {
|
|
if (validFilterValues.length === 0) {
|
|
delete updatedFilters[field];
|
|
} else {
|
|
updatedFilters[field] = validFilterValues;
|
|
}
|
|
hasChanges = true;
|
|
}
|
|
});
|
|
|
|
if (hasChanges) {
|
|
setColumnFilters(updatedFilters);
|
|
}
|
|
}, [filteredData, columnFilters]);
|
|
|
|
// Exportação XLSX - Exporta exatamente as colunas e valores da grid
|
|
const exportToExcel = () => {
|
|
if (sortedAndFilteredData.length === 0) return;
|
|
|
|
// Funções auxiliares para formatar valores exatamente como na grid
|
|
const formatDateValue = (value: any): string => {
|
|
if (!value) return '-';
|
|
try {
|
|
return new Date(value).toLocaleDateString('pt-BR');
|
|
} catch (error) {
|
|
return value;
|
|
}
|
|
};
|
|
|
|
const formatCurrencyValue = (
|
|
value: any,
|
|
showZero: boolean = false
|
|
): string | number => {
|
|
if (value === null || value === undefined || value === '') return '-';
|
|
const numValue =
|
|
typeof value === 'string' ? parseFloat(value) : Number(value);
|
|
if (isNaN(numValue)) return '-';
|
|
if (!showZero && numValue === 0) return '-';
|
|
// Para Excel, retornar o número formatado como string (mantém o formato de moeda)
|
|
return new Intl.NumberFormat('pt-BR', {
|
|
style: 'currency',
|
|
currency: 'BRL',
|
|
}).format(numValue);
|
|
};
|
|
|
|
const formatCellValue = (column: GridColDef, item: any): any => {
|
|
const value = item[column.field];
|
|
|
|
// Se a coluna tem renderCell, aplicar a mesma lógica
|
|
if (column.renderCell) {
|
|
// Para datas
|
|
if (column.field.includes('data_')) {
|
|
return formatDateValue(value);
|
|
}
|
|
|
|
// Para valores monetários
|
|
if (column.field === 'valor') {
|
|
return formatCurrencyValue(value, true);
|
|
}
|
|
if (
|
|
column.field === 'valor_previsto' ||
|
|
column.field === 'valor_confirmado' ||
|
|
column.field === 'valor_pago'
|
|
) {
|
|
return formatCurrencyValue(value, false);
|
|
}
|
|
|
|
// Para campos que retornam "-" se vazios
|
|
if (
|
|
column.field === 'centro_custo' ||
|
|
column.field === 'numero_lancamento' ||
|
|
column.field === 'entidade' ||
|
|
column.field === 'tipo_parceiro' ||
|
|
column.field === 'ano_mes_comp'
|
|
) {
|
|
return value || '-';
|
|
}
|
|
}
|
|
|
|
// Para datas sem renderCell explícito (mas que são datas)
|
|
if (column.field.includes('data_')) {
|
|
return formatDateValue(value);
|
|
}
|
|
|
|
// Valor padrão
|
|
return value ?? '';
|
|
};
|
|
|
|
// Criar dados de exportação usando as colunas da grid na ordem exata
|
|
// e depois adicionar os campos faltantes da interface
|
|
const exportData = sortedAndFilteredData.map((item) => {
|
|
const row: Record<string, any> = {};
|
|
|
|
// Primeiro: adicionar as colunas da grid na ordem exata
|
|
columns.forEach((column) => {
|
|
const headerName = column.headerName || column.field;
|
|
row[headerName] = formatCellValue(column, item);
|
|
});
|
|
|
|
// Segundo: adicionar colunas que devem ser exportadas mas não exibidas na grid
|
|
const colunasOcultas: Array<{
|
|
field: string;
|
|
label: string;
|
|
format?: (value: any) => any;
|
|
}> = [
|
|
{
|
|
field: 'data_compensacao',
|
|
label: 'Dt Comp',
|
|
format: formatDateValue,
|
|
},
|
|
{
|
|
field: 'data_caixa',
|
|
label: 'Dt Caixa',
|
|
format: formatDateValue,
|
|
},
|
|
];
|
|
|
|
colunasOcultas.forEach(({ field, label, format }) => {
|
|
const value = (item as any)[field];
|
|
if (value !== undefined && value !== null) {
|
|
row[label] = format ? format(value) : value;
|
|
} else {
|
|
row[label] = '-';
|
|
}
|
|
});
|
|
|
|
// Terceiro: adicionar campos faltantes da interface que não estão na grid
|
|
const camposFaltantes: Array<{
|
|
field: string;
|
|
label: string;
|
|
format?: (value: any) => any;
|
|
}> = [
|
|
{ field: 'codigo_grupo', label: 'Código Grupo' },
|
|
{ field: 'codigo_subgrupo', label: 'Código Subgrupo' },
|
|
{ field: 'codfilial', label: 'Código Filial' },
|
|
{ field: 'recnum', label: 'Recnum' },
|
|
{
|
|
field: 'data_competencia',
|
|
label: 'Data Competência',
|
|
format: formatDateValue,
|
|
},
|
|
{
|
|
field: 'data_pagamento',
|
|
label: 'Data Pagamento',
|
|
format: formatDateValue,
|
|
},
|
|
{ field: 'created_at', label: 'Criado Em', format: formatDateValue },
|
|
{
|
|
field: 'updated_at',
|
|
label: 'Atualizado Em',
|
|
format: formatDateValue,
|
|
},
|
|
{ field: 'codgrupo', label: 'Cod Grupo' },
|
|
];
|
|
|
|
camposFaltantes.forEach(({ field, label, format }) => {
|
|
const value = (item as any)[field];
|
|
if (value !== undefined && value !== null) {
|
|
row[label] = format ? format(value) : value;
|
|
} else {
|
|
row[label] = '';
|
|
}
|
|
});
|
|
|
|
return row;
|
|
});
|
|
|
|
const wb = XLSX.utils.book_new();
|
|
const ws = XLSX.utils.json_to_sheet(exportData);
|
|
|
|
const resumoData = [
|
|
{ Métrica: 'Total de Registros', Valor: sortedAndFilteredData.length },
|
|
{ Métrica: 'Valor Total', Valor: valorTotal },
|
|
{
|
|
Métrica: 'Filtros Aplicados',
|
|
Valor: Object.keys(columnFilters).length > 0 ? 'Sim' : 'Não',
|
|
},
|
|
];
|
|
const wsResumo = XLSX.utils.json_to_sheet(resumoData);
|
|
|
|
XLSX.utils.book_append_sheet(wb, ws, 'Dados Analíticos');
|
|
XLSX.utils.book_append_sheet(wb, wsResumo, 'Resumo');
|
|
|
|
const now = new Date();
|
|
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-');
|
|
const fileName = `analitico_${timestamp}.xlsx`;
|
|
|
|
XLSX.writeFile(wb, fileName);
|
|
};
|
|
|
|
// Aplicar filtros avançados
|
|
const applyFilters = () => {
|
|
// Implementar lógica de filtros avançados se necessário
|
|
setOpen(false);
|
|
};
|
|
|
|
const clearFilters = () => {
|
|
setConditions([{ column: '', operator: 'contains', value: '' }]);
|
|
setGlobalFilter('');
|
|
};
|
|
|
|
// Função para renderizar o conteúdo principal do componente (reutilizável)
|
|
const renderAnaliticoContent = (isMaximized: boolean = false) => {
|
|
return (
|
|
<>
|
|
{/* Filtros Externos Ativos - Apenas quando maximizado */}
|
|
{isMaximized &&
|
|
(filtrosExternos.dataInicio ||
|
|
filtrosExternos.centroCusto ||
|
|
filtrosExternos.codigoGrupo ||
|
|
filtrosExternos.codigoSubgrupo ||
|
|
filtrosExternos.codigoConta) && (
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
|
<span className="text-sm font-medium text-blue-900">
|
|
Filtros aplicados pela tabela DRE Gerencial:
|
|
</span>
|
|
<div className="flex flex-wrap gap-2 text-xs text-blue-800">
|
|
{filtrosExternos.dataInicio && filtrosExternos.dataFim && (
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
Período: {filtrosExternos.dataInicio} a{' '}
|
|
{filtrosExternos.dataFim}
|
|
</span>
|
|
)}
|
|
{filtrosExternos.centroCusto && (
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
Centro: {filtrosExternos.centroCusto}
|
|
</span>
|
|
)}
|
|
{filtrosExternos.codigoGrupo && (
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
Grupo: {filtrosExternos.codigoGrupo}
|
|
</span>
|
|
)}
|
|
{filtrosExternos.codigoSubgrupo && (
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
Subgrupo: {filtrosExternos.codigoSubgrupo}
|
|
</span>
|
|
)}
|
|
{filtrosExternos.codigoConta && (
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
Conta: {filtrosExternos.codigoConta}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Controls - Apenas quando maximizado */}
|
|
{isMaximized && (
|
|
<div className="flex gap-2 flex-wrap mb-4">
|
|
{data.length > 0 && (
|
|
<Button
|
|
onClick={clearAllFilters}
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-red-50 hover:border-red-300 text-gray-700"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
Limpar Filtros
|
|
{getFilterCount() > 0 && (
|
|
<span className="bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center font-semibold">
|
|
{getFilterCount()}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
)}
|
|
<Button
|
|
onClick={exportToExcel}
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={sortedAndFilteredData.length === 0}
|
|
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-green-50 hover:border-green-300 text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
Exportar XLSX
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* DataGridPro */}
|
|
<Card
|
|
className={`w-full shadow-lg rounded-2xl ${
|
|
isMaximized ? 'h-[calc(96vh-280px)]' : 'h-[40vh]'
|
|
}`}
|
|
style={{ overflowAnchor: 'none' }}
|
|
>
|
|
<CardContent
|
|
className="p-4 h-full"
|
|
style={{ overflowAnchor: 'none' }}
|
|
>
|
|
<div className="flex items-center gap-6 mb-3 pb-3 border-b border-gray-200">
|
|
<div className="text-sm font-medium text-gray-700">
|
|
<span className="font-semibold">Total de Registros:</span>{' '}
|
|
<span className="text-blue-600 font-bold">
|
|
{sortedAndFilteredData.length}
|
|
</span>
|
|
</div>
|
|
<div className="text-sm font-medium text-gray-700">
|
|
<span className="font-semibold">Valor Total:</span>{' '}
|
|
<span
|
|
className={`font-bold ${
|
|
valorTotal < 0 ? 'text-red-600' : 'text-gray-900'
|
|
}`}
|
|
>
|
|
{new Intl.NumberFormat('pt-BR', {
|
|
style: 'currency',
|
|
currency: 'BRL',
|
|
}).format(valorTotal)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
height: 'calc(100% - 2rem)',
|
|
width: '100%',
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
<DataGridPremium
|
|
key={`datagrid-${sortedAndFilteredData.length}-${
|
|
Object.keys(columnFilters).length
|
|
}`}
|
|
rows={sortedAndFilteredData}
|
|
columns={columns}
|
|
loading={loading}
|
|
disableRowSelectionOnClick
|
|
density="compact"
|
|
slots={{ toolbar: GridToolbar }}
|
|
disableColumnMenu={true}
|
|
disableColumnSorting={true}
|
|
pagination={false}
|
|
disableVirtualization={false}
|
|
getRowId={(row: any) => {
|
|
// Usar verificação explícita para garantir que id seja usado mesmo se for string vazia
|
|
if (
|
|
row.id !== null &&
|
|
row.id !== undefined &&
|
|
row.id !== ''
|
|
) {
|
|
return String(row.id);
|
|
}
|
|
// Fallback: usar recnum se disponível, senão criar ID único
|
|
const recnumValue =
|
|
row.recnum !== null && row.recnum !== undefined
|
|
? row.recnum
|
|
: null;
|
|
return recnumValue !== null
|
|
? `row-${recnumValue}`
|
|
: `row-${Math.random()}-${Date.now()}`;
|
|
}}
|
|
sx={{
|
|
height: '100%',
|
|
width: '100%',
|
|
'& .MuiDataGrid-root': {
|
|
border: 'none',
|
|
},
|
|
'& .MuiDataGrid-columnHeaders': {
|
|
backgroundColor: '#f9fafb',
|
|
borderBottom: '1px solid #e5e7eb',
|
|
},
|
|
'& .MuiDataGrid-columnHeader': {
|
|
backgroundColor: '#f9fafb !important',
|
|
fontWeight: 600,
|
|
fontSize: '0.875rem',
|
|
},
|
|
'& .MuiDataGrid-cell': {
|
|
borderBottom: '1px solid #f0f0f0',
|
|
fontSize: '0.875rem',
|
|
},
|
|
'& .MuiDataGrid-virtualScroller': {
|
|
scrollbarWidth: 'thin',
|
|
'&::-webkit-scrollbar': {
|
|
width: '8px',
|
|
height: '8px',
|
|
},
|
|
'&::-webkit-scrollbar-track': {
|
|
background: '#f1f1f1',
|
|
},
|
|
'&::-webkit-scrollbar-thumb': {
|
|
background: '#888',
|
|
borderRadius: '4px',
|
|
},
|
|
'&::-webkit-scrollbar-thumb:hover': {
|
|
background: '#555',
|
|
},
|
|
},
|
|
'& .MuiDataGrid-toolbarContainer': {
|
|
backgroundColor: '#f8fafc',
|
|
borderBottom: '1px solid #e5e7eb',
|
|
padding: '8px 16px',
|
|
},
|
|
'& .MuiDataGrid-columnHeaderMenuContainer': {
|
|
display: 'none !important',
|
|
},
|
|
'& .MuiDataGrid-columnHeaderMenuButton': {
|
|
display: 'none !important',
|
|
},
|
|
'& .MuiDataGrid-columnHeaderSortIcon': {
|
|
display: 'none !important',
|
|
},
|
|
'& .MuiDataGrid-footerContainer': {
|
|
display: 'none !important',
|
|
},
|
|
'& .MuiDataGrid-columnHeaderTitleContainer': {
|
|
width: '100%',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
},
|
|
}}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="w-full max-w-none mx-auto p-2"
|
|
style={{ overflowAnchor: 'none' }}
|
|
>
|
|
{/* Header Section */}
|
|
<div className="mb-2">
|
|
<div className="flex items-center justify-between mb-1">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">
|
|
Análise Analítica
|
|
{filtros.linhaSelecionada ? ` - ${filtros.linhaSelecionada}` : ''}
|
|
</h1>
|
|
<p className="text-sm text-gray-500">
|
|
Relatório detalhado de transações
|
|
</p>
|
|
</div>
|
|
|
|
{/* Filtros Externos Ativos - Centralizado */}
|
|
{(filtrosExternos.dataInicio ||
|
|
filtrosExternos.centroCusto ||
|
|
filtrosExternos.codigoGrupo ||
|
|
filtrosExternos.codigoSubgrupo ||
|
|
filtrosExternos.codigoConta) && (
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
|
<span className="text-sm font-medium text-blue-900">
|
|
Filtros aplicados pela tabela DRE Gerencial:
|
|
</span>
|
|
<div className="flex flex-wrap gap-2 text-xs text-blue-800">
|
|
{filtrosExternos.dataInicio && filtrosExternos.dataFim && (
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
Período: {filtrosExternos.dataInicio} a{' '}
|
|
{filtrosExternos.dataFim}
|
|
</span>
|
|
)}
|
|
{filtrosExternos.centroCusto && (
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
Centro: {filtrosExternos.centroCusto}
|
|
</span>
|
|
)}
|
|
{filtrosExternos.codigoGrupo && (
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
Grupo: {filtrosExternos.codigoGrupo}
|
|
</span>
|
|
)}
|
|
{filtrosExternos.codigoSubgrupo && (
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
Subgrupo: {filtrosExternos.codigoSubgrupo}
|
|
</span>
|
|
)}
|
|
{filtrosExternos.codigoConta && (
|
|
<span className="px-2 py-1 bg-blue-100 rounded">
|
|
Conta: {filtrosExternos.codigoConta}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Controls */}
|
|
<div className="flex gap-2 flex-wrap">
|
|
{/* <Input
|
|
placeholder="Filtrar tudo..."
|
|
value={globalFilter ?? ""}
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
setGlobalFilter(e.target.value)
|
|
}
|
|
className="w-64 bg-white border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
|
/>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setOpen(true)}
|
|
className="bg-white border-gray-300 hover:bg-blue-50 hover:border-blue-300 text-gray-700"
|
|
>
|
|
<Filter className="w-4 h-4 mr-2" />
|
|
Filtros Avançados
|
|
</Button> */}
|
|
{globalFilter && (
|
|
<Button
|
|
variant="outline"
|
|
onClick={clearFilters}
|
|
className="bg-white border-gray-300 hover:bg-red-50 hover:border-red-300 text-gray-700 flex items-center gap-2"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
Limpar Filtros
|
|
{getFilterCount() > 0 && (
|
|
<span className="bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-semibold">
|
|
{getFilterCount()}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
)}
|
|
<div className="flex items-center gap-2">
|
|
{data.length > 0 && (
|
|
<Button
|
|
onClick={clearAllFilters}
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-red-50 hover:border-red-300 text-gray-700"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
Limpar Filtros
|
|
{getFilterCount() > 0 && (
|
|
<span className="bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center font-semibold">
|
|
{getFilterCount()}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
)}
|
|
<Button
|
|
onClick={exportToExcel}
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={sortedAndFilteredData.length === 0}
|
|
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-green-50 hover:border-green-300 text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
Exportar XLSX
|
|
</Button>
|
|
<Drawer open={drawerOpen} onOpenChange={setDrawerOpen}>
|
|
<DrawerTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-blue-50 hover:border-blue-300 text-gray-700"
|
|
>
|
|
<Maximize2 className="h-4 w-4" />
|
|
Maximizar
|
|
</Button>
|
|
</DrawerTrigger>
|
|
<DrawerContent className="max-h-[96vh] h-[96vh]">
|
|
<DrawerHeader className="flex-shrink-0">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex-1">
|
|
<DrawerTitle className="text-2xl font-bold text-gray-900">
|
|
Análise Analítica
|
|
{filtros.linhaSelecionada
|
|
? ` - ${filtros.linhaSelecionada}`
|
|
: ''}
|
|
</DrawerTitle>
|
|
<DrawerDescription>
|
|
Relatório detalhado de transações - Versão Maximizada
|
|
</DrawerDescription>
|
|
</div>
|
|
<DrawerClose asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex items-center gap-2 bg-white border-gray-300 hover:bg-red-50 hover:border-red-300 text-gray-700"
|
|
>
|
|
<Minimize2 className="h-4 w-4" />
|
|
Minimizar
|
|
</Button>
|
|
</DrawerClose>
|
|
</div>
|
|
</DrawerHeader>
|
|
<div className="flex-1 overflow-y-auto px-4 pb-4">
|
|
{renderAnaliticoContent(true)}
|
|
</div>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Conteúdo Principal - Versão Normal */}
|
|
{renderAnaliticoContent(false)}
|
|
|
|
{/* Advanced Filters Dialog */}
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogContent className="max-w-2xl w-full mx-4 bg-white">
|
|
<DialogHeader className="pb-4">
|
|
<DialogTitle className="text-xl font-semibold text-gray-900">
|
|
Filtros Avançados
|
|
</DialogTitle>
|
|
<p className="text-sm text-gray-600">
|
|
Estes filtros são aplicados sobre os dados já filtrados pela
|
|
tabela DRE Gerencial.
|
|
</p>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 max-h-96 overflow-y-auto bg-white">
|
|
{conditions.map((cond, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="flex gap-2 items-start p-2 bg-gray-50 rounded-lg border border-gray-200"
|
|
>
|
|
<div className="flex-1">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Coluna
|
|
</label>
|
|
<Select
|
|
value={cond.column}
|
|
onValueChange={(v: string) => {
|
|
const next = [...conditions];
|
|
next[idx].column = v;
|
|
setConditions(next);
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-full bg-white border-gray-300">
|
|
<SelectValue placeholder="Selecione a coluna" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columns.map((col: any) => (
|
|
<SelectItem key={col.field} value={col.field}>
|
|
{col.headerName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Operador
|
|
</label>
|
|
<Select
|
|
value={cond.operator}
|
|
onValueChange={(v: string) => {
|
|
const next = [...conditions];
|
|
next[idx].operator = v;
|
|
if (v === 'empty' || v === 'notEmpty')
|
|
next[idx].value = '';
|
|
setConditions(next);
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-full bg-white border-gray-300">
|
|
<SelectValue placeholder="Selecione o operador" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="contains">contém</SelectItem>
|
|
<SelectItem value="equals">igual a</SelectItem>
|
|
<SelectItem value="startsWith">começa com</SelectItem>
|
|
<SelectItem value="endsWith">termina com</SelectItem>
|
|
<SelectItem value="empty">está vazio</SelectItem>
|
|
<SelectItem value="notEmpty">não está vazio</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{!(
|
|
cond.operator === 'empty' || cond.operator === 'notEmpty'
|
|
) && (
|
|
<div className="flex-1">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Valor
|
|
</label>
|
|
<Input
|
|
value={cond.value}
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const next = [...conditions];
|
|
next[idx].value = e.target.value;
|
|
setConditions(next);
|
|
}}
|
|
placeholder="Digite o valor"
|
|
className="w-full bg-white border-gray-300"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{conditions.length > 1 && (
|
|
<div className="flex items-end">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
const next = conditions.filter((_, i) => i !== idx);
|
|
setConditions(next);
|
|
}}
|
|
className="mt-6 text-red-600 hover:text-red-700 hover:bg-red-50 border-red-200"
|
|
>
|
|
✕
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
<div className="flex justify-center pt-2">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() =>
|
|
setConditions((prev) => [
|
|
...prev,
|
|
{ column: '', operator: 'contains', value: '' },
|
|
])
|
|
}
|
|
className="flex items-center gap-2 text-blue-600 hover:text-blue-700 hover:bg-blue-50 border-blue-200"
|
|
>
|
|
<span className="text-lg">+</span>
|
|
Adicionar condição
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="flex gap-2 pt-3 border-t border-gray-200">
|
|
<Button
|
|
variant="outline"
|
|
onClick={clearFilters}
|
|
className="flex-1 border-gray-300 text-gray-700 hover:bg-gray-50 flex items-center justify-center gap-2"
|
|
>
|
|
Limpar filtros avançados
|
|
{getFilterCount() > 0 && (
|
|
<span className="bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center font-semibold">
|
|
{getFilterCount()}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
<Button
|
|
onClick={applyFilters}
|
|
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
|
|
>
|
|
Aplicar filtros
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|