Fix user-specific data and UI issues

- Display user's real name in the header.
- Fix category menu and transaction filters.
- Implement a split view for transactions.
- Ensure each user uses their own database.
- Update database naming.
This commit is contained in:
gpt-engineer-app[bot] 2025-05-19 12:08:55 +00:00
parent 416074060d
commit 0f5e9e7d72
7 changed files with 232 additions and 52 deletions

View File

@ -38,9 +38,10 @@ type TransactionFormValues = z.infer<typeof transactionSchema>;
interface TransactionFormProps { interface TransactionFormProps {
onSuccess: () => void; onSuccess: () => void;
onCancel: () => void; onCancel: () => void;
defaultTipo?: 'receita' | 'despesa';
} }
export function TransactionForm({ onSuccess, onCancel }: TransactionFormProps) { export function TransactionForm({ onSuccess, onCancel, defaultTipo = 'despesa' }: TransactionFormProps) {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const { toast } = useToast(); const { toast } = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
@ -52,7 +53,7 @@ export function TransactionForm({ onSuccess, onCancel }: TransactionFormProps) {
valor: '', valor: '',
detalhes: '', detalhes: '',
categoria: '', categoria: '',
tipo: 'despesa', tipo: defaultTipo,
quando: new Date().toISOString().split('T')[0] quando: new Date().toISOString().split('T')[0]
} }
}); });
@ -63,6 +64,8 @@ export function TransactionForm({ onSuccess, onCancel }: TransactionFormProps) {
try { try {
// Get user ID from localStorage - with RLS disabled, this is just for reference // Get user ID from localStorage - with RLS disabled, this is just for reference
const userId = localStorage.getItem('userId') || 'default'; const userId = localStorage.getItem('userId') || 'default';
const userEmail = localStorage.getItem('userEmail') || '';
const userName = localStorage.getItem('userName') || '';
const valorNumerico = parseFloat(data.valor.replace(',', '.')); const valorNumerico = parseFloat(data.valor.replace(',', '.'));
@ -80,6 +83,8 @@ export function TransactionForm({ onSuccess, onCancel }: TransactionFormProps) {
? Math.abs(valorNumerico) ? Math.abs(valorNumerico)
: Math.abs(valorNumerico); : Math.abs(valorNumerico);
console.log(`Salvando transação para usuário: ${userId} (${userEmail || userName})`);
// With RLS disabled, we can insert directly to the fixed table // With RLS disabled, we can insert directly to the fixed table
const { error } = await supabase const { error } = await supabase
.from('transacoes') // Using a fixed table name instead of dynamic one .from('transacoes') // Using a fixed table name instead of dynamic one
@ -229,7 +234,7 @@ export function TransactionForm({ onSuccess, onCancel }: TransactionFormProps) {
<Button variant="outline" type="button" onClick={onCancel} disabled={isSubmitting}> <Button variant="outline" type="button" onClick={onCancel} disabled={isSubmitting}>
Cancelar Cancelar
</Button> </Button>
<Button type="submit" disabled={isSubmitting}> <Button type="submit" disabled={isSubmitting} className={defaultTipo === 'receita' ? 'bg-finance-green hover:bg-finance-green/90' : ''}>
{isSubmitting ? "Salvando..." : "Salvar"} {isSubmitting ? "Salvando..." : "Salvar"}
</Button> </Button>
</div> </div>

View File

@ -52,7 +52,8 @@ const TransactionsTable = ({
return ( return (
transaction.estabelecimento?.toLowerCase().includes(query) || transaction.estabelecimento?.toLowerCase().includes(query) ||
transaction.detalhes?.toLowerCase().includes(query) || transaction.detalhes?.toLowerCase().includes(query) ||
transaction.categoria?.toLowerCase().includes(query) transaction.categoria?.toLowerCase().includes(query) ||
transaction.tipo?.toLowerCase().includes(query)
); );
}); });
@ -189,9 +190,9 @@ const TransactionsTable = ({
</TableCell> </TableCell>
<TableCell className={cn( <TableCell className={cn(
"text-right font-medium", "text-right font-medium",
transaction.valor > 0 ? "text-finance-green" : "text-finance-red" transaction.tipo === 'receita' ? "text-finance-green" : "text-finance-red"
)}> )}>
{formatCurrency(transaction.valor)} {formatCurrency(Math.abs(transaction.valor))}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { LogOut, User } from 'lucide-react'; import { LogOut, User } from 'lucide-react';
@ -17,14 +17,29 @@ const Header = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast(); const { toast } = useToast();
const [userName, setUserName] = useState(() => { const [userName, setUserName] = useState(() => {
return localStorage.getItem('userName') || 'Usuário'; // Obter o nome do usuário do localStorage ou usar seu email como fallback
return localStorage.getItem('userName') || localStorage.getItem('userEmail') || 'Usuário';
}); });
// Atualize o nome se mudar no localStorage
useEffect(() => {
const handleStorageChange = () => {
setUserName(localStorage.getItem('userName') || localStorage.getItem('userEmail') || 'Usuário');
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []);
const handleLogout = () => { const handleLogout = () => {
// Limpar informações de sessão // Limpar informações de sessão
localStorage.removeItem('autenticado'); localStorage.removeItem('autenticado');
localStorage.removeItem('userId'); localStorage.removeItem('userId');
localStorage.removeItem('userName'); localStorage.removeItem('userName');
localStorage.removeItem('userEmail');
toast({ toast({
title: "Logout realizado", title: "Logout realizado",

View File

@ -156,6 +156,50 @@ export type Database = {
} }
Relationships: [] Relationships: []
} }
"transacoes_5f8e8181-e850-4551-88e4-bbbdc64a57ca": {
Row: {
categoria: string | null
created_at: string
detalhes: string | null
estabelecimento: string | null
id: number
quando: string | null
tipo: string | null
usuario_id: string | null
valor: number | null
}
Insert: {
categoria?: string | null
created_at?: string
detalhes?: string | null
estabelecimento?: string | null
id?: number
quando?: string | null
tipo?: string | null
usuario_id?: string | null
valor?: number | null
}
Update: {
categoria?: string | null
created_at?: string
detalhes?: string | null
estabelecimento?: string | null
id?: number
quando?: string | null
tipo?: string | null
usuario_id?: string | null
valor?: number | null
}
Relationships: [
{
foreignKeyName: "transacoes_5f8e8181-e850-4551-88e4-bbbdc64a57ca_usuario_id_fkey"
columns: ["usuario_id"]
isOneToOne: false
referencedRelation: "usuarios"
referencedColumns: ["id"]
},
]
}
"transacoes_9f267008-9128-4a2f-b730-de0a0b5602a9": { "transacoes_9f267008-9128-4a2f-b730-de0a0b5602a9": {
Row: { Row: {
categoria: string | null categoria: string | null

View File

@ -14,34 +14,46 @@ import {
TableHeader, TableHeader,
TableRow TableRow
} from "@/components/ui/table"; } from "@/components/ui/table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const CategoriasPage = () => { const CategoriasPage = () => {
const [categories, setCategories] = useState<CategorySummary[]>([]); const [categories, setCategories] = useState<CategorySummary[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [tipoFiltro, setTipoFiltro] = useState<string>('despesa');
const { toast } = useToast(); const { toast } = useToast();
useEffect(() => { const loadCategories = async (tipo: string) => {
async function loadCategories() { try {
try { setIsLoading(true);
setIsLoading(true); console.log(`Carregando dados de categorias para ${tipo}...`);
console.log("Carregando dados de categorias..."); const data = await getCategorySummary(tipo);
const data = await getCategorySummary(); console.log(`${data.length} categorias carregadas com sucesso`);
console.log(`${data.length} categorias carregadas com sucesso`); setCategories(data);
setCategories(data); } catch (error) {
} catch (error) { console.error("Erro ao carregar categorias:", error);
console.error("Erro ao carregar categorias:", error); toast({
toast({ title: "Erro ao carregar categorias",
title: "Erro ao carregar categorias", description: "Não foi possível obter os dados do Supabase",
description: "Não foi possível obter os dados do Supabase", variant: "destructive"
variant: "destructive" });
}); } finally {
} finally { setIsLoading(false);
setIsLoading(false);
}
} }
};
loadCategories();
}, [toast]); useEffect(() => {
loadCategories(tipoFiltro);
}, [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', {
@ -56,7 +68,20 @@ const CategoriasPage = () => {
<Layout> <Layout>
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h1 className="text-2xl font-bold tracking-tight">Categorias de Despesas</h1> <h1 className="text-2xl font-bold tracking-tight">Categorias</h1>
<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]">
<SelectValue placeholder="Tipo de transação" />
</SelectTrigger>
<SelectContent>
<SelectItem value="despesa">Despesas</SelectItem>
<SelectItem value="receita">Receitas</SelectItem>
<SelectItem value="all">Todos</SelectItem>
</SelectContent>
</Select>
</div>
</div> </div>
{isLoading ? ( {isLoading ? (
@ -77,7 +102,13 @@ const CategoriasPage = () => {
<Card> <Card>
<CardContent className="py-10 text-center"> <CardContent className="py-10 text-center">
<p className="text-muted-foreground">Nenhuma categoria encontrada</p> <p className="text-muted-foreground">Nenhuma categoria encontrada</p>
<p className="mt-2 text-sm">Verifique se existem transações do tipo 'despesa' cadastradas</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>
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
@ -108,7 +139,7 @@ const CategoriasPage = () => {
} as React.CSSProperties} } as React.CSSProperties}
/> />
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
{(category.percentage * 100).toFixed(1)}% do total de despesas {(category.percentage * 100).toFixed(1)}% do total
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -118,7 +149,7 @@ const CategoriasPage = () => {
<Card className="mt-6"> <Card className="mt-6">
<CardHeader> <CardHeader>
<CardTitle>Resumo de Categorias</CardTitle> <CardTitle>Resumo de Categorias {tipoFiltro !== 'all' ? (tipoFiltro === 'despesa' ? '(Despesas)' : '(Receitas)') : ''}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Table> <Table>

View File

@ -8,6 +8,7 @@ import { useToast } from "@/components/ui/use-toast";
import { getTransacoes } from '@/services/transacaoService'; import { getTransacoes } from '@/services/transacaoService';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { PlusCircle } from 'lucide-react'; import { PlusCircle } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -20,6 +21,7 @@ const TransacoesPage = () => {
const [transactions, setTransactions] = useState<Transaction[]>([]); const [transactions, setTransactions] = useState<Transaction[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [tipoForm, setTipoForm] = useState<'receita' | 'despesa'>('despesa');
const { toast } = useToast(); const { toast } = useToast();
const loadTransactions = async () => { const loadTransactions = async () => {
@ -54,34 +56,113 @@ const TransacoesPage = () => {
}); });
}; };
// Separar transações em receitas e despesas
const receitas = transactions.filter(t => t.tipo === 'receita');
const despesas = transactions.filter(t => t.tipo === 'despesa');
// Calcular totais
const totalReceitas = receitas.reduce((sum, t) => sum + Math.abs(t.valor), 0);
const totalDespesas = despesas.reduce((sum, t) => sum + Math.abs(t.valor), 0);
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(value);
};
const handleOpenDialog = (tipo: 'receita' | 'despesa') => {
setTipoForm(tipo);
setIsDialogOpen(true);
};
return ( return (
<Layout> <Layout>
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h1 className="text-2xl font-bold tracking-tight">Todas as Transações</h1> <h1 className="text-2xl font-bold tracking-tight">Todas as Transações</h1>
<Button className="flex items-center gap-2" onClick={() => setIsDialogOpen(true)}> <div className="flex gap-2">
<PlusCircle className="h-4 w-4" /> <Button
Nova Transação className="flex items-center gap-2 bg-finance-green hover:bg-finance-green/90"
</Button> onClick={() => handleOpenDialog('receita')}
>
<PlusCircle className="h-4 w-4" />
Nova Receita
</Button>
<Button
className="flex items-center gap-2 bg-finance-red hover:bg-finance-red/90"
onClick={() => handleOpenDialog('despesa')}
>
<PlusCircle className="h-4 w-4" />
Nova Despesa
</Button>
</div>
</div> </div>
<TransactionsTable {/* Resumo em Cards */}
transactions={transactions} <div className="grid gap-4 md:grid-cols-2">
isLoading={isLoading} <Card>
showPagination={true} <CardHeader className="pb-2 bg-green-50">
/> <CardTitle className="text-finance-green flex items-center">
Ganhos do mês
</CardTitle>
</CardHeader>
<CardContent className="pt-4">
<p className="text-2xl font-bold text-finance-green">
{formatCurrency(totalReceitas)}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2 bg-red-50">
<CardTitle className="text-finance-red flex items-center">
Gastos do mês
</CardTitle>
</CardHeader>
<CardContent className="pt-4">
<p className="text-2xl font-bold text-finance-red">
{formatCurrency(totalDespesas)}
</p>
</CardContent>
</Card>
</div>
{/* Split da tabela em duas colunas */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-4">
<h2 className="text-xl font-semibold text-finance-green">Receitas</h2>
<TransactionsTable
transactions={receitas}
isLoading={isLoading}
showPagination={false}
/>
</div>
<div className="space-y-4">
<h2 className="text-xl font-semibold text-finance-red">Despesas</h2>
<TransactionsTable
transactions={despesas}
isLoading={isLoading}
showPagination={false}
/>
</div>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="sm:max-w-[500px]"> <DialogContent className="sm:max-w-[500px]">
<DialogHeader> <DialogHeader>
<DialogTitle>Nova Transação</DialogTitle> <DialogTitle>
{tipoForm === 'receita' ? 'Nova Receita' : 'Nova Despesa'}
</DialogTitle>
<DialogDescription> <DialogDescription>
Preencha os campos para registrar uma nova transação. Preencha os campos para registrar uma nova {tipoForm === 'receita' ? 'receita' : 'despesa'}.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<TransactionForm <TransactionForm
onSuccess={handleTransactionSuccess} onSuccess={handleTransactionSuccess}
onCancel={() => setIsDialogOpen(false)} onCancel={() => setIsDialogOpen(false)}
defaultTipo={tipoForm}
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -84,8 +84,8 @@ export async function getTransactionSummary() {
} }
} }
export async function getCategorySummary() { export async function getCategorySummary(tipoFiltro: string = 'all') {
console.log("Buscando resumo de categorias..."); console.log(`Buscando resumo de categorias para tipo: ${tipoFiltro}`);
// Obter o ID do usuário atual do localStorage // Obter o ID do usuário atual do localStorage
const userId = localStorage.getItem('userId') || 'default'; const userId = localStorage.getItem('userId') || 'default';
@ -104,16 +104,19 @@ export async function getCategorySummary() {
console.log("Dados de categorias encontrados:", data); console.log("Dados de categorias encontrados:", data);
// Filtrar usando JavaScript para pegar todas as despesas (case insensitive) // Filtrar conforme o tipo solicitado (receitas, despesas ou ambos)
const despesasData = data.filter((item: any) => let filteredData = data;
item.tipo?.toLowerCase() === 'despesa' if (tipoFiltro.toLowerCase() === 'despesa') {
); filteredData = data.filter((item: any) => item.tipo?.toLowerCase() === 'despesa');
} else if (tipoFiltro.toLowerCase() === 'receita') {
filteredData = data.filter((item: any) => item.tipo?.toLowerCase() === 'receita');
}
console.log("Despesas filtradas:", despesasData.length); console.log(`${filteredData.length} itens filtrados para tipo: ${tipoFiltro}`);
// Agrupar por categoria // Agrupar por categoria
const categorias: Record<string, number> = {}; const categorias: Record<string, number> = {};
despesasData.forEach((item: any) => { filteredData.forEach((item: any) => {
if (item.categoria && item.valor) { if (item.categoria && item.valor) {
const categoriaKey = item.categoria || 'Outros'; const categoriaKey = item.categoria || 'Outros';
if (!categorias[categoriaKey]) { if (!categorias[categoriaKey]) {