feat: Create finance dashboard
Implement a dashboard to display financial information from a Supabase database. The data will be fetched from the 'transacoes' table, which includes columns like id, user, created_at, valor, quando, detalhes, estabelecimento, tipo, and categoria.
This commit is contained in:
parent
d755d23300
commit
c59ba67daa
13
index.html
13
index.html
@ -1,14 +1,15 @@
|
|||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="pt-BR">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>budget-view-finance</title>
|
<title>Dashboard Financeiro</title>
|
||||||
<meta name="description" content="Lovable Generated Project" />
|
<meta name="description" content="Dashboard para gerenciamento de finanças pessoais" />
|
||||||
<meta name="author" content="Lovable" />
|
<meta name="author" content="Finance Dashboard" />
|
||||||
|
|
||||||
<meta property="og:title" content="budget-view-finance" />
|
<meta property="og:title" content="Dashboard Financeiro" />
|
||||||
<meta property="og:description" content="Lovable Generated Project" />
|
<meta property="og:description" content="Dashboard para gerenciamento de finanças pessoais" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||||
|
|
||||||
|
|||||||
12
src/App.tsx
12
src/App.tsx
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { Toaster as Sonner } from "@/components/ui/sonner";
|
import { Toaster as Sonner } from "@/components/ui/sonner";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
@ -6,7 +7,14 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|||||||
import Index from "./pages/Index";
|
import Index from "./pages/Index";
|
||||||
import NotFound from "./pages/NotFound";
|
import NotFound from "./pages/NotFound";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
@ -16,7 +24,7 @@ const App = () => (
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Index />} />
|
<Route path="/" element={<Index />} />
|
||||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
{/* Adicione novas rotas acima desta linha */}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
85
src/components/dashboard/CategoryChart.tsx
Normal file
85
src/components/dashboard/CategoryChart.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
|
||||||
|
import { CategorySummary } from '@/types/financialTypes';
|
||||||
|
|
||||||
|
interface CategoryChartProps {
|
||||||
|
categories: CategorySummary[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = false }) => {
|
||||||
|
const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent }: any) => {
|
||||||
|
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
||||||
|
const x = cx + radius * Math.cos(-midAngle * Math.PI / 180);
|
||||||
|
const y = cy + radius * Math.sin(-midAngle * Math.PI / 180);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<text
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
fill="#fff"
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="central"
|
||||||
|
className="text-xs font-medium"
|
||||||
|
>
|
||||||
|
{`${(percent * 100).toFixed(0)}%`}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return new Intl.NumberFormat('pt-BR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'BRL',
|
||||||
|
}).format(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="dashboard-card h-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Gastos por Categoria</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center h-[250px]">
|
||||||
|
<div className="h-32 w-32 rounded-full border-4 border-t-primary border-opacity-20 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : categories.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center h-[250px] text-muted-foreground">
|
||||||
|
Sem dados disponíveis
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-[250px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={categories}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
labelLine={false}
|
||||||
|
label={renderCustomizedLabel}
|
||||||
|
outerRadius={80}
|
||||||
|
fill="#8884d8"
|
||||||
|
dataKey="valor"
|
||||||
|
>
|
||||||
|
{categories.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value: number) => formatCurrency(value)}
|
||||||
|
labelFormatter={(index) => categories[index].categoria}
|
||||||
|
/>
|
||||||
|
<Legend />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategoryChart;
|
||||||
84
src/components/dashboard/MonthlyChart.tsx
Normal file
84
src/components/dashboard/MonthlyChart.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
ResponsiveContainer,
|
||||||
|
} from 'recharts';
|
||||||
|
import { MonthlyData } from '@/types/financialTypes';
|
||||||
|
|
||||||
|
interface MonthlyChartProps {
|
||||||
|
data: MonthlyData[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MonthlyChart: React.FC<MonthlyChartProps> = ({ data, isLoading = false }) => {
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return new Intl.NumberFormat('pt-BR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'BRL',
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="dashboard-card h-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Receitas vs Despesas</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center h-[250px]">
|
||||||
|
<div className="h-32 w-32 rounded-full border-4 border-t-primary border-opacity-20 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : data.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center h-[250px] text-muted-foreground">
|
||||||
|
Sem dados disponíveis
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-[250px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={data}
|
||||||
|
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
||||||
|
<XAxis dataKey="month" />
|
||||||
|
<YAxis
|
||||||
|
tickFormatter={formatCurrency}
|
||||||
|
width={80}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value: number) => formatCurrency(value)}
|
||||||
|
labelFormatter={(label) => `Mês: ${label}`}
|
||||||
|
/>
|
||||||
|
<Legend />
|
||||||
|
<Bar
|
||||||
|
name="Receitas"
|
||||||
|
dataKey="receitas"
|
||||||
|
fill="#10B981"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
/>
|
||||||
|
<Bar
|
||||||
|
name="Despesas"
|
||||||
|
dataKey="despesas"
|
||||||
|
fill="#EF4444"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MonthlyChart;
|
||||||
50
src/components/dashboard/SummaryCard.tsx
Normal file
50
src/components/dashboard/SummaryCard.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface SummaryCardProps {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
trend?: number;
|
||||||
|
className?: string;
|
||||||
|
iconClass?: string;
|
||||||
|
valueClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SummaryCard: React.FC<SummaryCardProps> = ({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
icon,
|
||||||
|
trend,
|
||||||
|
className,
|
||||||
|
iconClass,
|
||||||
|
valueClass,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card className={cn("dashboard-card animate-fade-in", className)}>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className={cn("p-2 rounded-md", iconClass || "bg-primary/10")}>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
{trend !== undefined && (
|
||||||
|
<div className={cn(
|
||||||
|
"text-xs font-medium",
|
||||||
|
trend > 0 ? "text-finance-green" : trend < 0 ? "text-finance-red" : "text-muted-foreground"
|
||||||
|
)}>
|
||||||
|
{trend > 0 && '+'}{trend}%
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">{title}</p>
|
||||||
|
<p className={cn("text-2xl font-bold", valueClass)}>{value}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SummaryCard;
|
||||||
198
src/components/dashboard/TransactionsTable.tsx
Normal file
198
src/components/dashboard/TransactionsTable.tsx
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { ChevronDown, Search } from 'lucide-react';
|
||||||
|
import { Transaction } from '@/types/financialTypes';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface TransactionsTableProps {
|
||||||
|
transactions: Transaction[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransactionsTable = ({ transactions, isLoading = false }: TransactionsTableProps) => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [sortColumn, setSortColumn] = useState<string>('quando');
|
||||||
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||||
|
|
||||||
|
const handleSort = (column: string) => {
|
||||||
|
if (sortColumn === column) {
|
||||||
|
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||||
|
} else {
|
||||||
|
setSortColumn(column);
|
||||||
|
setSortDirection('asc');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredTransactions = transactions.filter((transaction) => {
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
return (
|
||||||
|
transaction.estabelecimento.toLowerCase().includes(query) ||
|
||||||
|
transaction.detalhes.toLowerCase().includes(query) ||
|
||||||
|
transaction.categoria.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedTransactions = [...filteredTransactions].sort((a, b) => {
|
||||||
|
if (sortColumn === 'valor') {
|
||||||
|
return sortDirection === 'asc' ? a.valor - b.valor : b.valor - a.valor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortColumn === 'quando') {
|
||||||
|
return sortDirection === 'asc'
|
||||||
|
? new Date(a.quando).getTime() - new Date(b.quando).getTime()
|
||||||
|
: new Date(b.quando).getTime() - new Date(a.quando).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
const aValue = a[sortColumn as keyof Transaction]?.toString().toLowerCase() || '';
|
||||||
|
const bValue = b[sortColumn as keyof Transaction]?.toString().toLowerCase() || '';
|
||||||
|
|
||||||
|
return sortDirection === 'asc'
|
||||||
|
? aValue.localeCompare(bValue)
|
||||||
|
: bValue.localeCompare(aValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return new Intl.NumberFormat('pt-BR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'BRL',
|
||||||
|
}).format(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="relative w-full max-w-sm">
|
||||||
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Buscar transações..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="ml-2">
|
||||||
|
Filtrar <ChevronDown className="ml-1 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => setSearchQuery('')}>
|
||||||
|
Todas
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setSearchQuery('entrada')}>
|
||||||
|
Receitas
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setSearchQuery('saida')}>
|
||||||
|
Despesas
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead
|
||||||
|
className="cursor-pointer w-[160px]"
|
||||||
|
onClick={() => handleSort('quando')}
|
||||||
|
>
|
||||||
|
Data {sortColumn === 'quando' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => handleSort('estabelecimento')}
|
||||||
|
>
|
||||||
|
Estabelecimento {sortColumn === 'estabelecimento' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => handleSort('detalhes')}
|
||||||
|
>
|
||||||
|
Detalhes {sortColumn === 'detalhes' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => handleSort('categoria')}
|
||||||
|
>
|
||||||
|
Categoria {sortColumn === 'categoria' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead
|
||||||
|
className="text-right cursor-pointer"
|
||||||
|
onClick={() => handleSort('valor')}
|
||||||
|
>
|
||||||
|
Valor {sortColumn === 'valor' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{isLoading ? (
|
||||||
|
Array(5).fill(0).map((_, i) => (
|
||||||
|
<TableRow key={`skeleton-${i}`}>
|
||||||
|
{Array(5).fill(0).map((_, j) => (
|
||||||
|
<TableCell key={`cell-${i}-${j}`} className="p-2">
|
||||||
|
<div className="h-4 bg-muted rounded animate-pulse-gentle" />
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : sortedTransactions.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
||||||
|
{searchQuery ? 'Nenhuma transação encontrada' : 'Não há transações disponíveis'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
sortedTransactions.slice(0, 5).map((transaction) => (
|
||||||
|
<TableRow key={transaction.id}>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{format(new Date(transaction.quando), 'dd/MM/yyyy')}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{transaction.estabelecimento}</TableCell>
|
||||||
|
<TableCell>{transaction.detalhes}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="inline-block px-2 py-1 text-xs font-medium rounded-md bg-secondary">
|
||||||
|
{transaction.categoria}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className={cn(
|
||||||
|
"text-right font-medium",
|
||||||
|
transaction.tipo === 'entrada' ? "text-finance-green" : "text-finance-red"
|
||||||
|
)}>
|
||||||
|
{transaction.tipo === 'entrada' ? '+' : '-'}{formatCurrency(Math.abs(transaction.valor))}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center pt-2">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
Ver todas as transações
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransactionsTable;
|
||||||
23
src/components/layout/Layout.tsx
Normal file
23
src/components/layout/Layout.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Sidebar from '@/components/layout/Sidebar';
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen overflow-hidden">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="flex-1 overflow-auto p-6">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
83
src/components/layout/Sidebar.tsx
Normal file
83
src/components/layout/Sidebar.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ChevronLeft, ChevronRight, Home, CalendarDays, DollarSign, PieChart } from 'lucide-react';
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{
|
||||||
|
name: 'Dashboard',
|
||||||
|
path: '/',
|
||||||
|
icon: <Home className="mr-2 h-5 w-5" />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Transações',
|
||||||
|
path: '/transacoes',
|
||||||
|
icon: <DollarSign className="mr-2 h-5 w-5" />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Categorias',
|
||||||
|
path: '/categorias',
|
||||||
|
icon: <PieChart className="mr-2 h-5 w-5" />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Calendário',
|
||||||
|
path: '/calendario',
|
||||||
|
icon: <CalendarDays className="mr-2 h-5 w-5" />
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Sidebar = ({ className }: SidebarProps) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col h-screen bg-sidebar border-r border-border transition-all duration-300 ease-in-out",
|
||||||
|
collapsed ? "w-[60px]" : "w-[250px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between p-4">
|
||||||
|
{!collapsed && (
|
||||||
|
<h1 className="font-bold text-xl text-primary">FinDash</h1>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
className="ml-auto"
|
||||||
|
>
|
||||||
|
{collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex-1 py-4 space-y-1">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
to={item.path}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors",
|
||||||
|
location.pathname === item.path
|
||||||
|
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
||||||
|
: "text-sidebar-foreground hover:bg-sidebar-accent/50",
|
||||||
|
collapsed ? "justify-center" : "justify-start"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={collapsed ? "mr-0" : "mr-2"}>{item.icon}</span>
|
||||||
|
{!collapsed && <span>{item.name}</span>}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
120
src/data/mockData.ts
Normal file
120
src/data/mockData.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
|
||||||
|
import { Transaction, CategorySummary, MonthlyData } from "@/types/financialTypes";
|
||||||
|
|
||||||
|
// Mock transaction data
|
||||||
|
export const mockTransactions: Transaction[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
user: "user123",
|
||||||
|
created_at: "2023-05-15T10:30:00Z",
|
||||||
|
valor: 1500.00,
|
||||||
|
quando: "2023-05-15",
|
||||||
|
detalhes: "Salário mensal",
|
||||||
|
estabelecimento: "Empresa XYZ",
|
||||||
|
tipo: "entrada",
|
||||||
|
categoria: "Salário"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
user: "user123",
|
||||||
|
created_at: "2023-05-16T14:20:00Z",
|
||||||
|
valor: 120.50,
|
||||||
|
quando: "2023-05-16",
|
||||||
|
detalhes: "Compras semanais",
|
||||||
|
estabelecimento: "Supermercado Bom Preço",
|
||||||
|
tipo: "saida",
|
||||||
|
categoria: "Alimentação"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
user: "user123",
|
||||||
|
created_at: "2023-05-17T09:15:00Z",
|
||||||
|
valor: 75.00,
|
||||||
|
quando: "2023-05-17",
|
||||||
|
detalhes: "Combustível",
|
||||||
|
estabelecimento: "Posto Shell",
|
||||||
|
tipo: "saida",
|
||||||
|
categoria: "Transporte"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
user: "user123",
|
||||||
|
created_at: "2023-05-18T18:45:00Z",
|
||||||
|
valor: 200.00,
|
||||||
|
quando: "2023-05-18",
|
||||||
|
detalhes: "Jantar com amigos",
|
||||||
|
estabelecimento: "Restaurante Sabor & Arte",
|
||||||
|
tipo: "saida",
|
||||||
|
categoria: "Lazer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
user: "user123",
|
||||||
|
created_at: "2023-05-20T11:30:00Z",
|
||||||
|
valor: 450.00,
|
||||||
|
quando: "2023-05-20",
|
||||||
|
detalhes: "Aluguel",
|
||||||
|
estabelecimento: "Imobiliária Central",
|
||||||
|
tipo: "saida",
|
||||||
|
categoria: "Moradia"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "6",
|
||||||
|
user: "user123",
|
||||||
|
created_at: "2023-05-22T15:10:00Z",
|
||||||
|
valor: 89.90,
|
||||||
|
quando: "2023-05-22",
|
||||||
|
detalhes: "Internet",
|
||||||
|
estabelecimento: "Telecomunicações Brasil",
|
||||||
|
tipo: "saida",
|
||||||
|
categoria: "Serviços"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
user: "user123",
|
||||||
|
created_at: "2023-05-25T08:20:00Z",
|
||||||
|
valor: 300.00,
|
||||||
|
quando: "2023-05-25",
|
||||||
|
detalhes: "Freelance design",
|
||||||
|
estabelecimento: "Cliente Particular",
|
||||||
|
tipo: "entrada",
|
||||||
|
categoria: "Freelance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "8",
|
||||||
|
user: "user123",
|
||||||
|
created_at: "2023-05-27T14:00:00Z",
|
||||||
|
valor: 65.00,
|
||||||
|
quando: "2023-05-27",
|
||||||
|
detalhes: "Farmácia",
|
||||||
|
estabelecimento: "Drogaria Saúde",
|
||||||
|
tipo: "saida",
|
||||||
|
categoria: "Saúde"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock category data
|
||||||
|
export const mockCategories: CategorySummary[] = [
|
||||||
|
{ categoria: "Alimentação", valor: 250.50, percentage: 0.25, color: "#F59E0B" },
|
||||||
|
{ categoria: "Transporte", valor: 150.00, percentage: 0.15, color: "#60A5FA" },
|
||||||
|
{ categoria: "Lazer", valor: 200.00, percentage: 0.2, color: "#8B5CF6" },
|
||||||
|
{ categoria: "Moradia", valor: 450.00, percentage: 0.45, color: "#EF4444" },
|
||||||
|
{ categoria: "Serviços", valor: 89.90, percentage: 0.09, color: "#10B981" }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock monthly data
|
||||||
|
export const mockMonthlyData: MonthlyData[] = [
|
||||||
|
{ month: "Jan", receitas: 2000, despesas: 1500 },
|
||||||
|
{ month: "Fev", receitas: 2200, despesas: 1800 },
|
||||||
|
{ month: "Mar", receitas: 1900, despesas: 1700 },
|
||||||
|
{ month: "Abr", receitas: 2400, despesas: 1600 },
|
||||||
|
{ month: "Mai", receitas: 1800, despesas: 1900 },
|
||||||
|
{ month: "Jun", receitas: 2100, despesas: 1750 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock total values
|
||||||
|
export const mockTotals = {
|
||||||
|
receitas: 1800,
|
||||||
|
despesas: 1000,
|
||||||
|
saldo: 800
|
||||||
|
};
|
||||||
@ -1,10 +1,11 @@
|
|||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 210 40% 98%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
@ -13,7 +14,7 @@
|
|||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--primary: 222.2 47.4% 11.2%;
|
--primary: 221.2 83% 53.3%;
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 210 40% 96.1%;
|
||||||
@ -30,25 +31,18 @@
|
|||||||
|
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 214.3 31.8% 91.4%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 214.3 31.8% 91.4%;
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 221.2 83% 53.3%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
--sidebar-background: 0 0% 98%;
|
--sidebar-background: 210 40% 98%;
|
||||||
|
--sidebar-foreground: 222.2 84% 4.9%;
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
--sidebar-primary: 221.2 83% 53.3%;
|
||||||
|
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
|
||||||
|
|
||||||
--sidebar-primary-foreground: 0 0% 98%;
|
--sidebar-primary-foreground: 0 0% 98%;
|
||||||
|
--sidebar-accent: 210 40% 96.1%;
|
||||||
--sidebar-accent: 240 4.8% 95.9%;
|
--sidebar-accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--sidebar-border: 214.3 31.8% 91.4%;
|
||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
--sidebar-ring: 221.2 83% 53.3%;
|
||||||
|
|
||||||
--sidebar-border: 220 13% 91%;
|
|
||||||
|
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@ -61,7 +55,7 @@
|
|||||||
--popover: 222.2 84% 4.9%;
|
--popover: 222.2 84% 4.9%;
|
||||||
--popover-foreground: 210 40% 98%;
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--primary: 210 40% 98%;
|
--primary: 217.2 91.2% 59.8%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
@ -78,15 +72,16 @@
|
|||||||
|
|
||||||
--border: 217.2 32.6% 17.5%;
|
--border: 217.2 32.6% 17.5%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--input: 217.2 32.6% 17.5%;
|
||||||
--ring: 212.7 26.8% 83.9%;
|
--ring: 224.3 76.3% 48%;
|
||||||
--sidebar-background: 240 5.9% 10%;
|
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
--sidebar-background: 222.2 84% 4.9%;
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
--sidebar-foreground: 210 40% 98%;
|
||||||
--sidebar-primary-foreground: 0 0% 100%;
|
--sidebar-primary: 217.2 91.2% 59.8%;
|
||||||
--sidebar-accent: 240 3.7% 15.9%;
|
--sidebar-primary-foreground: 222.2 47.4% 11.2%;
|
||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
--sidebar-accent: 217.2 32.6% 17.5%;
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
--sidebar-accent-foreground: 210 40% 98%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-border: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-ring: 224.3 76.3% 48%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,5 +92,25 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
|
font-feature-settings: "rlig" 1, "calt" 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card {
|
||||||
|
@apply bg-card rounded-lg border p-5 shadow-sm transition-all hover:shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-card {
|
||||||
|
@apply bg-card rounded-lg border p-4 shadow-sm transition-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,14 +1,93 @@
|
|||||||
// Update this page (the content is just a fallback if you fail to update the page)
|
|
||||||
|
|
||||||
const Index = () => {
|
import { useState, useEffect } from 'react';
|
||||||
|
import Layout from '@/components/layout/Layout';
|
||||||
|
import SummaryCard from '@/components/dashboard/SummaryCard';
|
||||||
|
import TransactionsTable from '@/components/dashboard/TransactionsTable';
|
||||||
|
import CategoryChart from '@/components/dashboard/CategoryChart';
|
||||||
|
import MonthlyChart from '@/components/dashboard/MonthlyChart';
|
||||||
|
import { DollarSign, TrendingUp, TrendingDown, PiggyBank } from 'lucide-react';
|
||||||
|
import { mockTransactions, mockCategories, mockMonthlyData, mockTotals } from '@/data/mockData';
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Simulando carregamento de dados
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
toast({
|
||||||
|
title: "Dados carregados com sucesso",
|
||||||
|
description: "Conecte ao Supabase para ver seus dados reais"
|
||||||
|
});
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [toast]);
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return new Intl.NumberFormat('pt-BR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'BRL',
|
||||||
|
}).format(value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
<Layout>
|
||||||
<div className="text-center">
|
<div className="space-y-6">
|
||||||
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1>
|
<div className="flex justify-between items-center">
|
||||||
<p className="text-xl text-gray-600">Start building your amazing project here!</p>
|
<h1 className="text-2xl font-bold tracking-tight">Dashboard Financeiro</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Última atualização: {new Date().toLocaleDateString('pt-BR')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<SummaryCard
|
||||||
|
title="Receitas"
|
||||||
|
value={formatCurrency(mockTotals.receitas)}
|
||||||
|
icon={<DollarSign className="h-4 w-4 text-finance-green" />}
|
||||||
|
trend={5}
|
||||||
|
iconClass="bg-finance-green/10"
|
||||||
|
valueClass="text-finance-green"
|
||||||
|
/>
|
||||||
|
<SummaryCard
|
||||||
|
title="Despesas"
|
||||||
|
value={formatCurrency(mockTotals.despesas)}
|
||||||
|
icon={<TrendingDown className="h-4 w-4 text-finance-red" />}
|
||||||
|
trend={-2}
|
||||||
|
iconClass="bg-finance-red/10"
|
||||||
|
valueClass="text-finance-red"
|
||||||
|
/>
|
||||||
|
<SummaryCard
|
||||||
|
title="Saldo"
|
||||||
|
value={formatCurrency(mockTotals.saldo)}
|
||||||
|
icon={<TrendingUp className="h-4 w-4 text-finance-blue" />}
|
||||||
|
iconClass="bg-finance-blue/10"
|
||||||
|
valueClass="text-finance-blue"
|
||||||
|
/>
|
||||||
|
<SummaryCard
|
||||||
|
title="Economia"
|
||||||
|
value={`${((mockTotals.saldo / mockTotals.receitas) * 100).toFixed(1)}%`}
|
||||||
|
icon={<PiggyBank className="h-4 w-4 text-finance-purple" />}
|
||||||
|
iconClass="bg-finance-purple/10"
|
||||||
|
valueClass="text-finance-purple"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<CategoryChart categories={mockCategories} isLoading={isLoading} />
|
||||||
|
<MonthlyChart data={mockMonthlyData} isLoading={isLoading} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="text-xl font-semibold">Transações Recentes</h2>
|
||||||
|
<TransactionsTable transactions={mockTransactions} isLoading={isLoading} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Index;
|
export default Dashboard;
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
const NotFound = () => {
|
const NotFound = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -12,13 +15,16 @@ const NotFound = () => {
|
|||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
<div className="min-h-screen flex flex-col items-center justify-center bg-background">
|
||||||
<div className="text-center">
|
<div className="text-center max-w-md px-4">
|
||||||
<h1 className="text-4xl font-bold mb-4">404</h1>
|
<h1 className="text-6xl font-extrabold text-finance-blue mb-4">404</h1>
|
||||||
<p className="text-xl text-gray-600 mb-4">Oops! Page not found</p>
|
<p className="text-2xl font-semibold mb-6">Página não encontrada</p>
|
||||||
<a href="/" className="text-blue-500 hover:text-blue-700 underline">
|
<p className="text-muted-foreground mb-8">
|
||||||
Return to Home
|
A página que você está procurando não existe ou foi movida para outro lugar.
|
||||||
</a>
|
</p>
|
||||||
|
<Button asChild>
|
||||||
|
<Link to="/">Voltar ao Dashboard</Link>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
39
src/types/financialTypes.ts
Normal file
39
src/types/financialTypes.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
export interface Transaction {
|
||||||
|
id: string;
|
||||||
|
user: string;
|
||||||
|
created_at: string;
|
||||||
|
valor: number;
|
||||||
|
quando: string;
|
||||||
|
detalhes: string;
|
||||||
|
estabelecimento: string;
|
||||||
|
tipo: 'entrada' | 'saida';
|
||||||
|
categoria: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionSummary {
|
||||||
|
totalReceitas: number;
|
||||||
|
totalDespesas: number;
|
||||||
|
saldo: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategorySummary {
|
||||||
|
categoria: string;
|
||||||
|
valor: number;
|
||||||
|
percentage: number;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MonthlyData {
|
||||||
|
month: string;
|
||||||
|
receitas: number;
|
||||||
|
despesas: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionFilters {
|
||||||
|
search: string;
|
||||||
|
startDate: Date | null;
|
||||||
|
endDate: Date | null;
|
||||||
|
tipo: string;
|
||||||
|
categoria: string;
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -61,6 +62,16 @@ export default {
|
|||||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||||
border: 'hsl(var(--sidebar-border))',
|
border: 'hsl(var(--sidebar-border))',
|
||||||
ring: 'hsl(var(--sidebar-ring))'
|
ring: 'hsl(var(--sidebar-ring))'
|
||||||
|
},
|
||||||
|
// Financial dashboard specific colors
|
||||||
|
finance: {
|
||||||
|
'blue': '#1D4ED8',
|
||||||
|
'light-blue': '#60A5FA',
|
||||||
|
'green': '#10B981',
|
||||||
|
'light-green': '#34D399',
|
||||||
|
'red': '#EF4444',
|
||||||
|
'yellow': '#F59E0B',
|
||||||
|
'purple': '#8B5CF6'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
@ -84,11 +95,27 @@ export default {
|
|||||||
to: {
|
to: {
|
||||||
height: '0'
|
height: '0'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'fade-in': {
|
||||||
|
'0%': {
|
||||||
|
opacity: '0',
|
||||||
|
transform: 'translateY(10px)'
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
opacity: '1',
|
||||||
|
transform: 'translateY(0)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'pulse-gentle': {
|
||||||
|
'0%, 100%': { opacity: '1' },
|
||||||
|
'50%': { opacity: '0.8' }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||||
|
'fade-in': 'fade-in 0.5s ease-out',
|
||||||
|
'pulse-gentle': 'pulse-gentle 2s infinite'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user