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>
|
||||
<html lang="en">
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>budget-view-finance</title>
|
||||
<meta name="description" content="Lovable Generated Project" />
|
||||
<meta name="author" content="Lovable" />
|
||||
<title>Dashboard Financeiro</title>
|
||||
<meta name="description" content="Dashboard para gerenciamento de finanças pessoais" />
|
||||
<meta name="author" content="Finance Dashboard" />
|
||||
|
||||
<meta property="og:title" content="budget-view-finance" />
|
||||
<meta property="og:description" content="Lovable Generated Project" />
|
||||
<meta property="og:title" content="Dashboard Financeiro" />
|
||||
<meta property="og:description" content="Dashboard para gerenciamento de finanças pessoais" />
|
||||
<meta property="og:type" content="website" />
|
||||
<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 as Sonner } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
@ -6,7 +7,14 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Index from "./pages/Index";
|
||||
import NotFound from "./pages/NotFound";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const App = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@ -16,7 +24,7 @@ const App = () => (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
{/* Adicione novas rotas acima desta linha */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</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 components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--background: 210 40% 98%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
@ -13,7 +14,7 @@
|
||||
--popover: 0 0% 100%;
|
||||
--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%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
@ -30,25 +31,18 @@
|
||||
|
||||
--border: 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;
|
||||
|
||||
--sidebar-background: 0 0% 98%;
|
||||
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
|
||||
--sidebar-background: 210 40% 98%;
|
||||
--sidebar-foreground: 222.2 84% 4.9%;
|
||||
--sidebar-primary: 221.2 83% 53.3%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--sidebar-border: 220 13% 91%;
|
||||
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
--sidebar-accent: 210 40% 96.1%;
|
||||
--sidebar-accent-foreground: 222.2 47.4% 11.2%;
|
||||
--sidebar-border: 214.3 31.8% 91.4%;
|
||||
--sidebar-ring: 221.2 83% 53.3%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@ -61,7 +55,7 @@
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
@ -78,15 +72,16 @@
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
--ring: 224.3 76.3% 48%;
|
||||
|
||||
--sidebar-background: 222.2 84% 4.9%;
|
||||
--sidebar-foreground: 210 40% 98%;
|
||||
--sidebar-primary: 217.2 91.2% 59.8%;
|
||||
--sidebar-primary-foreground: 222.2 47.4% 11.2%;
|
||||
--sidebar-accent: 217.2 32.6% 17.5%;
|
||||
--sidebar-accent-foreground: 210 40% 98%;
|
||||
--sidebar-border: 217.2 32.6% 17.5%;
|
||||
--sidebar-ring: 224.3 76.3% 48%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,5 +92,25 @@
|
||||
|
||||
body {
|
||||
@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 (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1>
|
||||
<p className="text-xl text-gray-600">Start building your amazing project here!</p>
|
||||
<Layout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<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>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
export default Dashboard;
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const NotFound = () => {
|
||||
const location = useLocation();
|
||||
@ -12,13 +15,16 @@ const NotFound = () => {
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">404</h1>
|
||||
<p className="text-xl text-gray-600 mb-4">Oops! Page not found</p>
|
||||
<a href="/" className="text-blue-500 hover:text-blue-700 underline">
|
||||
Return to Home
|
||||
</a>
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-background">
|
||||
<div className="text-center max-w-md px-4">
|
||||
<h1 className="text-6xl font-extrabold text-finance-blue mb-4">404</h1>
|
||||
<p className="text-2xl font-semibold mb-6">Página não encontrada</p>
|
||||
<p className="text-muted-foreground mb-8">
|
||||
A página que você está procurando não existe ou foi movida para outro lugar.
|
||||
</p>
|
||||
<Button asChild>
|
||||
<Link to="/">Voltar ao Dashboard</Link>
|
||||
</Button>
|
||||
</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";
|
||||
|
||||
export default {
|
||||
@ -61,6 +62,16 @@ export default {
|
||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||
border: 'hsl(var(--sidebar-border))',
|
||||
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: {
|
||||
@ -84,11 +95,27 @@ export default {
|
||||
to: {
|
||||
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: {
|
||||
'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