Refactor dashboard layout and card effects
- Reordered dashboard components. - Removed card spotlight effect from expense category card. - Applied hover effect from account alerts page to transaction and category cards. - Added 3D card effect to landing page images. - Added 3d card component.
This commit is contained in:
parent
2990ab9d30
commit
5c3280f64e
@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CardSpotlight } from '@/components/ui/card-spotlight';
|
|
||||||
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
|
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
|
||||||
import { CategorySummary } from '@/types/financialTypes';
|
import { CategorySummary } from '@/types/financialTypes';
|
||||||
|
|
||||||
@ -52,57 +51,50 @@ const CategoryChart: React.FC<CategoryChartProps> = ({ categories, isLoading = f
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardSpotlight className="dashboard-card h-full animate-fade-in bg-gradient-to-br from-orange-50 via-white to-indigo-50 dark:from-zinc-900 dark:via-zinc-800 dark:to-zinc-900">
|
<div className="h-full animate-fade-in">
|
||||||
<div className="pb-2 bg-gradient-to-r from-indigo-50/60 to-orange-50/0 dark:from-zinc-900 dark:to-zinc-900 rounded-t-2xl -m-6 mb-4 p-6">
|
{isLoading ? (
|
||||||
<div className="text-lg font-bold tracking-tight text-zinc-700 dark:text-white flex items-center relative z-20">
|
<div className="flex items-center justify-center h-[260px]">
|
||||||
<span className="mr-2">🧾</span> Gastos por Categoria
|
<div className="h-32 w-32 rounded-full border-4 border-t-primary border-opacity-20 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : validCategories.length === 0 ? (
|
||||||
<div className="relative z-20">
|
<div className="flex flex-col items-center justify-center h-[260px] text-muted-foreground">
|
||||||
{isLoading ? (
|
<span>Sem dados disponíveis</span>
|
||||||
<div className="flex items-center justify-center h-[260px]">
|
<span className="text-xs mt-2">Verifique se existem transações do tipo 'despesa' cadastradas</span>
|
||||||
<div className="h-32 w-32 rounded-full border-4 border-t-primary border-opacity-20 animate-spin" />
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : validCategories.length === 0 ? (
|
<div className="h-[300px] relative">
|
||||||
<div className="flex flex-col items-center justify-center h-[260px] text-muted-foreground">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<span>Sem dados disponíveis</span>
|
<PieChart>
|
||||||
<span className="text-xs mt-2">Verifique se existem transações do tipo 'despesa' cadastradas</span>
|
<Pie
|
||||||
</div>
|
data={validCategories}
|
||||||
) : (
|
cx="50%"
|
||||||
<div className="h-[300px] relative">
|
cy="45%"
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
outerRadius={90}
|
||||||
<PieChart>
|
innerRadius={40}
|
||||||
<Pie
|
fill="#8884d8"
|
||||||
data={validCategories}
|
dataKey="valor"
|
||||||
cx="50%"
|
nameKey="categoria"
|
||||||
cy="45%"
|
paddingAngle={2}
|
||||||
outerRadius={90}
|
strokeWidth={2}
|
||||||
innerRadius={40}
|
stroke="#fff"
|
||||||
fill="#8884d8"
|
isAnimationActive={true}
|
||||||
dataKey="valor"
|
animationDuration={1300}
|
||||||
nameKey="categoria"
|
>
|
||||||
paddingAngle={2}
|
{validCategories.map((entry, index) => (
|
||||||
strokeWidth={2}
|
<Cell
|
||||||
stroke="#fff"
|
key={`cell-${index}`}
|
||||||
isAnimationActive={true}
|
fill={entry.color}
|
||||||
animationDuration={1300}
|
style={{ filter: 'drop-shadow(0px 2px 3px rgba(0,0,0,0.08))' }}
|
||||||
>
|
/>
|
||||||
{validCategories.map((entry, index) => (
|
))}
|
||||||
<Cell
|
</Pie>
|
||||||
key={`cell-${index}`}
|
<Tooltip content={renderCustomTooltip} />
|
||||||
fill={entry.color}
|
<Legend content={renderCustomLegend} layout="horizontal" verticalAlign="bottom" />
|
||||||
style={{ filter: 'drop-shadow(0px 2px 3px rgba(0,0,0,0.08))' }}
|
</PieChart>
|
||||||
/>
|
</ResponsiveContainer>
|
||||||
))}
|
</div>
|
||||||
</Pie>
|
)}
|
||||||
<Tooltip content={renderCustomTooltip} />
|
</div>
|
||||||
<Legend content={renderCustomLegend} layout="horizontal" verticalAlign="bottom" />
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardSpotlight>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CardSpotlight } from '@/components/ui/card-spotlight';
|
|
||||||
import { ArrowUp, ArrowDown, CreditCard } from 'lucide-react';
|
import { ArrowUp, ArrowDown, CreditCard } from 'lucide-react';
|
||||||
|
import { SimpleCard } from '@/components/ui/simple-card';
|
||||||
|
|
||||||
interface TransactionSummaryCardsProps {
|
interface TransactionSummaryCardsProps {
|
||||||
totalReceitas: number;
|
totalReceitas: number;
|
||||||
@ -21,42 +21,42 @@ export const TransactionSummaryCards = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
<CardSpotlight className="border-2 border-green-200">
|
<SimpleCard className="border-green-200">
|
||||||
<div className="pb-2 bg-green-50 rounded-t-lg -m-6 mb-4 p-6">
|
<div className="pb-2 bg-green-50 rounded-t-lg -m-6 mb-4 p-6">
|
||||||
<div className="text-green-700 flex items-center font-bold relative z-20">
|
<div className="text-green-700 flex items-center font-bold">
|
||||||
<ArrowUp className="h-5 w-5 mr-2 text-green-600" />
|
<ArrowUp className="h-5 w-5 mr-2 text-green-600" />
|
||||||
Ganhos do mês
|
Ganhos do mês
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-0 relative z-20">
|
<div className="pt-0">
|
||||||
<p className="text-2xl font-bold text-green-600">
|
<p className="text-2xl font-bold text-green-600">
|
||||||
{formatCurrency(totalReceitas)}
|
{formatCurrency(totalReceitas)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardSpotlight>
|
</SimpleCard>
|
||||||
|
|
||||||
<CardSpotlight className="border-2 border-red-200">
|
<SimpleCard className="border-red-200">
|
||||||
<div className="pb-2 bg-red-50 rounded-t-lg -m-6 mb-4 p-6">
|
<div className="pb-2 bg-red-50 rounded-t-lg -m-6 mb-4 p-6">
|
||||||
<div className="text-red-700 flex items-center font-bold relative z-20">
|
<div className="text-red-700 flex items-center font-bold">
|
||||||
<ArrowDown className="h-5 w-5 mr-2 text-red-600" />
|
<ArrowDown className="h-5 w-5 mr-2 text-red-600" />
|
||||||
Gastos do mês
|
Gastos do mês
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-0 relative z-20">
|
<div className="pt-0">
|
||||||
<p className="text-2xl font-bold text-red-600">
|
<p className="text-2xl font-bold text-red-600">
|
||||||
{formatCurrency(totalDespesas)}
|
{formatCurrency(totalDespesas)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardSpotlight>
|
</SimpleCard>
|
||||||
|
|
||||||
<CardSpotlight className="border-2 border-blue-200">
|
<SimpleCard className="border-blue-200">
|
||||||
<div className="pb-2 bg-blue-50 rounded-t-lg -m-6 mb-4 p-6">
|
<div className="pb-2 bg-blue-50 rounded-t-lg -m-6 mb-4 p-6">
|
||||||
<div className="text-blue-700 flex items-center font-bold relative z-20">
|
<div className="text-blue-700 flex items-center font-bold">
|
||||||
<CreditCard className="h-5 w-5 mr-2 text-blue-600" />
|
<CreditCard className="h-5 w-5 mr-2 text-blue-600" />
|
||||||
Gastos em cartões
|
Gastos em cartões
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-0 space-y-1 relative z-20">
|
<div className="pt-0 space-y-1">
|
||||||
<p className="text-2xl font-bold text-blue-600">
|
<p className="text-2xl font-bold text-blue-600">
|
||||||
{formatCurrency(totalCartoes)}
|
{formatCurrency(totalCartoes)}
|
||||||
</p>
|
</p>
|
||||||
@ -65,7 +65,7 @@ export const TransactionSummaryCards = ({
|
|||||||
<span className="font-bold text-red-600">{formatCurrency(totalGeral)}</span>
|
<span className="font-bold text-red-600">{formatCurrency(totalGeral)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardSpotlight>
|
</SimpleCard>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
156
src/components/ui/3d-card.tsx
Normal file
156
src/components/ui/3d-card.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useState,
|
||||||
|
useContext,
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
const MouseEnterContext = createContext<
|
||||||
|
[boolean, React.Dispatch<React.SetStateAction<boolean>>] | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export const CardContainer = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
containerClassName,
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
containerClassName?: string;
|
||||||
|
}) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isMouseEntered, setIsMouseEntered] = useState(false);
|
||||||
|
|
||||||
|
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const { left, top, width, height } =
|
||||||
|
containerRef.current.getBoundingClientRect();
|
||||||
|
const x = (e.clientX - left - width / 2) / 25;
|
||||||
|
const y = (e.clientY - top - height / 2) / 25;
|
||||||
|
containerRef.current.style.transform = `rotateY(${x}deg) rotateX(${y}deg)`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
setIsMouseEntered(true);
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
setIsMouseEntered(false);
|
||||||
|
containerRef.current.style.transform = `rotateY(0deg) rotateX(0deg)`;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<MouseEnterContext.Provider value={[isMouseEntered, setIsMouseEntered]}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"py-20 flex items-center justify-center",
|
||||||
|
containerClassName
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
perspective: "1000px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center relative transition-all duration-200 ease-linear",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
transformStyle: "preserve-3d",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MouseEnterContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CardBody = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-96 w-96 [transform-style:preserve-3d] [&>*]:[transform-style:preserve-3d]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CardItem = ({
|
||||||
|
as: Tag = "div",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
translateX = 0,
|
||||||
|
translateY = 0,
|
||||||
|
translateZ = 0,
|
||||||
|
rotateX = 0,
|
||||||
|
rotateY = 0,
|
||||||
|
rotateZ = 0,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
as?: React.ElementType;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
translateX?: number | string;
|
||||||
|
translateY?: number | string;
|
||||||
|
translateZ?: number | string;
|
||||||
|
rotateX?: number | string;
|
||||||
|
rotateY?: number | string;
|
||||||
|
rotateZ?: number | string;
|
||||||
|
[key: string]: any;
|
||||||
|
}) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [isMouseEntered] = useMouseEnter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleAnimations();
|
||||||
|
}, [isMouseEntered]);
|
||||||
|
|
||||||
|
const handleAnimations = () => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
if (isMouseEntered) {
|
||||||
|
ref.current.style.transform = `translateX(${translateX}px) translateY(${translateY}px) translateZ(${translateZ}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg)`;
|
||||||
|
} else {
|
||||||
|
ref.current.style.transform = `translateX(0px) translateY(0px) translateZ(0px) rotateX(0deg) rotateY(0deg) rotateZ(0deg)`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
ref={ref}
|
||||||
|
className={cn("w-fit transition duration-200 ease-linear", className)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a hook to use the context
|
||||||
|
export const useMouseEnter = () => {
|
||||||
|
const context = useContext(MouseEnterContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useMouseEnter must be used within a MouseEnterProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
31
src/components/ui/simple-card.tsx
Normal file
31
src/components/ui/simple-card.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface SimpleCardProps {
|
||||||
|
title?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
headerClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SimpleCard: React.FC<SimpleCardProps> = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
headerClassName
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card className={cn("border-2 shadow-sm hover:shadow-md transition-shadow", className)}>
|
||||||
|
{title && (
|
||||||
|
<CardHeader className={cn("pb-3", headerClassName)}>
|
||||||
|
<CardTitle className="text-lg font-semibold">{title}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
)}
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
{children}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,71 +1,50 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { getCategorySummary } from '@/services/transacao';
|
import { Button } from "@/components/ui/button";
|
||||||
import { CategorySummary } from '@/types/financialTypes';
|
import { Input } from "@/components/ui/input";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Plus, Search, Edit, Trash2 } from 'lucide-react';
|
||||||
import {
|
import { useToast } from "@/hooks/use-toast";
|
||||||
Table,
|
import { SimpleCard } from "@/components/ui/simple-card";
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
|
|
||||||
const CategoriasPage = () => {
|
interface Categoria {
|
||||||
const [categories, setCategories] = useState<CategorySummary[]>([]);
|
id: string;
|
||||||
|
nome: string;
|
||||||
|
tipo: 'receita' | 'despesa';
|
||||||
|
cor: string;
|
||||||
|
total?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Categorias = () => {
|
||||||
|
const [categorias, setCategorias] = useState<Categoria[]>([]);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [tipoFiltro, setTipoFiltro] = useState<string>('despesa');
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Atualizar o userId no localStorage para garantir consistência
|
// Mock data - replace with actual API calls
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedUserId = localStorage.getItem('userId');
|
const mockCategorias: Categoria[] = [
|
||||||
const finDashUser = localStorage.getItem('finDashUser');
|
{ id: '1', nome: 'Alimentação', tipo: 'despesa', cor: '#ef4444', total: 850.50 },
|
||||||
|
{ id: '2', nome: 'Transporte', tipo: 'despesa', cor: '#f97316', total: 420.30 },
|
||||||
|
{ id: '3', nome: 'Lazer', tipo: 'despesa', cor: '#eab308', total: 300.00 },
|
||||||
|
{ id: '4', nome: 'Salário', tipo: 'receita', cor: '#22c55e', total: 5000.00 },
|
||||||
|
{ id: '5', nome: 'Freelance', tipo: 'receita', cor: '#10b981', total: 1200.00 },
|
||||||
|
{ id: '6', nome: 'Educação', tipo: 'despesa', cor: '#3b82f6', total: 450.00 },
|
||||||
|
];
|
||||||
|
|
||||||
if (!storedUserId && finDashUser) {
|
setTimeout(() => {
|
||||||
localStorage.setItem('userId', finDashUser);
|
setCategorias(mockCategorias);
|
||||||
} else if (!storedUserId && !finDashUser) {
|
setIsLoading(false);
|
||||||
const defaultId = '9f267008-9128-4a2f-b730-de0a0b5602a9';
|
}, 1000);
|
||||||
localStorage.setItem('userId', defaultId);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadCategories = async (tipo: string) => {
|
const filteredCategorias = categorias.filter(categoria =>
|
||||||
try {
|
categoria.nome.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
setIsLoading(true);
|
);
|
||||||
console.log(`Carregando dados de categorias para ${tipo}...`);
|
|
||||||
const data = await getCategorySummary(tipo);
|
|
||||||
console.log(`${data.length} categorias carregadas com sucesso`);
|
|
||||||
setCategories(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro ao carregar categorias:", error);
|
|
||||||
toast({
|
|
||||||
title: "Erro ao carregar categorias",
|
|
||||||
description: "Não foi possível obter os dados do Supabase",
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
const receitas = filteredCategorias.filter(cat => cat.tipo === 'receita');
|
||||||
loadCategories(tipoFiltro);
|
const despesas = filteredCategorias.filter(cat => cat.tipo === 'despesa');
|
||||||
}, [tipoFiltro, toast]);
|
|
||||||
|
|
||||||
const handleTipoChange = (value: string) => {
|
|
||||||
setTipoFiltro(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatCurrency = (value: number) => {
|
const formatCurrency = (value: number) => {
|
||||||
return new Intl.NumberFormat('pt-BR', {
|
return new Intl.NumberFormat('pt-BR', {
|
||||||
@ -74,138 +53,172 @@ const CategoriasPage = () => {
|
|||||||
}).format(value);
|
}).format(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const totalGastos = categories.reduce((total, category) => total + category.valor, 0);
|
const handleEdit = (categoria: Categoria) => {
|
||||||
|
toast({
|
||||||
|
title: "Editar categoria",
|
||||||
|
description: `Função de edição para ${categoria.nome} será implementada`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const handleDelete = (categoria: Categoria) => {
|
||||||
<div className="space-y-6">
|
toast({
|
||||||
<div className="flex justify-between items-center">
|
title: "Excluir categoria",
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Categorias</h1>
|
description: `Função de exclusão para ${categoria.nome} será implementada`,
|
||||||
<div className="flex items-center gap-2">
|
});
|
||||||
<span className="text-sm text-muted-foreground">Filtrar por:</span>
|
};
|
||||||
<Select value={tipoFiltro} onValueChange={handleTipoChange}>
|
|
||||||
<SelectTrigger className="w-[180px]">
|
if (isLoading) {
|
||||||
<SelectValue placeholder="Tipo de transação" />
|
return (
|
||||||
</SelectTrigger>
|
<div className="space-y-6">
|
||||||
<SelectContent>
|
<div className="flex justify-between items-center">
|
||||||
<SelectItem value="despesa">Despesas</SelectItem>
|
<h1 className="text-2xl font-bold">Categorias</h1>
|
||||||
<SelectItem value="receita">Receitas</SelectItem>
|
<Button disabled>
|
||||||
<SelectItem value="all">Todos</SelectItem>
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
</SelectContent>
|
Nova Categoria
|
||||||
</Select>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
<Card key={i} className="animate-pulse">
|
<Card key={i} className="animate-pulse">
|
||||||
<CardContent className="p-6">
|
<CardHeader>
|
||||||
<div className="pb-2">
|
<div className="h-4 bg-muted rounded w-3/4"></div>
|
||||||
<div className="h-5 bg-muted rounded w-1/2"></div>
|
</CardHeader>
|
||||||
</div>
|
<CardContent>
|
||||||
<div>
|
<div className="h-6 bg-muted rounded w-1/2 mb-2"></div>
|
||||||
<div className="h-4 bg-muted rounded w-1/4 mb-3"></div>
|
<div className="h-3 bg-muted rounded w-full"></div>
|
||||||
<div className="h-2 bg-muted rounded"></div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : categories.length === 0 ? (
|
</div>
|
||||||
<Card>
|
);
|
||||||
<CardContent className="p-6">
|
}
|
||||||
<div className="py-10 text-center">
|
|
||||||
<p className="text-muted-foreground">Nenhuma categoria encontrada</p>
|
|
||||||
<p className="mt-2 text-sm">
|
|
||||||
{tipoFiltro === 'despesa'
|
|
||||||
? 'Verifique se existem transações do tipo "despesa" cadastradas'
|
|
||||||
: tipoFiltro === 'receita'
|
|
||||||
? 'Verifique se existem transações do tipo "receita" cadastradas'
|
|
||||||
: 'Verifique se existem transações cadastradas'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{categories.map((category) => (
|
|
||||||
<Card key={category.categoria}>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="pb-2">
|
|
||||||
<div className="text-base flex items-center font-semibold">
|
|
||||||
<span
|
|
||||||
className="w-3 h-3 rounded-full mr-2"
|
|
||||||
style={{ backgroundColor: category.color }}
|
|
||||||
/>
|
|
||||||
{category.categoria}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-2xl font-bold mb-2">
|
|
||||||
{formatCurrency(category.valor)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Progress
|
|
||||||
value={category.percentage * 100}
|
|
||||||
className="h-2"
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.1)',
|
|
||||||
'--progress-background': category.color
|
|
||||||
} as React.CSSProperties}
|
|
||||||
/>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
{(category.percentage * 100).toFixed(1)}% do total
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card className="mt-6">
|
return (
|
||||||
<CardContent className="p-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div className="flex justify-between items-center">
|
||||||
<h3 className="text-xl font-bold mb-4">Resumo de Categorias {tipoFiltro !== 'all' ? (tipoFiltro === 'despesa' ? '(Despesas)' : '(Receitas)') : ''}</h3>
|
<h1 className="text-2xl font-bold">Categorias</h1>
|
||||||
</div>
|
<Button>
|
||||||
<div>
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
<Table>
|
Nova Categoria
|
||||||
<TableHeader>
|
</Button>
|
||||||
<TableRow>
|
</div>
|
||||||
<TableHead>Categoria</TableHead>
|
|
||||||
<TableHead>Valor</TableHead>
|
<SimpleCard className="border-gray-200">
|
||||||
<TableHead>Percentual</TableHead>
|
<div className="relative">
|
||||||
</TableRow>
|
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||||
</TableHeader>
|
<Input
|
||||||
<TableBody>
|
placeholder="Buscar categorias..."
|
||||||
{categories.map((category) => (
|
value={searchTerm}
|
||||||
<TableRow key={category.categoria}>
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
<TableCell className="font-medium flex items-center">
|
className="pl-10"
|
||||||
<span
|
/>
|
||||||
className="w-3 h-3 rounded-full mr-2"
|
</div>
|
||||||
style={{ backgroundColor: category.color }}
|
</SimpleCard>
|
||||||
/>
|
|
||||||
{category.categoria}
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
</TableCell>
|
<SimpleCard title="Receitas" className="border-green-200">
|
||||||
<TableCell>{formatCurrency(category.valor)}</TableCell>
|
<div className="space-y-3">
|
||||||
<TableCell>{(category.percentage * 100).toFixed(1)}%</TableCell>
|
{receitas.length === 0 ? (
|
||||||
</TableRow>
|
<p className="text-muted-foreground text-center py-4">
|
||||||
))}
|
Nenhuma categoria de receita encontrada
|
||||||
<TableRow className="bg-muted/50">
|
</p>
|
||||||
<TableCell className="font-bold">Total</TableCell>
|
) : (
|
||||||
<TableCell className="font-bold">{formatCurrency(totalGastos)}</TableCell>
|
receitas.map((categoria) => (
|
||||||
<TableCell>100%</TableCell>
|
<div
|
||||||
</TableRow>
|
key={categoria.id}
|
||||||
</TableBody>
|
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
</Table>
|
>
|
||||||
</div>
|
<div className="flex items-center space-x-3">
|
||||||
</CardContent>
|
<div
|
||||||
</Card>
|
className="w-4 h-4 rounded-full"
|
||||||
</>
|
style={{ backgroundColor: categoria.cor }}
|
||||||
)}
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{categoria.nome}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{categoria.total ? formatCurrency(categoria.total) : 'R$ 0,00'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Badge variant="secondary" className="bg-green-100 text-green-800">
|
||||||
|
{categoria.tipo}
|
||||||
|
</Badge>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEdit(categoria)}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(categoria)}
|
||||||
|
className="text-red-600 hover:text-red-700"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SimpleCard>
|
||||||
|
|
||||||
|
<SimpleCard title="Despesas" className="border-red-200">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{despesas.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground text-center py-4">
|
||||||
|
Nenhuma categoria de despesa encontrada
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
despesas.map((categoria) => (
|
||||||
|
<div
|
||||||
|
key={categoria.id}
|
||||||
|
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div
|
||||||
|
className="w-4 h-4 rounded-full"
|
||||||
|
style={{ backgroundColor: categoria.cor }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{categoria.nome}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{categoria.total ? formatCurrency(categoria.total) : 'R$ 0,00'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Badge variant="secondary" className="bg-red-100 text-red-800">
|
||||||
|
{categoria.tipo}
|
||||||
|
</Badge>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEdit(categoria)}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(categoria)}
|
||||||
|
className="text-red-600 hover:text-red-700"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SimpleCard>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CategoriasPage;
|
export default Categorias;
|
||||||
|
|||||||
@ -2,12 +2,13 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { ArrowUpIcon, ArrowDownIcon, CreditCardIcon, Target } from "lucide-react";
|
import { ArrowUpIcon, ArrowDownIcon, CreditCardIcon, Target, TrendingUpIcon, TrendingDownIcon } from "lucide-react";
|
||||||
import { getResumoFinanceiro, getCategorySummary } from "@/services/transacao";
|
import { getResumoFinanceiro, getCategorySummary } from "@/services/transacao";
|
||||||
import { ResumoFinanceiro, CategorySummary } from "@/types/financialTypes";
|
import { ResumoFinanceiro, CategorySummary } from "@/types/financialTypes";
|
||||||
import TransactionsTable from "@/components/dashboard/TransactionsTable";
|
import TransactionsTable from "@/components/dashboard/TransactionsTable";
|
||||||
import { useTransactions } from "@/hooks/useTransactions";
|
import { useTransactions } from "@/hooks/useTransactions";
|
||||||
import CategoryChart from "@/components/dashboard/CategoryChart";
|
import CategoryChart from "@/components/dashboard/CategoryChart";
|
||||||
|
import { SimpleCard } from "@/components/ui/simple-card";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const [resumo, setResumo] = useState<ResumoFinanceiro | null>(null);
|
const [resumo, setResumo] = useState<ResumoFinanceiro | null>(null);
|
||||||
@ -86,6 +87,8 @@ const Dashboard = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saldo = resumo ? resumo.totalReceitas - resumo.totalDespesas - (resumo.totalCartoes || 0) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6" data-tour="dashboard-content">
|
<div className="space-y-6" data-tour="dashboard-content">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -139,43 +142,67 @@ const Dashboard = () => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className={`text-2xl font-bold ${
|
<div className={`text-2xl font-bold ${
|
||||||
resumo && (resumo.totalReceitas - resumo.totalDespesas - (resumo.totalCartoes || 0)) >= 0
|
saldo >= 0 ? 'text-green-600' : 'text-red-600'
|
||||||
? 'text-green-600'
|
|
||||||
: 'text-red-600'
|
|
||||||
}`}>
|
}`}>
|
||||||
{resumo ? formatCurrency(resumo.totalReceitas - resumo.totalDespesas - (resumo.totalCartoes || 0)) : 'R$ 0,00'}
|
{resumo ? formatCurrency(saldo) : 'R$ 0,00'}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">Resultado do mês</p>
|
<p className="text-xs text-muted-foreground">Resultado do mês</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="space-y-6">
|
||||||
<Card className="md:col-span-1">
|
{/* 1. Receitas vs Despesas */}
|
||||||
<CardHeader>
|
<SimpleCard title="Receitas vs Despesas" className="border-green-200">
|
||||||
<CardTitle>Despesas por Categoria</CardTitle>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<CardDescription>Distribuição dos seus gastos</CardDescription>
|
<div className="flex items-center space-x-3">
|
||||||
</CardHeader>
|
<div className="p-2 bg-green-100 rounded-lg">
|
||||||
<CardContent>
|
<TrendingUpIcon className="h-6 w-6 text-green-600" />
|
||||||
<CategoryChart categories={categories} isLoading={isLoading} />
|
</div>
|
||||||
</CardContent>
|
<div>
|
||||||
</Card>
|
<p className="text-sm text-muted-foreground">Receitas</p>
|
||||||
|
<p className="text-xl font-bold text-green-600">
|
||||||
|
{resumo ? formatCurrency(resumo.totalReceitas) : 'R$ 0,00'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-2 bg-red-100 rounded-lg">
|
||||||
|
<TrendingDownIcon className="h-6 w-6 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Despesas</p>
|
||||||
|
<p className="text-xl font-bold text-red-600">
|
||||||
|
{resumo ? formatCurrency(resumo.totalDespesas + (resumo.totalCartoes || 0)) : 'R$ 0,00'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 pt-4 border-t">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium">Resultado:</span>
|
||||||
|
<span className={`text-lg font-bold ${saldo >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{resumo ? formatCurrency(saldo) : 'R$ 0,00'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SimpleCard>
|
||||||
|
|
||||||
<Card className="md:col-span-1">
|
{/* 2. Gastos por Categoria */}
|
||||||
<CardHeader>
|
<SimpleCard title="Despesas por Categoria" className="border-orange-200">
|
||||||
<CardTitle>Últimas Transações</CardTitle>
|
<CategoryChart categories={categories} isLoading={isLoading} />
|
||||||
<CardDescription>Suas transações mais recentes</CardDescription>
|
</SimpleCard>
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
{/* 3. Últimas Transações */}
|
||||||
<TransactionsTable
|
<SimpleCard title="Últimas Transações" className="border-blue-200">
|
||||||
transactions={transactions.slice(0, 5)}
|
<TransactionsTable
|
||||||
isLoading={transactionsLoading}
|
transactions={transactions.slice(0, 5)}
|
||||||
showPagination={false}
|
isLoading={transactionsLoading}
|
||||||
onEdit={() => {}}
|
showPagination={false}
|
||||||
onDelete={() => {}}
|
onEdit={() => {}}
|
||||||
/>
|
onDelete={() => {}}
|
||||||
</CardContent>
|
/>
|
||||||
</Card>
|
</SimpleCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
BarChart3,
|
BarChart3,
|
||||||
Calendar
|
Calendar
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { CardContainer, CardBody, CardItem } from '@/components/ui/3d-card';
|
||||||
|
|
||||||
const Landing = () => {
|
const Landing = () => {
|
||||||
const features = [
|
const features = [
|
||||||
@ -178,7 +179,7 @@ const Landing = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-6xl mx-auto space-y-20">
|
<div className="max-w-6xl mx-auto space-y-20">
|
||||||
{/* Step 1 - WhatsApp Methods */}
|
{/* Step 1 - WhatsApp Methods with 3D Cards */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-green-500 text-white rounded-full text-xl font-bold mb-6">
|
<div className="inline-flex items-center justify-center w-12 h-12 bg-green-500 text-white rounded-full text-xl font-bold mb-6">
|
||||||
{howItWorksSteps[0].step}
|
{howItWorksSteps[0].step}
|
||||||
@ -188,29 +189,47 @@ const Landing = () => {
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
{howItWorksSteps[0].methods.map((method, index) => (
|
{howItWorksSteps[0].methods.map((method, index) => (
|
||||||
<div key={index} className="bg-slate-700/50 p-6 rounded-xl border border-slate-600 hover:border-green-500 transition-all duration-300">
|
<CardContainer key={index} className="inter-var" containerClassName="py-10">
|
||||||
<div className="bg-green-500/20 p-3 rounded-lg w-fit mx-auto mb-4 text-green-400">
|
<CardBody className="bg-slate-700/50 relative group/card hover:shadow-2xl hover:shadow-emerald-500/[0.1] border-slate-600 w-auto h-auto rounded-xl p-6 border">
|
||||||
{method.icon}
|
<CardItem
|
||||||
</div>
|
translateZ="50"
|
||||||
<h4 className="text-xl font-semibold text-white mb-2">{method.title}</h4>
|
className="bg-green-500/20 p-3 rounded-lg w-fit mx-auto mb-4 text-green-400"
|
||||||
<p className="text-slate-300 mb-4">{method.description}</p>
|
>
|
||||||
<div className="aspect-video bg-slate-800 rounded-lg flex items-center justify-center overflow-hidden">
|
{method.icon}
|
||||||
<img
|
</CardItem>
|
||||||
src={method.image}
|
<CardItem
|
||||||
alt={`Exemplo ${method.title}`}
|
translateZ="60"
|
||||||
className="max-w-full max-h-full object-contain rounded-lg"
|
className="text-xl font-semibold text-white mb-2 text-center"
|
||||||
onError={(e) => {
|
>
|
||||||
console.error(`Failed to load image: ${method.image}`);
|
{method.title}
|
||||||
e.currentTarget.style.display = 'none';
|
</CardItem>
|
||||||
}}
|
<CardItem
|
||||||
/>
|
as="p"
|
||||||
</div>
|
translateZ="60"
|
||||||
</div>
|
className="text-slate-300 mb-4 text-center"
|
||||||
|
>
|
||||||
|
{method.description}
|
||||||
|
</CardItem>
|
||||||
|
<CardItem translateZ="100" className="w-full">
|
||||||
|
<div className="aspect-video bg-slate-800 rounded-lg flex items-center justify-center overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={method.image}
|
||||||
|
alt={`Exemplo ${method.title}`}
|
||||||
|
className="max-w-full max-h-full object-contain rounded-lg group-hover/card:shadow-xl"
|
||||||
|
onError={(e) => {
|
||||||
|
console.error(`Failed to load image: ${method.image}`);
|
||||||
|
e.currentTarget.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardItem>
|
||||||
|
</CardBody>
|
||||||
|
</CardContainer>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step 2 - Dashboard */}
|
{/* Step 2 - Dashboard with 3D Card */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-blue-500 text-white rounded-full text-xl font-bold mb-6">
|
<div className="inline-flex items-center justify-center w-12 h-12 bg-blue-500 text-white rounded-full text-xl font-bold mb-6">
|
||||||
{howItWorksSteps[1].step}
|
{howItWorksSteps[1].step}
|
||||||
@ -219,23 +238,27 @@ const Landing = () => {
|
|||||||
<p className="text-lg text-slate-300 mb-8">{howItWorksSteps[1].description}</p>
|
<p className="text-lg text-slate-300 mb-8">{howItWorksSteps[1].description}</p>
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<div className="bg-slate-700/50 p-6 rounded-xl border border-slate-600">
|
<CardContainer className="inter-var" containerClassName="py-10">
|
||||||
<div className="aspect-video bg-slate-800 rounded-lg flex items-center justify-center overflow-hidden">
|
<CardBody className="bg-slate-700/50 relative group/card hover:shadow-2xl hover:shadow-blue-500/[0.1] border-slate-600 w-full h-auto rounded-xl p-6 border">
|
||||||
<img
|
<CardItem translateZ="100" className="w-full">
|
||||||
src={howItWorksSteps[1].image}
|
<div className="aspect-video bg-slate-800 rounded-lg flex items-center justify-center overflow-hidden">
|
||||||
alt="Dashboard do sistema"
|
<img
|
||||||
className="max-w-full max-h-full object-contain rounded-lg"
|
src={howItWorksSteps[1].image}
|
||||||
onError={(e) => {
|
alt="Dashboard do sistema"
|
||||||
console.error(`Failed to load image: ${howItWorksSteps[1].image}`);
|
className="max-w-full max-h-full object-contain rounded-lg group-hover/card:shadow-xl"
|
||||||
e.currentTarget.style.display = 'none';
|
onError={(e) => {
|
||||||
}}
|
console.error(`Failed to load image: ${howItWorksSteps[1].image}`);
|
||||||
/>
|
e.currentTarget.style.display = 'none';
|
||||||
</div>
|
}}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</CardItem>
|
||||||
|
</CardBody>
|
||||||
|
</CardContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step 3 - Calendar */}
|
{/* Step 3 - Calendar with 3D Card */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-purple-500 text-white rounded-full text-xl font-bold mb-6">
|
<div className="inline-flex items-center justify-center w-12 h-12 bg-purple-500 text-white rounded-full text-xl font-bold mb-6">
|
||||||
{howItWorksSteps[2].step}
|
{howItWorksSteps[2].step}
|
||||||
@ -244,19 +267,23 @@ const Landing = () => {
|
|||||||
<p className="text-lg text-slate-300 mb-8">{howItWorksSteps[2].description}</p>
|
<p className="text-lg text-slate-300 mb-8">{howItWorksSteps[2].description}</p>
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<div className="bg-slate-700/50 p-6 rounded-xl border border-slate-600">
|
<CardContainer className="inter-var" containerClassName="py-10">
|
||||||
<div className="aspect-video bg-slate-800 rounded-lg flex items-center justify-center overflow-hidden">
|
<CardBody className="bg-slate-700/50 relative group/card hover:shadow-2xl hover:shadow-purple-500/[0.1] border-slate-600 w-full h-auto rounded-xl p-6 border">
|
||||||
<img
|
<CardItem translateZ="100" className="w-full">
|
||||||
src={howItWorksSteps[2].image}
|
<div className="aspect-video bg-slate-800 rounded-lg flex items-center justify-center overflow-hidden">
|
||||||
alt="Calendário do sistema"
|
<img
|
||||||
className="max-w-full max-h-full object-contain rounded-lg"
|
src={howItWorksSteps[2].image}
|
||||||
onError={(e) => {
|
alt="Calendário do sistema"
|
||||||
console.error(`Failed to load image: ${howItWorksSteps[2].image}`);
|
className="max-w-full max-h-full object-contain rounded-lg group-hover/card:shadow-xl"
|
||||||
e.currentTarget.style.display = 'none';
|
onError={(e) => {
|
||||||
}}
|
console.error(`Failed to load image: ${howItWorksSteps[2].image}`);
|
||||||
/>
|
e.currentTarget.style.display = 'none';
|
||||||
</div>
|
}}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</CardItem>
|
||||||
|
</CardBody>
|
||||||
|
</CardContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,24 +1,12 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import TransactionsTable from '@/components/dashboard/TransactionsTable';
|
|
||||||
import { useTransactions } from '@/hooks/useTransactions';
|
import { useTransactions } from '@/hooks/useTransactions';
|
||||||
import { TransactionHeader } from '@/components/transacoes/TransactionHeader';
|
|
||||||
import { TransactionSummaryCards } from '@/components/transacoes/TransactionSummaryCards';
|
import { TransactionSummaryCards } from '@/components/transacoes/TransactionSummaryCards';
|
||||||
import { TransactionDialogs } from '@/components/transacoes/TransactionDialogs';
|
import { TransactionDialogs } from '@/components/transacoes/TransactionDialogs';
|
||||||
import { MonthFilter } from '@/components/filters/MonthFilter';
|
import { TransactionHeader } from '@/components/transacoes/TransactionHeader';
|
||||||
import { useAccessControl } from '@/hooks/useAccessControl';
|
import TransactionsTable from '@/components/dashboard/TransactionsTable';
|
||||||
|
import { SimpleCard } from "@/components/ui/simple-card";
|
||||||
const TransacoesPage = () => {
|
|
||||||
// Função para obter o mês atual no formato YYYY-MM
|
|
||||||
const getCurrentMonth = () => {
|
|
||||||
const now = new Date();
|
|
||||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [selectedMonth, setSelectedMonth] = useState<string>(getCurrentMonth());
|
|
||||||
|
|
||||||
const access = useAccessControl();
|
|
||||||
|
|
||||||
|
const Transacoes = () => {
|
||||||
const {
|
const {
|
||||||
transactions,
|
transactions,
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -40,60 +28,15 @@ const TransacoesPage = () => {
|
|||||||
handleOpenDialog,
|
handleOpenDialog,
|
||||||
handleOpenCartaoCreditoDialog,
|
handleOpenCartaoCreditoDialog,
|
||||||
loadTransactions
|
loadTransactions
|
||||||
} = useTransactions({ monthFilter: selectedMonth });
|
} = useTransactions();
|
||||||
|
|
||||||
// Função para formatar o mês para exibição
|
|
||||||
const formatMonthDisplay = (month: string) => {
|
|
||||||
const [year, monthNum] = month.split('-');
|
|
||||||
const months = [
|
|
||||||
'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho',
|
|
||||||
'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'
|
|
||||||
];
|
|
||||||
return `${months[parseInt(monthNum) - 1]} ${year}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Transações</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Dados de: {formatMonthDisplay(selectedMonth)}
|
|
||||||
</p>
|
|
||||||
{/* Exibe mensagem para trial OU para admin-liberou */}
|
|
||||||
{!access.loading && access.podeAdicionarTransacao && (
|
|
||||||
access.adminLiberou ? (
|
|
||||||
<div className="mt-1 text-xs text-green-600 font-bold">
|
|
||||||
{access.motivo}
|
|
||||||
</div>
|
|
||||||
) : (access.diasRestantesTrial > 0 && (
|
|
||||||
<div className="mt-1 text-xs text-blue-600 font-medium">
|
|
||||||
{`Você está em período gratuito. ${access.diasRestantesTrial} dia(s) restante(s) de teste.`}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<MonthFilter
|
|
||||||
selectedMonth={selectedMonth}
|
|
||||||
onMonthChange={setSelectedMonth}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mensagem de bloqueio */}
|
|
||||||
{!access.loading && !access.podeAdicionarTransacao && (
|
|
||||||
<div className="bg-yellow-100 border border-yellow-300 text-yellow-800 px-4 py-3 rounded relative mb-4">
|
|
||||||
<span className="font-medium">Atenção:</span> {access.motivo}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Só passa as funções de adicionar se permitido */}
|
|
||||||
<TransactionHeader
|
<TransactionHeader
|
||||||
onOpenDialog={access.podeAdicionarTransacao ? handleOpenDialog : () => {}}
|
onOpenDialog={handleOpenDialog}
|
||||||
onOpenCartaoCreditoDialog={access.podeAdicionarTransacao ? handleOpenCartaoCreditoDialog : () => {}}
|
onOpenCartaoCreditoDialog={handleOpenCartaoCreditoDialog}
|
||||||
disableAdicionar={!access.podeAdicionarTransacao}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Resumo em Cards */}
|
|
||||||
<TransactionSummaryCards
|
<TransactionSummaryCards
|
||||||
totalReceitas={totalReceitas}
|
totalReceitas={totalReceitas}
|
||||||
totalDespesas={totalDespesas}
|
totalDespesas={totalDespesas}
|
||||||
@ -101,9 +44,7 @@ const TransacoesPage = () => {
|
|||||||
formatCurrency={formatCurrency}
|
formatCurrency={formatCurrency}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Tabela completa de transações */}
|
<SimpleCard title="Todas as Transações" className="border-gray-200">
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-xl font-semibold">Todas as Transações</h2>
|
|
||||||
<TransactionsTable
|
<TransactionsTable
|
||||||
transactions={transactions}
|
transactions={transactions}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@ -111,15 +52,14 @@ const TransacoesPage = () => {
|
|||||||
onEdit={handleEditTransaction}
|
onEdit={handleEditTransaction}
|
||||||
onDelete={loadTransactions}
|
onDelete={loadTransactions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</SimpleCard>
|
||||||
|
|
||||||
{/* Diálogos */}
|
|
||||||
<TransactionDialogs
|
<TransactionDialogs
|
||||||
isDialogOpen={isDialogOpen}
|
isDialogOpen={isDialogOpen}
|
||||||
isCartaoCreditoDialogOpen={isCartaoCreditoDialogOpen}
|
|
||||||
tipoForm={tipoForm}
|
tipoForm={tipoForm}
|
||||||
selectedTransaction={selectedTransaction}
|
selectedTransaction={selectedTransaction}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
|
isCartaoCreditoDialogOpen={isCartaoCreditoDialogOpen}
|
||||||
cartoes={cartoes}
|
cartoes={cartoes}
|
||||||
onTransactionSuccess={handleTransactionSuccess}
|
onTransactionSuccess={handleTransactionSuccess}
|
||||||
onDespesaCartaoSuccess={handleDespesaCartaoSuccess}
|
onDespesaCartaoSuccess={handleDespesaCartaoSuccess}
|
||||||
@ -130,4 +70,4 @@ const TransacoesPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TransacoesPage;
|
export default Transacoes;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user