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:
gpt-engineer-app[bot] 2025-05-17 23:25:52 +00:00
parent d755d23300
commit c59ba67daa
14 changed files with 870 additions and 52 deletions

View File

@ -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" />

View File

@ -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>

View 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;

View 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;

View 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;

View 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;

View 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;

View 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
View 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
};

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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>
);

View 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;
}

View File

@ -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'
}
}
},