iniciando projeto
This commit is contained in:
parent
fb06c48cdf
commit
3015974658
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import 'dotenv/config';
|
||||||
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
out: './drizzle',
|
||||||
|
schema: './src/db/schema.ts',
|
||||||
|
dialect: 'postgresql',
|
||||||
|
dbCredentials: {
|
||||||
|
database: process.env.POSTGRES_DB || 'dre_gerencial',
|
||||||
|
host: process.env.POSTGRES_HOST || 'localhost',
|
||||||
|
port: Number(process.env.POSTGRES_PORT) || 5432,
|
||||||
|
user: process.env.POSTGRES_USER || 'postgres',
|
||||||
|
password: process.env.POSTGRES_PASSWORD || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
|
|
@ -9,19 +9,31 @@
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"drizzle-orm": "^0.44.6",
|
||||||
|
"lucide-react": "^0.545.0",
|
||||||
|
"next": "15.5.4",
|
||||||
|
"pg": "^8.16.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"next": "15.5.4"
|
"tailwind-merge": "^3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/pg": "^8.15.5",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@tailwindcss/postcss": "^4",
|
"drizzle-kit": "^0.31.5",
|
||||||
"tailwindcss": "^4",
|
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.5.4",
|
"eslint-config-next": "15.5.4",
|
||||||
"@eslint/eslintrc": "^3"
|
"tailwindcss": "^4",
|
||||||
|
"tsx": "^4.20.6",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import Teste from './teste';
|
||||||
|
|
||||||
|
export default function DrePage() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-2 max-w-10/12 mx-auto">
|
||||||
|
<h1>DRE-GERENCIAL</h1>
|
||||||
|
|
||||||
|
<Teste />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,550 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import { ArrowDown, ArrowUp, ArrowUpDown, BarChart3 } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface DREItem {
|
||||||
|
codfilial: string;
|
||||||
|
data_competencia: string;
|
||||||
|
data_cai: string;
|
||||||
|
grupo: string;
|
||||||
|
subgrupo: string;
|
||||||
|
centro_custo: string;
|
||||||
|
codigo_conta: number;
|
||||||
|
conta: string;
|
||||||
|
valor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HierarchicalRow {
|
||||||
|
type: 'grupo' | 'subgrupo' | 'centro_custo' | 'conta';
|
||||||
|
level: number;
|
||||||
|
grupo?: string;
|
||||||
|
subgrupo?: string;
|
||||||
|
centro_custo?: string;
|
||||||
|
conta?: string;
|
||||||
|
codigo_conta?: number;
|
||||||
|
total?: number;
|
||||||
|
isExpanded?: boolean;
|
||||||
|
valoresPorMes?: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SortField = 'descricao' | 'valor';
|
||||||
|
type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
|
interface SortConfig {
|
||||||
|
field: SortField;
|
||||||
|
direction: SortDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Teste() {
|
||||||
|
const [data, setData] = useState<DREItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||||
|
const [expandedSubgrupos, setExpandedSubgrupos] = useState<Set<string>>(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
const [expandedCentros, setExpandedCentros] = useState<Set<string>>(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||||
|
field: 'descricao',
|
||||||
|
direction: 'asc',
|
||||||
|
});
|
||||||
|
const [mesesDisponiveis, setMesesDisponiveis] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/dre');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Erro ao carregar dados');
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
setData(result);
|
||||||
|
|
||||||
|
// Extrair meses únicos dos dados
|
||||||
|
const meses = [
|
||||||
|
...new Set(
|
||||||
|
result.map((item: DREItem) => {
|
||||||
|
const dataCompetencia = new Date(item.data_competencia);
|
||||||
|
return `${dataCompetencia.getFullYear()}-${String(
|
||||||
|
dataCompetencia.getMonth() + 1
|
||||||
|
).padStart(2, '0')}`;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
].sort() as string[];
|
||||||
|
|
||||||
|
setMesesDisponiveis(meses);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Erro desconhecido');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (value: string | number) => {
|
||||||
|
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
||||||
|
return numValue.toLocaleString('pt-BR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'BRL',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleGroup = (grupo: string) => {
|
||||||
|
const newExpanded = new Set(expandedGroups);
|
||||||
|
if (newExpanded.has(grupo)) {
|
||||||
|
newExpanded.delete(grupo);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(grupo);
|
||||||
|
}
|
||||||
|
setExpandedGroups(newExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSubgrupo = (subgrupo: string) => {
|
||||||
|
const newExpanded = new Set(expandedSubgrupos);
|
||||||
|
if (newExpanded.has(subgrupo)) {
|
||||||
|
newExpanded.delete(subgrupo);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(subgrupo);
|
||||||
|
}
|
||||||
|
setExpandedSubgrupos(newExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCentro = (centro: string) => {
|
||||||
|
const newExpanded = new Set(expandedCentros);
|
||||||
|
if (newExpanded.has(centro)) {
|
||||||
|
newExpanded.delete(centro);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(centro);
|
||||||
|
}
|
||||||
|
setExpandedCentros(newExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSort = (field: SortField) => {
|
||||||
|
setSortConfig((prev) => ({
|
||||||
|
field,
|
||||||
|
direction:
|
||||||
|
prev.field === field && prev.direction === 'asc' ? 'desc' : 'asc',
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSortIcon = (field: SortField) => {
|
||||||
|
if (sortConfig.field !== field) {
|
||||||
|
return <ArrowUpDown className="h-4 w-4" />;
|
||||||
|
}
|
||||||
|
return sortConfig.direction === 'asc' ? (
|
||||||
|
<ArrowUp className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ArrowDown className="h-4 w-4" />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcularValoresPorMes = (items: DREItem[]): Record<string, number> => {
|
||||||
|
const valoresPorMes: Record<string, number> = {};
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const dataCompetencia = new Date(item.data_competencia);
|
||||||
|
const anoMes = `${dataCompetencia.getFullYear()}-${String(
|
||||||
|
dataCompetencia.getMonth() + 1
|
||||||
|
).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
if (!valoresPorMes[anoMes]) {
|
||||||
|
valoresPorMes[anoMes] = 0;
|
||||||
|
}
|
||||||
|
valoresPorMes[anoMes] += parseFloat(item.valor);
|
||||||
|
});
|
||||||
|
|
||||||
|
return valoresPorMes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildHierarchicalData = (): HierarchicalRow[] => {
|
||||||
|
const rows: HierarchicalRow[] = [];
|
||||||
|
|
||||||
|
// Agrupar por grupo
|
||||||
|
const grupos = data.reduce((acc, item) => {
|
||||||
|
if (!acc[item.grupo]) {
|
||||||
|
acc[item.grupo] = [];
|
||||||
|
}
|
||||||
|
acc[item.grupo].push(item);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, DREItem[]>);
|
||||||
|
|
||||||
|
// Ordenar grupos
|
||||||
|
const sortedGrupos = Object.entries(grupos).sort(([a], [b]) => {
|
||||||
|
if (sortConfig.field === 'descricao') {
|
||||||
|
return sortConfig.direction === 'asc'
|
||||||
|
? a.localeCompare(b)
|
||||||
|
: b.localeCompare(a);
|
||||||
|
} else {
|
||||||
|
const totalA = grupos[a].reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalB = grupos[b].reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return sortConfig.direction === 'asc'
|
||||||
|
? totalA - totalB
|
||||||
|
: totalB - totalA;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedGrupos.forEach(([grupo, items]) => {
|
||||||
|
const totalGrupo = items.reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Linha do grupo
|
||||||
|
rows.push({
|
||||||
|
type: 'grupo',
|
||||||
|
level: 0,
|
||||||
|
grupo,
|
||||||
|
total: totalGrupo,
|
||||||
|
isExpanded: expandedGroups.has(grupo),
|
||||||
|
valoresPorMes: calcularValoresPorMes(items),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (expandedGroups.has(grupo)) {
|
||||||
|
// Agrupar por subgrupo dentro do grupo
|
||||||
|
const subgrupos = items.reduce((acc, item) => {
|
||||||
|
if (!acc[item.subgrupo]) {
|
||||||
|
acc[item.subgrupo] = [];
|
||||||
|
}
|
||||||
|
acc[item.subgrupo].push(item);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, DREItem[]>);
|
||||||
|
|
||||||
|
// Ordenar subgrupos
|
||||||
|
const sortedSubgrupos = Object.entries(subgrupos).sort(([a], [b]) => {
|
||||||
|
if (sortConfig.field === 'descricao') {
|
||||||
|
return sortConfig.direction === 'asc'
|
||||||
|
? a.localeCompare(b)
|
||||||
|
: b.localeCompare(a);
|
||||||
|
} else {
|
||||||
|
const totalA = subgrupos[a].reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalB = subgrupos[b].reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return sortConfig.direction === 'asc'
|
||||||
|
? totalA - totalB
|
||||||
|
: totalB - totalA;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedSubgrupos.forEach(([subgrupo, subgrupoItems]) => {
|
||||||
|
const totalSubgrupo = subgrupoItems.reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Linha do subgrupo
|
||||||
|
rows.push({
|
||||||
|
type: 'subgrupo',
|
||||||
|
level: 1,
|
||||||
|
grupo,
|
||||||
|
subgrupo,
|
||||||
|
total: totalSubgrupo,
|
||||||
|
isExpanded: expandedSubgrupos.has(`${grupo}-${subgrupo}`),
|
||||||
|
valoresPorMes: calcularValoresPorMes(subgrupoItems),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (expandedSubgrupos.has(`${grupo}-${subgrupo}`)) {
|
||||||
|
// Agrupar por centro de custo dentro do subgrupo
|
||||||
|
const centros = subgrupoItems.reduce((acc, item) => {
|
||||||
|
if (!acc[item.centro_custo]) {
|
||||||
|
acc[item.centro_custo] = [];
|
||||||
|
}
|
||||||
|
acc[item.centro_custo].push(item);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, DREItem[]>);
|
||||||
|
|
||||||
|
// Ordenar centros de custo
|
||||||
|
const sortedCentros = Object.entries(centros).sort(([a], [b]) => {
|
||||||
|
if (sortConfig.field === 'descricao') {
|
||||||
|
return sortConfig.direction === 'asc'
|
||||||
|
? a.localeCompare(b)
|
||||||
|
: b.localeCompare(a);
|
||||||
|
} else {
|
||||||
|
const totalA = centros[a].reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalB = centros[b].reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return sortConfig.direction === 'asc'
|
||||||
|
? totalA - totalB
|
||||||
|
: totalB - totalA;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedCentros.forEach(([centro, centroItems]) => {
|
||||||
|
const totalCentro = centroItems.reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Linha do centro de custo
|
||||||
|
rows.push({
|
||||||
|
type: 'centro_custo',
|
||||||
|
level: 2,
|
||||||
|
grupo,
|
||||||
|
subgrupo,
|
||||||
|
centro_custo: centro,
|
||||||
|
total: totalCentro,
|
||||||
|
isExpanded: expandedCentros.has(
|
||||||
|
`${grupo}-${subgrupo}-${centro}`
|
||||||
|
),
|
||||||
|
valoresPorMes: calcularValoresPorMes(centroItems),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (expandedCentros.has(`${grupo}-${subgrupo}-${centro}`)) {
|
||||||
|
// Agrupar por conta dentro do centro de custo
|
||||||
|
const contas = centroItems.reduce((acc, item) => {
|
||||||
|
if (!acc[item.conta]) {
|
||||||
|
acc[item.conta] = [];
|
||||||
|
}
|
||||||
|
acc[item.conta].push(item);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, DREItem[]>);
|
||||||
|
|
||||||
|
// Ordenar contas
|
||||||
|
const sortedContas = Object.entries(contas).sort(([a], [b]) => {
|
||||||
|
if (sortConfig.field === 'descricao') {
|
||||||
|
return sortConfig.direction === 'asc'
|
||||||
|
? a.localeCompare(b)
|
||||||
|
: b.localeCompare(a);
|
||||||
|
} else {
|
||||||
|
const totalA = contas[a].reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalB = contas[b].reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return sortConfig.direction === 'asc'
|
||||||
|
? totalA - totalB
|
||||||
|
: totalB - totalA;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedContas.forEach(([conta, contaItems]) => {
|
||||||
|
const totalConta = contaItems.reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Linha da conta (sem ano/mês no nome)
|
||||||
|
rows.push({
|
||||||
|
type: 'conta',
|
||||||
|
level: 3,
|
||||||
|
grupo,
|
||||||
|
subgrupo,
|
||||||
|
centro_custo: centro,
|
||||||
|
conta,
|
||||||
|
codigo_conta: contaItems[0].codigo_conta,
|
||||||
|
total: totalConta,
|
||||||
|
valoresPorMes: calcularValoresPorMes(contaItems),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRowStyle = (row: HierarchicalRow) => {
|
||||||
|
const baseStyle = 'transition-colors hover:bg-muted/50';
|
||||||
|
|
||||||
|
switch (row.type) {
|
||||||
|
case 'grupo':
|
||||||
|
return `${baseStyle} bg-primary/5 font-semibold`;
|
||||||
|
case 'subgrupo':
|
||||||
|
return `${baseStyle} bg-primary/10 font-medium`;
|
||||||
|
case 'centro_custo':
|
||||||
|
return `${baseStyle} bg-secondary/30 font-medium`;
|
||||||
|
case 'conta':
|
||||||
|
return `${baseStyle} bg-muted/20`;
|
||||||
|
default:
|
||||||
|
return baseStyle;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIndentStyle = (level: number) => {
|
||||||
|
return { paddingLeft: `${level * 20}px` };
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCellContent = (row: HierarchicalRow) => {
|
||||||
|
switch (row.type) {
|
||||||
|
case 'grupo':
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleGroup(row.grupo!)}
|
||||||
|
className="p-1 hover:bg-muted rounded"
|
||||||
|
>
|
||||||
|
{row.isExpanded ? '▼' : '▶'}
|
||||||
|
</button>
|
||||||
|
<span className="font-semibold">{row.grupo}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'subgrupo':
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSubgrupo(`${row.grupo}-${row.subgrupo}`)}
|
||||||
|
className="p-1 hover:bg-muted rounded"
|
||||||
|
>
|
||||||
|
{row.isExpanded ? '▼' : '▶'}
|
||||||
|
</button>
|
||||||
|
<span className="font-medium">{row.subgrupo}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'centro_custo':
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
toggleCentro(`${row.grupo}-${row.subgrupo}-${row.centro_custo}`)
|
||||||
|
}
|
||||||
|
className="p-1 hover:bg-muted rounded"
|
||||||
|
>
|
||||||
|
{row.isExpanded ? '▼' : '▶'}
|
||||||
|
</button>
|
||||||
|
<span className="font-medium">{row.centro_custo}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'conta':
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-muted-foreground">•</span>
|
||||||
|
<span>{row.conta}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-center">
|
||||||
|
<BarChart3 className="h-8 w-8 animate-spin mx-auto mb-2" />
|
||||||
|
<p>Carregando dados...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<h1 className="text-2xl font-bold mb-4 text-destructive">
|
||||||
|
Erro ao carregar DRE Gerencial
|
||||||
|
</h1>
|
||||||
|
<div className="bg-destructive/10 border border-destructive/20 rounded-md p-4">
|
||||||
|
<p className="text-destructive">{error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hierarchicalData = buildHierarchicalData();
|
||||||
|
const totalGeral = data.reduce(
|
||||||
|
(sum, item) => sum + parseFloat(item.valor),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-2xl font-bold mb-2">DRE Gerencial - Hierárquica</h1>
|
||||||
|
<div className="p-4 bg-muted rounded-lg">
|
||||||
|
<h3 className="text-lg font-semibold">
|
||||||
|
Total Geral: {formatCurrency(totalGeral)}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleSort('descricao')}
|
||||||
|
className="h-auto p-0 font-semibold"
|
||||||
|
>
|
||||||
|
Descrição
|
||||||
|
{getSortIcon('descricao')}
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
|
{mesesDisponiveis.map((mes) => (
|
||||||
|
<TableHead key={mes} className="text-right">
|
||||||
|
{mes}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
<TableHead className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleSort('valor')}
|
||||||
|
className="h-auto p-0 font-semibold"
|
||||||
|
>
|
||||||
|
Total
|
||||||
|
{getSortIcon('valor')}
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{hierarchicalData.map((row, index) => (
|
||||||
|
<TableRow key={index} className={getRowStyle(row)}>
|
||||||
|
<TableCell style={getIndentStyle(row.level)}>
|
||||||
|
{renderCellContent(row)}
|
||||||
|
</TableCell>
|
||||||
|
{mesesDisponiveis.map((mes) => (
|
||||||
|
<TableCell key={mes} className="text-right font-medium">
|
||||||
|
{row.valoresPorMes && row.valoresPorMes[mes]
|
||||||
|
? formatCurrency(row.valoresPorMes[mes])
|
||||||
|
: '-'}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
<TableCell className="text-right font-medium">
|
||||||
|
{formatCurrency(row.total!)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import db from '@/db';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const data = await db.execute(sql`SELECT * FROM view_dre_gerencial`);
|
||||||
|
|
||||||
|
return NextResponse.json(data.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao buscar dados da view:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erro ao carregar dados' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,71 @@
|
||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--radius: 0.65rem;
|
||||||
--foreground: #171717;
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
.dark {
|
||||||
--color-background: var(--background);
|
--background: oklch(0.145 0 0);
|
||||||
--color-foreground: var(--foreground);
|
--foreground: oklch(0.985 0 0);
|
||||||
--font-sans: var(--font-geist-sans);
|
--card: oklch(0.205 0 0);
|
||||||
--font-mono: var(--font-geist-mono);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
}
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
@media (prefers-color-scheme: dark) {
|
--primary: oklch(0.922 0 0);
|
||||||
:root {
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
--background: #0a0a0a;
|
--secondary: oklch(0.269 0 0);
|
||||||
--foreground: #ededed;
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
}
|
--muted: oklch(0.269 0 0);
|
||||||
}
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
body {
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
background: var(--background);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
color: var(--foreground);
|
--border: oklch(1 0 0 / 10%);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from 'next';
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from 'next/font/google';
|
||||||
import "./globals.css";
|
import './globals.css';
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: '--font-geist-sans',
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const geistMono = Geist_Mono({
|
||||||
variable: "--font-geist-mono",
|
variable: '--font-geist-mono',
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: 'DRE- Gerencial',
|
||||||
description: "Generated by create next app",
|
description: 'DRE- Gerencial',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|
@ -23,7 +23,7 @@ export default function RootLayout({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="pt-BR">
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
103
src/app/page.tsx
103
src/app/page.tsx
|
|
@ -1,103 +0,0 @@
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
|
||||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={180}
|
|
||||||
height={38}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
|
||||||
<li className="mb-2 tracking-[-.01em]">
|
|
||||||
Get started by editing{" "}
|
|
||||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
|
||||||
src/app/page.tsx
|
|
||||||
</code>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
<li className="tracking-[-.01em]">
|
|
||||||
Save and see your changes instantly.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
Deploy now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Read our docs
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/file.svg"
|
|
||||||
alt="File icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Learn
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/window.svg"
|
|
||||||
alt="Window icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Examples
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/globe.svg"
|
|
||||||
alt="Globe icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Go to nextjs.org →
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
|
destructive:
|
||||||
|
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||||
|
outline:
|
||||||
|
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||||
|
secondary:
|
||||||
|
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-10 px-4 py-2',
|
||||||
|
sm: 'h-9 rounded-md px-3',
|
||||||
|
lg: 'h-11 rounded-md px-8',
|
||||||
|
icon: 'h-10 w-10',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : 'button';
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="table-container"
|
||||||
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot="table"
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
className={cn(
|
||||||
|
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import 'dotenv/config';
|
||||||
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
|
import { Pool } from 'pg';
|
||||||
|
import * as schema from './schema';
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
database: process.env.POSTGRES_DB,
|
||||||
|
host: process.env.POSTGRES_HOST,
|
||||||
|
port: Number(process.env.POSTGRES_PORT),
|
||||||
|
user: process.env.POSTGRES_USER,
|
||||||
|
password: process.env.POSTGRES_PASSWORD,
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = drizzle({
|
||||||
|
client: pool,
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default db;
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { date, integer, numeric, pgView, text } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
|
export const view = pgView('view_dre_gerencial', {
|
||||||
|
codfilial: text(),
|
||||||
|
data_competencia: date(),
|
||||||
|
data_caixa: date(),
|
||||||
|
grupo: text(),
|
||||||
|
subgrupo: text(),
|
||||||
|
centro_custo: text(),
|
||||||
|
codigo_conta: integer(),
|
||||||
|
conta: text(),
|
||||||
|
valor: numeric(),
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue