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:
parent
416074060d
commit
0f5e9e7d72
@ -38,9 +38,10 @@ type TransactionFormValues = z.infer<typeof transactionSchema>;
|
||||
interface TransactionFormProps {
|
||||
onSuccess: () => 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 { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
@ -52,7 +53,7 @@ export function TransactionForm({ onSuccess, onCancel }: TransactionFormProps) {
|
||||
valor: '',
|
||||
detalhes: '',
|
||||
categoria: '',
|
||||
tipo: 'despesa',
|
||||
tipo: defaultTipo,
|
||||
quando: new Date().toISOString().split('T')[0]
|
||||
}
|
||||
});
|
||||
@ -63,6 +64,8 @@ export function TransactionForm({ onSuccess, onCancel }: TransactionFormProps) {
|
||||
try {
|
||||
// Get user ID from localStorage - with RLS disabled, this is just for reference
|
||||
const userId = localStorage.getItem('userId') || 'default';
|
||||
const userEmail = localStorage.getItem('userEmail') || '';
|
||||
const userName = localStorage.getItem('userName') || '';
|
||||
|
||||
const valorNumerico = parseFloat(data.valor.replace(',', '.'));
|
||||
|
||||
@ -80,6 +83,8 @@ export function TransactionForm({ onSuccess, onCancel }: TransactionFormProps) {
|
||||
? 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
|
||||
const { error } = await supabase
|
||||
.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}>
|
||||
Cancelar
|
||||
</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"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -52,7 +52,8 @@ const TransactionsTable = ({
|
||||
return (
|
||||
transaction.estabelecimento?.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 className={cn(
|
||||
"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>
|
||||
</TableRow>
|
||||
))
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { LogOut, User } from 'lucide-react';
|
||||
@ -17,14 +17,29 @@ const Header = () => {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
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 = () => {
|
||||
// Limpar informações de sessão
|
||||
localStorage.removeItem('autenticado');
|
||||
localStorage.removeItem('userId');
|
||||
localStorage.removeItem('userName');
|
||||
localStorage.removeItem('userEmail');
|
||||
|
||||
toast({
|
||||
title: "Logout realizado",
|
||||
|
||||
@ -156,6 +156,50 @@ export type Database = {
|
||||
}
|
||||
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": {
|
||||
Row: {
|
||||
categoria: string | null
|
||||
|
||||
@ -14,34 +14,46 @@ import {
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
const CategoriasPage = () => {
|
||||
const [categories, setCategories] = useState<CategorySummary[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [tipoFiltro, setTipoFiltro] = useState<string>('despesa');
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
async function loadCategories() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
console.log("Carregando dados de categorias...");
|
||||
const data = await getCategorySummary();
|
||||
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);
|
||||
}
|
||||
const loadCategories = async (tipo: string) => {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
|
||||
loadCategories();
|
||||
}, [toast]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadCategories(tipoFiltro);
|
||||
}, [tipoFiltro, toast]);
|
||||
|
||||
const handleTipoChange = (value: string) => {
|
||||
setTipoFiltro(value);
|
||||
};
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('pt-BR', {
|
||||
@ -56,7 +68,20 @@ const CategoriasPage = () => {
|
||||
<Layout>
|
||||
<div className="space-y-6">
|
||||
<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>
|
||||
|
||||
{isLoading ? (
|
||||
@ -77,7 +102,13 @@ const CategoriasPage = () => {
|
||||
<Card>
|
||||
<CardContent className="py-10 text-center">
|
||||
<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>
|
||||
</Card>
|
||||
) : (
|
||||
@ -108,7 +139,7 @@ const CategoriasPage = () => {
|
||||
} as React.CSSProperties}
|
||||
/>
|
||||
<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>
|
||||
</CardContent>
|
||||
@ -118,7 +149,7 @@ const CategoriasPage = () => {
|
||||
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Resumo de Categorias</CardTitle>
|
||||
<CardTitle>Resumo de Categorias {tipoFiltro !== 'all' ? (tipoFiltro === 'despesa' ? '(Despesas)' : '(Receitas)') : ''}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
|
||||
@ -8,6 +8,7 @@ import { useToast } from "@/components/ui/use-toast";
|
||||
import { getTransacoes } from '@/services/transacaoService';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { PlusCircle } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -20,6 +21,7 @@ const TransacoesPage = () => {
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [tipoForm, setTipoForm] = useState<'receita' | 'despesa'>('despesa');
|
||||
const { toast } = useToast();
|
||||
|
||||
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 (
|
||||
<Layout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Todas as Transações</h1>
|
||||
<Button className="flex items-center gap-2" onClick={() => setIsDialogOpen(true)}>
|
||||
<PlusCircle className="h-4 w-4" />
|
||||
Nova Transação
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2 bg-finance-green hover:bg-finance-green/90"
|
||||
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>
|
||||
|
||||
<TransactionsTable
|
||||
transactions={transactions}
|
||||
isLoading={isLoading}
|
||||
showPagination={true}
|
||||
/>
|
||||
{/* Resumo em Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<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}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nova Transação</DialogTitle>
|
||||
<DialogTitle>
|
||||
{tipoForm === 'receita' ? 'Nova Receita' : 'Nova Despesa'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Preencha os campos para registrar uma nova transação.
|
||||
Preencha os campos para registrar uma nova {tipoForm === 'receita' ? 'receita' : 'despesa'}.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<TransactionForm
|
||||
onSuccess={handleTransactionSuccess}
|
||||
onCancel={() => setIsDialogOpen(false)}
|
||||
defaultTipo={tipoForm}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -84,8 +84,8 @@ export async function getTransactionSummary() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCategorySummary() {
|
||||
console.log("Buscando resumo de categorias...");
|
||||
export async function getCategorySummary(tipoFiltro: string = 'all') {
|
||||
console.log(`Buscando resumo de categorias para tipo: ${tipoFiltro}`);
|
||||
|
||||
// Obter o ID do usuário atual do localStorage
|
||||
const userId = localStorage.getItem('userId') || 'default';
|
||||
@ -104,16 +104,19 @@ export async function getCategorySummary() {
|
||||
|
||||
console.log("Dados de categorias encontrados:", data);
|
||||
|
||||
// Filtrar usando JavaScript para pegar todas as despesas (case insensitive)
|
||||
const despesasData = data.filter((item: any) =>
|
||||
item.tipo?.toLowerCase() === 'despesa'
|
||||
);
|
||||
// Filtrar conforme o tipo solicitado (receitas, despesas ou ambos)
|
||||
let filteredData = data;
|
||||
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
|
||||
const categorias: Record<string, number> = {};
|
||||
despesasData.forEach((item: any) => {
|
||||
filteredData.forEach((item: any) => {
|
||||
if (item.categoria && item.valor) {
|
||||
const categoriaKey = item.categoria || 'Outros';
|
||||
if (!categorias[categoriaKey]) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user