Feat: Add group name input and transaction actions

- Added an input field in the GruposWhatsApp menu for users to name their WhatsApp groups, and store the name in the database.
- Added delete and edit buttons to the transactions table, with database integration.
This commit is contained in:
gpt-engineer-app[bot] 2025-05-20 20:13:08 +00:00
parent a12e1411ef
commit c5f3a3d161
6 changed files with 305 additions and 56 deletions

View File

@ -2,7 +2,7 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { format } from 'date-fns';
import { ChevronDown, Search, ChevronLeft, ChevronRight } from 'lucide-react';
import { ChevronDown, Search, ChevronLeft, ChevronRight, Edit, Trash2 } from 'lucide-react';
import { Transaction } from '@/types/financialTypes';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@ -21,22 +21,32 @@ import {
TableRow,
} from '@/components/ui/table';
import { cn } from '@/lib/utils';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
import { useToast } from '@/hooks/use-toast';
import { deleteTransacao } from '@/services/transacaoService';
interface TransactionsTableProps {
transactions: Transaction[];
isLoading?: boolean;
showPagination?: boolean;
onEdit?: (transaction: Transaction) => void;
onDelete?: () => void;
}
const TransactionsTable = ({
transactions,
isLoading = false,
showPagination = false
showPagination = false,
onEdit,
onDelete
}: TransactionsTableProps) => {
const [searchQuery, setSearchQuery] = useState('');
const [sortColumn, setSortColumn] = useState<string>('quando');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
const [currentPage, setCurrentPage] = useState(1);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [transactionToDelete, setTransactionToDelete] = useState<Transaction | null>(null);
const { toast } = useToast();
const itemsPerPage = showPagination ? 10 : 5;
const handleSort = (column: string) => {
@ -48,6 +58,47 @@ const TransactionsTable = ({
}
};
const handleEditTransaction = (transaction: Transaction) => {
if (onEdit) {
onEdit(transaction);
} else {
console.log('Editar transação:', transaction);
// Implementar edição futura se não houver callback
}
};
const handleDeleteClick = (transaction: Transaction) => {
setTransactionToDelete(transaction);
setDeleteConfirmOpen(true);
};
const handleConfirmDelete = async () => {
if (!transactionToDelete) return;
try {
await deleteTransacao(transactionToDelete.id);
toast({
title: "Transação excluída",
description: "A transação foi removida com sucesso",
});
// Callback para recarregar os dados
if (onDelete) {
onDelete();
}
} catch (error) {
console.error('Erro ao excluir transação:', error);
toast({
title: "Erro",
description: "Não foi possível excluir a transação",
variant: "destructive"
});
} finally {
setDeleteConfirmOpen(false);
setTransactionToDelete(null);
}
};
const filteredTransactions = transactions.filter((transaction) => {
const query = searchQuery.toLowerCase();
return (
@ -157,13 +208,14 @@ const TransactionsTable = ({
>
Valor {sortColumn === 'valor' && (sortDirection === 'asc' ? '↑' : '↓')}
</TableHead>
<TableHead className="w-[120px] text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
Array(5).fill(0).map((_, i) => (
<TableRow key={`skeleton-${i}`}>
{Array(5).fill(0).map((_, j) => (
{Array(6).fill(0).map((_, j) => (
<TableCell key={`cell-${i}-${j}`} className="p-2">
<div className="h-4 bg-muted rounded animate-pulse-gentle" />
</TableCell>
@ -172,7 +224,7 @@ const TransactionsTable = ({
))
) : paginatedTransactions.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
{searchQuery ? 'Nenhuma transação encontrada' : 'Não há transações disponíveis'}
</TableCell>
</TableRow>
@ -195,6 +247,26 @@ const TransactionsTable = ({
)}>
{formatCurrency(Math.abs(transaction.valor))}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-1">
<Button
variant="ghost"
size="icon"
onClick={() => handleEditTransaction(transaction)}
title="Editar"
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteClick(transaction)}
title="Excluir"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
@ -247,6 +319,23 @@ const TransactionsTable = ({
</Link>
</div>
)}
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
<AlertDialogDescription>
Tem certeza que deseja excluir esta transação? Esta ação não pode ser desfeita.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirmDelete} className="bg-red-600 hover:bg-red-700">
Excluir
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
};

View File

@ -5,11 +5,13 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Loader2, Plus, RefreshCw, MessageSquare } from 'lucide-react';
import { Loader2, Plus, RefreshCw, MessageSquare, Edit } from 'lucide-react';
import { cadastrarGrupoWhatsApp, listarGruposWhatsApp } from '@/services/gruposWhatsAppService';
import { WhatsAppGroup } from '@/types/financialTypes';
import { useToast } from '@/hooks/use-toast';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
const GruposWhatsApp = () => {
const { toast } = useToast();
@ -19,6 +21,7 @@ const GruposWhatsApp = () => {
const [userEmail, setUserEmail] = useState<string>('');
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [debugInfo, setDebugInfo] = useState<string | null>(null);
const [nomeGrupo, setNomeGrupo] = useState<string>('');
// Buscar os grupos do usuário ao carregar a página
const buscarGrupos = async () => {
@ -64,10 +67,19 @@ const GruposWhatsApp = () => {
return;
}
if (!nomeGrupo.trim()) {
toast({
title: 'Atenção',
description: 'Digite um nome para o grupo',
variant: 'destructive',
});
return;
}
setCadastrando(true);
try {
console.log("Iniciando processo de cadastro de grupo...");
const grupo = await cadastrarGrupoWhatsApp();
const grupo = await cadastrarGrupoWhatsApp(nomeGrupo.trim());
if (grupo) {
let successMessage = 'Grupo registrado com sucesso';
@ -87,6 +99,9 @@ const GruposWhatsApp = () => {
variant: variant,
});
// Resetar o campo de nome
setNomeGrupo('');
// Atualizar a lista de grupos
buscarGrupos();
} else {
@ -138,22 +153,6 @@ const GruposWhatsApp = () => {
<RefreshCw className={`h-4 w-4 mr-2 ${carregando ? 'animate-spin' : ''}`} />
{carregando ? 'Atualizando...' : 'Atualizar'}
</Button>
<Button
onClick={handleCadastrarGrupo}
disabled={cadastrando || !userEmail}
>
{cadastrando ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Cadastrando...
</>
) : (
<>
<Plus className="h-4 w-4 mr-2" />
Cadastrar Grupo
</>
)}
</Button>
</div>
</div>
@ -177,25 +176,58 @@ const GruposWhatsApp = () => {
<Card>
<CardHeader>
<CardTitle>Como cadastrar um grupo</CardTitle>
<CardTitle>Cadastrar novo grupo</CardTitle>
<CardDescription>
Siga os passos abaixo para vincular um grupo do WhatsApp à sua conta
Preencha as informações abaixo para cadastrar um novo grupo do WhatsApp
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<ol className="list-decimal list-inside space-y-2">
<li>Clique no botão "Cadastrar Grupo" acima</li>
<li>Adicione o número (61)99244-4275 ao grupo do WhatsApp que deseja automatizar</li>
<li>Envie uma mensagem neste grupo com o seguinte texto:</li>
</ol>
<div className="bg-muted p-3 rounded-md font-mono">
{userEmail}
<div className="space-y-2">
<Label htmlFor="nomeGrupo">Nome do grupo</Label>
<Input
id="nomeGrupo"
placeholder="Ex: Controle de Gastos da Família"
value={nomeGrupo}
onChange={(e) => setNomeGrupo(e.target.value)}
/>
<p className="text-sm text-muted-foreground">
Digite um nome descritivo para o grupo que você irá criar
</p>
</div>
<p className="text-sm text-muted-foreground">
Copie e cole o email exatamente como aparece acima
</p>
<Button
onClick={handleCadastrarGrupo}
disabled={cadastrando || !userEmail || !nomeGrupo.trim()}
className="w-full"
>
{cadastrando ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Cadastrando...
</>
) : (
<>
<Plus className="h-4 w-4 mr-2" />
Cadastrar Grupo
</>
)}
</Button>
<div className="mt-4 border-t pt-4">
<h3 className="font-medium mb-2">Após cadastrar:</h3>
<ol className="list-decimal list-inside space-y-2">
<li>Adicione o número (61)99244-4275 ao grupo do WhatsApp que deseja automatizar</li>
<li>Envie uma mensagem neste grupo com o seguinte texto:</li>
</ol>
<div className="bg-muted p-3 rounded-md font-mono mt-2">
{userEmail}
</div>
<p className="text-sm text-muted-foreground mt-2">
Copie e cole o email exatamente como aparece acima
</p>
</div>
</CardContent>
</Card>
@ -258,15 +290,6 @@ const GruposWhatsApp = () => {
<div className="text-center py-8">
<MessageSquare className="h-12 w-12 text-muted-foreground opacity-20 mx-auto mb-4" />
<p className="text-muted-foreground">Você ainda não cadastrou nenhum grupo</p>
<Button
variant="outline"
className="mt-4"
onClick={handleCadastrarGrupo}
disabled={cadastrando || !userEmail}
>
<Plus className="h-4 w-4 mr-2" />
Cadastrar seu primeiro grupo
</Button>
</div>
)}
</CardContent>

View File

@ -4,7 +4,7 @@ import Layout from '@/components/layout/Layout';
import TransactionsTable from '@/components/dashboard/TransactionsTable';
import { TransactionForm } from '@/components/dashboard/TransactionForm';
import { Transaction } from '@/types/financialTypes';
import { useToast } from "@/components/ui/use-toast";
import { useToast } from "@/hooks/use-toast";
import { getTransacoes } from '@/services/transacaoService';
import { Button } from '@/components/ui/button';
import { PlusCircle } from 'lucide-react';
@ -22,6 +22,8 @@ const TransacoesPage = () => {
const [isLoading, setIsLoading] = useState(true);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [tipoForm, setTipoForm] = useState<'receita' | 'despesa'>('despesa');
const [selectedTransaction, setSelectedTransaction] = useState<Transaction | null>(null);
const [isEditing, setIsEditing] = useState(false);
const { toast } = useToast();
const loadTransactions = async () => {
@ -49,13 +51,30 @@ const TransacoesPage = () => {
const handleTransactionSuccess = () => {
setIsDialogOpen(false);
setSelectedTransaction(null);
setIsEditing(false);
loadTransactions();
toast({
title: "Transação registrada",
description: "A nova transação foi adicionada com sucesso",
title: isEditing ? "Transação atualizada" : "Transação registrada",
description: isEditing
? "A transação foi atualizada com sucesso"
: "A nova transação foi adicionada com sucesso",
});
};
const handleEditTransaction = (transaction: Transaction) => {
setSelectedTransaction(transaction);
setTipoForm(transaction.tipo as 'receita' | 'despesa');
setIsEditing(true);
setIsDialogOpen(true);
};
const handleCloseDialog = () => {
setIsDialogOpen(false);
setSelectedTransaction(null);
setIsEditing(false);
};
// Separar transações em receitas e despesas
const receitas = transactions.filter(t => t.tipo === 'receita');
const despesas = transactions.filter(t => t.tipo === 'despesa');
@ -73,6 +92,8 @@ const TransacoesPage = () => {
const handleOpenDialog = (tipo: 'receita' | 'despesa') => {
setTipoForm(tipo);
setIsEditing(false);
setSelectedTransaction(null);
setIsDialogOpen(true);
};
@ -135,23 +156,33 @@ const TransacoesPage = () => {
transactions={transactions}
isLoading={isLoading}
showPagination={true}
onEdit={handleEditTransaction}
onDelete={loadTransactions}
/>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<Dialog open={isDialogOpen} onOpenChange={handleCloseDialog}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>
{tipoForm === 'receita' ? 'Nova Receita' : 'Nova Despesa'}
{isEditing
? `Editar ${tipoForm === 'receita' ? 'Receita' : 'Despesa'}`
: `Nova ${tipoForm === 'receita' ? 'Receita' : 'Despesa'}`
}
</DialogTitle>
<DialogDescription>
Preencha os campos para registrar uma nova {tipoForm === 'receita' ? 'receita' : 'despesa'}.
{isEditing
? 'Edite os campos para atualizar a transação.'
: `Preencha os campos para registrar uma nova ${tipoForm === 'receita' ? 'receita' : 'despesa'}.`
}
</DialogDescription>
</DialogHeader>
<TransactionForm
onSuccess={handleTransactionSuccess}
onCancel={() => setIsDialogOpen(false)}
onCancel={handleCloseDialog}
defaultTipo={tipoForm}
transaction={selectedTransaction}
isEditing={isEditing}
/>
</DialogContent>
</Dialog>

View File

@ -6,11 +6,12 @@ import { findOrCreateWhatsAppGroup, listWhatsAppGroups, updateWorkflowId } from
/**
* Main function to register a WhatsApp group and create its workflow
* Orchestrates the process using the specialized services
* @param nomeGrupo Nome definido pelo usuário para o grupo
*/
export async function cadastrarGrupoWhatsApp(): Promise<WhatsAppGroup | null> {
export async function cadastrarGrupoWhatsApp(nomeGrupo?: string): Promise<WhatsAppGroup | null> {
try {
// Find or create a WhatsApp group for the current user
const group = await findOrCreateWhatsAppGroup();
const group = await findOrCreateWhatsAppGroup(nomeGrupo);
// If no group was found or created, return null
if (!group) {

View File

@ -1,4 +1,3 @@
import { supabase } from "@/integrations/supabase/client";
import { Transaction, CategorySummary } from "@/types/financialTypes";
@ -292,3 +291,92 @@ export async function getMonthlyData() {
return [];
}
}
/**
* Delete uma transação específica
* @param id ID da transação a ser excluída
*/
export async function deleteTransacao(id: string): Promise<void> {
console.log(`Excluindo transação com ID: ${id}`);
try {
const { error } = await supabase
.from('transacoes')
.delete()
.eq('id', id);
if (error) {
console.error('Erro ao excluir transação:', error);
throw new Error('Não foi possível excluir a transação');
}
console.log('Transação excluída com sucesso');
} catch (error) {
console.error('Erro ao excluir transação:', error);
throw error;
}
}
/**
* Atualiza os dados de uma transação existente
* @param transaction A transação com os dados atualizados
* @returns A transação atualizada
*/
export async function updateTransacao(transaction: Transaction): Promise<Transaction> {
console.log(`Atualizando transação com ID: ${transaction.id}`, transaction);
// Obter o email do usuário do localStorage
const userEmail = localStorage.getItem('userEmail');
if (!userEmail) {
console.error('Email do usuário não encontrado no localStorage');
throw new Error('Usuário não autenticado');
}
// Normalizar o email (minúsculo e sem espaços)
const normalizedEmail = userEmail.trim().toLowerCase();
const transacaoData = {
login: normalizedEmail,
valor: transaction.tipo === 'receita' ? Math.abs(transaction.valor) : Math.abs(transaction.valor) * -1,
quando: transaction.quando,
detalhes: transaction.detalhes,
estabelecimento: transaction.estabelecimento,
tipo: transaction.tipo,
categoria: transaction.categoria
};
try {
const { data, error } = await supabase
.from('transacoes')
.update(transacaoData)
.eq('id', transaction.id)
.select('*')
.single();
if (error) {
console.error('Erro ao atualizar transação:', error);
throw new Error('Não foi possível atualizar a transação');
}
console.log('Transação atualizada com sucesso:', data);
// Transformar para o formato esperado
return {
id: data.id.toString(),
user: data.user || '',
login: data.login || normalizedEmail,
created_at: data.created_at,
valor: data.tipo === 'receita' ? Math.abs(data.valor || 0) : -Math.abs(data.valor || 0),
quando: data.quando || new Date().toISOString(),
detalhes: data.detalhes || '',
estabelecimento: data.estabelecimento || '',
tipo: data.tipo?.toLowerCase() || 'despesa',
categoria: data.categoria || 'Outros',
grupo_id: data.grupo_id || null
};
} catch (error) {
console.error('Erro ao atualizar transação:', error);
throw error;
}
}

View File

@ -29,9 +29,10 @@ export async function updateWorkflowId(groupId: number, workflowId: string): Pro
/**
* Gets an existing or creates a new WhatsApp group for a user
* @param nomeGrupo Optional name for the group
* @returns The created or existing WhatsApp group
*/
export async function findOrCreateWhatsAppGroup(): Promise<WhatsAppGroup | null> {
export async function findOrCreateWhatsAppGroup(nomeGrupo?: string): Promise<WhatsAppGroup | null> {
try {
// Obter o email do usuário do localStorage
const userEmail = localStorage.getItem('userEmail');
@ -58,6 +59,21 @@ export async function findOrCreateWhatsAppGroup(): Promise<WhatsAppGroup | null>
console.log('Grupo pendente já existe para este usuário:', existingGroups[0]);
groupToUse = existingGroups[0];
// Atualizar nome do grupo se foi fornecido
if (nomeGrupo && nomeGrupo.trim() !== '' && nomeGrupo !== groupToUse.nome_grupo) {
console.log('Atualizando nome do grupo existente para:', nomeGrupo);
const { error } = await supabase
.from('grupos_whatsapp')
.update({ nome_grupo: nomeGrupo.trim() })
.eq('id', groupToUse.id);
if (error) {
console.error('Erro ao atualizar nome do grupo:', error);
} else {
groupToUse.nome_grupo = nomeGrupo.trim();
}
}
// Se já existe um workflow_id, não precisamos criar novamente
if (groupToUse.workflow_id) {
console.log('Workflow já existe para este grupo:', groupToUse.workflow_id);
@ -72,7 +88,8 @@ export async function findOrCreateWhatsAppGroup(): Promise<WhatsAppGroup | null>
user_id: localStorage.getItem('userId') || '',
remote_jid: '',
login: normalizedEmail,
status: 'pendente'
status: 'pendente',
nome_grupo: nomeGrupo?.trim() || null
})
.select();