Apply SQL schema changes

Apply the SQL schema changes to create tables for recurring payment reminders, including tables for recurring accounts and sent reminders, along with necessary indexes and RLS policies.
This commit is contained in:
gpt-engineer-app[bot] 2025-06-23 21:39:47 +00:00
parent f01ed29305
commit ee2273383b
8 changed files with 961 additions and 236 deletions

View File

@ -1,101 +1,92 @@
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { Suspense, lazy } from 'react';
import { Toaster } from "@/components/ui/sonner";
import Auth from './pages/Auth';
import Landing from './pages/Landing';
import EmailConfirmation from './pages/EmailConfirmation';
import CompleteProfile from './pages/CompleteProfile';
import ProtectedRoute from './components/auth/ProtectedRoute';
import { authStore } from './stores/authStore';
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { useEffect } from "react";
import { supabase } from "@/integrations/supabase/client";
import { useAuthStore } from "@/stores/authStore";
import ProtectedRoute from "@/components/auth/ProtectedRoute";
import Layout from "@/components/layout/Layout";
// Lazy-loaded components
const Dashboard = lazy(() => import('./pages/Index'));
const Transacoes = lazy(() => import('./pages/Transacoes'));
const Categorias = lazy(() => import('./pages/Categorias'));
const Metas = lazy(() => import('./pages/Metas'));
const Calendario = lazy(() => import('./pages/Calendario'));
const Assinatura = lazy(() => import('./pages/Assinatura'));
const WhatsApp = lazy(() => import('./pages/WhatsApp'));
const GruposWhatsApp = lazy(() => import('./pages/GruposWhatsApp'));
const NotFound = lazy(() => import('./pages/NotFound'));
const CartoesCredito = lazy(() => import('./pages/CartoesCredito'));
const Configuracoes = lazy(() => import('./pages/Configuracoes'));
const AdminFAQ = lazy(() => import('./pages/AdminFAQ'));
// Pages
import Index from "./pages/Index";
import Auth from "./pages/Auth";
import Landing from "./pages/Landing";
import Transacoes from "./pages/Transacoes";
import CartoesCredito from "./pages/CartoesCredito";
import Configuracoes from "./pages/Configuracoes";
import Metas from "./pages/Metas";
import Calendario from "./pages/Calendario";
import WhatsApp from "./pages/WhatsApp";
import GruposWhatsApp from "./pages/GruposWhatsApp";
import EmailConfirmation from "./pages/EmailConfirmation";
import CompleteProfile from "./pages/CompleteProfile";
import NotFound from "./pages/NotFound";
import Categorias from "./pages/Categorias";
import AdminFAQ from "./pages/AdminFAQ";
import Assinatura from "./pages/Assinatura";
import AvisosContas from "./pages/AvisosContas";
const queryClient = new QueryClient();
function App() {
const isLoggedIn = authStore((state) => state.isLoggedIn);
const { setUser, setSession } = useAuthStore();
useEffect(() => {
// Verificar sessão inicial
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
setUser(session?.user ?? null);
});
// Escutar mudanças de autenticação
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
setUser(session?.user ?? null);
});
return () => subscription.unsubscribe();
}, [setUser, setSession]);
return (
<Router>
<Toaster />
<Routes>
{/* Landing page - só mostra se não estiver logado */}
<Route
path="/"
element={isLoggedIn ? <Navigate to="/dashboard" replace /> : <Landing />}
/>
{/* Auth route - redireciona para dashboard se já estiver logado */}
<Route
path="/auth"
element={isLoggedIn ? <Navigate to="/dashboard" replace /> : <Auth />}
/>
{/* Email confirmation route */}
<Route
path="/email-confirmation"
element={<EmailConfirmation />}
/>
{/* Complete profile route - deve ser acessível para usuários logados */}
<Route
path="/complete-profile"
element={<CompleteProfile />}
/>
{/* Protected routes */}
<Route path="/dashboard" element={
<ProtectedRoute>
<Suspense fallback={<div className="flex items-center justify-center min-h-screen">Carregando...</div>}>
<Dashboard />
</Suspense>
</ProtectedRoute>
} />
<Route path="/transacoes" element={
<ProtectedRoute>
<Suspense fallback={<div className="flex items-center justify-center min-h-screen">Carregando...</div>}>
<Transacoes />
</Suspense>
</ProtectedRoute>
} />
<Route path="/cartoes" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><CartoesCredito /></Suspense></ProtectedRoute>} />
<Route path="/categorias" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><Categorias /></Suspense></ProtectedRoute>} />
<Route path="/metas" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><Metas /></Suspense></ProtectedRoute>} />
<Route path="/calendario" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><Calendario /></Suspense></ProtectedRoute>} />
<Route path="/assinatura" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><Assinatura /></Suspense></ProtectedRoute>} />
<Route path="/whatsapp" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><WhatsApp /></Suspense></ProtectedRoute>} />
<Route path="/grupos-whatsapp" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><GruposWhatsApp /></Suspense></ProtectedRoute>} />
<Route path="/admin-faq" element={<ProtectedRoute><Suspense fallback={<div>Carregando...</div>}><AdminFAQ /></Suspense></ProtectedRoute>} />
<Route path="/configuracoes" element={
<ProtectedRoute>
<Suspense fallback={<div className="flex items-center justify-center min-h-screen">Carregando...</div>}>
<Configuracoes />
</Suspense>
</ProtectedRoute>
} />
{/* Not found route */}
<Route path="*" element={
<Suspense fallback={<div className="flex items-center justify-center min-h-screen">Carregando...</div>}>
<NotFound />
</Suspense>
} />
</Routes>
</Router>
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<Toaster />
<BrowserRouter>
<Routes>
{/* Rotas públicas */}
<Route path="/" element={<Landing />} />
<Route path="/auth" element={<Auth />} />
<Route path="/email-confirmation" element={<EmailConfirmation />} />
{/* Rotas protegidas */}
<Route element={<ProtectedRoute />}>
<Route element={<Layout />}>
<Route path="/dashboard" element={<Index />} />
<Route path="/transacoes" element={<Transacoes />} />
<Route path="/cartoes" element={<CartoesCredito />} />
<Route path="/configuracoes" element={<Configuracoes />} />
<Route path="/metas" element={<Metas />} />
<Route path="/calendario" element={<Calendario />} />
<Route path="/whatsapp" element={<WhatsApp />} />
<Route path="/grupos-whatsapp" element={<GruposWhatsApp />} />
<Route path="/complete-profile" element={<CompleteProfile />} />
<Route path="/categorias" element={<Categorias />} />
<Route path="/admin/faq" element={<AdminFAQ />} />
<Route path="/assinatura" element={<Assinatura />} />
<Route path="/avisos-contas" element={<AvisosContas />} />
</Route>
</Route>
{/* Rota 404 */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</TooltipProvider>
</QueryClientProvider>
);
}

View File

@ -0,0 +1,194 @@
import React from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { X, Save } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { useAuthStore } from '@/stores/authStore';
interface ContaRecorrenteFormProps {
onClose: () => void;
onSuccess: () => void;
}
interface FormData {
nome_conta: string;
descricao: string;
valor: number;
dia_vencimento: number;
hora_aviso: string;
dias_antecedencia: number;
}
const ContaRecorrenteForm = ({ onClose, onSuccess }: ContaRecorrenteFormProps) => {
const { user } = useAuthStore();
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormData>({
defaultValues: {
hora_aviso: '09:00',
dias_antecedencia: 1
}
});
const onSubmit = async (data: FormData) => {
if (!user?.id) {
toast.error('Usuário não encontrado. Faça login novamente.');
return;
}
try {
const { error } = await supabase
.from('contas_recorrentes')
.insert([{
...data,
user_id: user.id,
valor: data.valor || null
}]);
if (error) throw error;
toast.success('Conta recorrente cadastrada com sucesso!');
onSuccess();
} catch (error) {
console.error('Erro ao cadastrar conta:', error);
toast.error('Erro ao cadastrar conta. Tente novamente.');
}
};
return (
<Card className="w-full">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Nova Conta Recorrente</CardTitle>
<CardDescription>
Configure uma conta que será cobrada mensalmente no mesmo dia
</CardDescription>
</div>
<Button variant="ghost" size="sm" onClick={onClose}>
<X className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="nome_conta">Nome da Conta *</Label>
<Input
id="nome_conta"
{...register('nome_conta', { required: 'Nome da conta é obrigatório' })}
placeholder="Ex: Energia elétrica, Internet, Financiamento"
/>
{errors.nome_conta && (
<p className="text-sm text-red-600">{errors.nome_conta.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="valor">Valor (opcional)</Label>
<Input
id="valor"
type="number"
step="0.01"
{...register('valor', {
valueAsNumber: true,
min: { value: 0, message: 'Valor deve ser positivo' }
})}
placeholder="0,00"
/>
{errors.valor && (
<p className="text-sm text-red-600">{errors.valor.message}</p>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="descricao">Descrição (opcional)</Label>
<Textarea
id="descricao"
{...register('descricao')}
placeholder="Adicione detalhes sobre esta conta..."
rows={3}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="dia_vencimento">Dia do Vencimento *</Label>
<Input
id="dia_vencimento"
type="number"
{...register('dia_vencimento', {
required: 'Dia do vencimento é obrigatório',
valueAsNumber: true,
min: { value: 1, message: 'Dia deve ser entre 1 e 31' },
max: { value: 31, message: 'Dia deve ser entre 1 e 31' }
})}
placeholder="15"
/>
{errors.dia_vencimento && (
<p className="text-sm text-red-600">{errors.dia_vencimento.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="hora_aviso">Horário do Aviso *</Label>
<Input
id="hora_aviso"
type="time"
{...register('hora_aviso', { required: 'Horário é obrigatório' })}
/>
{errors.hora_aviso && (
<p className="text-sm text-red-600">{errors.hora_aviso.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="dias_antecedencia">Dias de Antecedência *</Label>
<Input
id="dias_antecedencia"
type="number"
{...register('dias_antecedencia', {
required: 'Dias de antecedência é obrigatório',
valueAsNumber: true,
min: { value: 1, message: 'Mínimo 1 dia' },
max: { value: 30, message: 'Máximo 30 dias' }
})}
placeholder="1"
/>
{errors.dias_antecedencia && (
<p className="text-sm text-red-600">{errors.dias_antecedencia.message}</p>
)}
</div>
</div>
<div className="bg-blue-50 p-4 rounded-lg">
<h4 className="font-medium text-blue-900 mb-2">Como funcionam os avisos:</h4>
<ul className="text-sm text-blue-800 space-y-1">
<li> Se escolher 1 dia de antecedência: receberá 1 aviso no dia anterior</li>
<li> Se escolher 3 dias: receberá avisos nos 3 dias anteriores ao vencimento</li>
<li> Os avisos são enviados no horário configurado via WhatsApp</li>
</ul>
</div>
<div className="flex gap-2 pt-4">
<Button type="submit" disabled={isSubmitting} className="bg-blue-600 hover:bg-blue-700">
<Save className="h-4 w-4 mr-2" />
{isSubmitting ? 'Salvando...' : 'Salvar Conta'}
</Button>
<Button type="button" variant="outline" onClick={onClose}>
Cancelar
</Button>
</div>
</form>
</CardContent>
</Card>
);
};
export default ContaRecorrenteForm;

View File

@ -0,0 +1,197 @@
import React, { useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { supabase } from '@/integrations/supabase/client';
import { useAuthStore } from '@/stores/authStore';
import { Edit, Trash2, Calendar, Clock, DollarSign, Bell } from 'lucide-react';
import { toast } from 'sonner';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
interface ContaRecorrente {
id: string;
nome_conta: string;
descricao: string | null;
valor: number | null;
dia_vencimento: number;
hora_aviso: string;
dias_antecedencia: number;
ativo: boolean;
created_at: string;
}
const ContasRecorrentesList = () => {
const { user } = useAuthStore();
const queryClient = useQueryClient();
const [deletingId, setDeletingId] = useState<string | null>(null);
const { data: contas, isLoading } = useQuery({
queryKey: ['contas-recorrentes', user?.id],
queryFn: async () => {
if (!user?.id) return [];
const { data, error } = await supabase
.from('contas_recorrentes')
.select('*')
.eq('user_id', user.id)
.order('created_at', { ascending: false });
if (error) throw error;
return data as ContaRecorrente[];
},
enabled: !!user?.id
});
const handleDelete = async (id: string) => {
setDeletingId(id);
try {
const { error } = await supabase
.from('contas_recorrentes')
.delete()
.eq('id', id);
if (error) throw error;
toast.success('Conta removida com sucesso!');
queryClient.invalidateQueries({ queryKey: ['contas-recorrentes'] });
} catch (error) {
console.error('Erro ao remover conta:', error);
toast.error('Erro ao remover conta. Tente novamente.');
} finally {
setDeletingId(null);
}
};
const toggleAtivo = async (id: string, ativo: boolean) => {
try {
const { error } = await supabase
.from('contas_recorrentes')
.update({ ativo: !ativo })
.eq('id', id);
if (error) throw error;
toast.success(ativo ? 'Conta desativada' : 'Conta ativada');
queryClient.invalidateQueries({ queryKey: ['contas-recorrentes'] });
} catch (error) {
console.error('Erro ao alterar status:', error);
toast.error('Erro ao alterar status da conta.');
}
};
const formatCurrency = (value: number | null) => {
if (value === null) return 'Não informado';
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(value);
};
if (isLoading) {
return (
<Card>
<CardContent className="p-6">
<div className="text-center">Carregando contas...</div>
</CardContent>
</Card>
);
}
if (!contas || contas.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle>Suas Contas Recorrentes</CardTitle>
<CardDescription>Você ainda não cadastrou nenhuma conta recorrente</CardDescription>
</CardHeader>
<CardContent>
<div className="text-center py-8">
<Bell className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">Clique em "Nova Conta" para começar</p>
</div>
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Suas Contas Recorrentes</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{contas.map((conta) => (
<Card key={conta.id} className={`${conta.ativo ? '' : 'opacity-60'}`}>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="text-lg">{conta.nome_conta}</CardTitle>
{conta.descricao && (
<CardDescription className="mt-1">{conta.descricao}</CardDescription>
)}
</div>
<Badge variant={conta.ativo ? 'default' : 'secondary'}>
{conta.ativo ? 'Ativo' : 'Inativo'}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<DollarSign className="h-4 w-4 text-green-600" />
<span className="font-medium">Valor:</span>
<span>{formatCurrency(conta.valor)}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Calendar className="h-4 w-4 text-blue-600" />
<span className="font-medium">Vencimento:</span>
<span>Todo dia {conta.dia_vencimento}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Clock className="h-4 w-4 text-purple-600" />
<span className="font-medium">Horário:</span>
<span>{conta.hora_aviso}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Bell className="h-4 w-4 text-orange-600" />
<span className="font-medium">Antecedência:</span>
<span>{conta.dias_antecedencia} dia(s)</span>
</div>
</div>
<div className="flex gap-2 pt-2 border-t">
<Button
variant="outline"
size="sm"
onClick={() => toggleAtivo(conta.id, conta.ativo)}
className="flex-1"
>
{conta.ativo ? 'Desativar' : 'Ativar'}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDelete(conta.id)}
disabled={deletingId === conta.id}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
};
export default ContasRecorrentesList;

View File

@ -1,158 +1,77 @@
import { Link, NavLink, useLocation } from 'react-router-dom';
import { useIsMobile } from '@/hooks/use-mobile';
import {
Home,
ListFilter,
Receipt,
Target,
Calendar,
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { cn } from '@/lib/utils';
import {
Home,
CreditCard,
MessageSquareText,
Users,
Menu,
X,
Settings,
Target,
Calendar,
MessageSquare,
Users,
Receipt,
BarChart3,
Bell,
HelpCircle,
Crown
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
interface SidebarProps {
isOpen: boolean;
onClose: () => void;
}
const navigation = [
{ name: 'Dashboard', href: '/dashboard', icon: Home },
{ name: 'Transações', href: '/transacoes', icon: Receipt },
{ name: 'Cartões de Crédito', href: '/cartoes', icon: CreditCard },
{ name: 'Metas', href: '/metas', icon: Target },
{ name: 'Calendário', href: '/calendario', icon: Calendar },
{ name: 'Avisos de Contas', href: '/avisos-contas', icon: Bell },
{ name: 'WhatsApp', href: '/whatsapp', icon: MessageSquare },
{ name: 'Grupos WhatsApp', href: '/grupos-whatsapp', icon: Users },
{ name: 'Categorias', href: '/categorias', icon: BarChart3 },
{ name: 'Assinatura', href: '/assinatura', icon: Crown },
{ name: 'Configurações', href: '/configuracoes', icon: Settings },
];
const getNavLinkClass = (isActive: boolean) => {
return cn(
"group flex items-center rounded-md px-3 py-2 text-sm font-medium transition-all duration-200 hover:bg-gradient-to-r hover:from-blue-50 hover:to-indigo-50 hover:text-blue-700 hover:scale-105 hover:shadow-sm",
isActive ? "bg-gradient-to-r from-blue-100 to-indigo-100 text-blue-700 shadow-sm" : "text-muted-foreground"
);
};
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
const isMobile = useIsMobile();
const Sidebar = () => {
const location = useLocation();
return (
<aside className={cn(
"flex h-full w-64 flex-col border-r bg-background shadow-sm",
isMobile ? 'fixed left-0 top-0 z-50 w-full transition-all duration-300' : 'relative'
)}>
<div className="flex items-center justify-between px-4 py-2">
<Link to="/" className="flex items-center space-x-2 font-semibold text-blue-700 hover:text-blue-800 transition-colors">
<span className="text-lg">Finance Home</span>
</Link>
{isMobile && (
<Button variant="ghost" size="icon" onClick={onClose}>
<X className="h-5 w-5" />
</Button>
)}
</div>
<div className="scrollbar-thin h-full space-y-4 py-4 overflow-y-auto">
<div className="px-3 py-2">
<div className="space-y-1">
<NavLink
to="/"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<Home className="mr-2 h-4 w-4 transition-transform group-hover:scale-110" />
<span>Dashboard</span>
</NavLink>
<NavLink
to="/transacoes"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<Receipt className="mr-2 h-4 w-4 transition-transform group-hover:scale-110" />
<span>Transações</span>
</NavLink>
<NavLink
to="/cartoes"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<CreditCard className="mr-2 h-4 w-4 transition-transform group-hover:scale-110" />
<span>Cartões de Crédito</span>
</NavLink>
<NavLink
to="/categorias"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<ListFilter className="mr-2 h-4 w-4 transition-transform group-hover:scale-110" />
<span>Categorias</span>
</NavLink>
<NavLink
to="/metas"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<Target className="mr-2 h-4 w-4 transition-transform group-hover:scale-110" />
<span>Metas</span>
</NavLink>
<NavLink
to="/calendario"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<Calendar className="mr-2 h-4 w-4 transition-transform group-hover:scale-110" />
<span>Calendário</span>
</NavLink>
<NavLink
to="/assinatura"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<Crown className="mr-2 h-4 w-4 transition-transform group-hover:scale-110" />
<span>Assinatura</span>
</NavLink>
</div>
</div>
<div className="px-3 py-2">
<h2 className="mb-2 px-3 text-sm font-semibold tracking-tight text-gray-600">
WhatsApp
</h2>
<div className="space-y-1">
<NavLink
to="/whatsapp"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
data-tour="whatsapp-menu"
>
<MessageSquareText className="mr-2 h-4 w-4 transition-transform group-hover:scale-110" />
<span>Conectar WhatsApp</span>
</NavLink>
<NavLink
to="/grupos-whatsapp"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
data-tour="grupos-menu"
>
<Users className="mr-2 h-4 w-4 transition-transform group-hover:scale-110" />
<span>Grupos</span>
</NavLink>
</div>
</div>
<div className="px-3 py-2">
<h2 className="mb-2 px-3 text-sm font-semibold tracking-tight text-gray-600">
Configurações
</h2>
<div className="space-y-1">
<NavLink
to="/configuracoes"
className={({ isActive }) => getNavLinkClass(isActive)}
onClick={isMobile ? onClose : undefined}
>
<Settings className="mr-2 h-4 w-4 transition-transform group-hover:scale-110" />
<span>Configurações</span>
</NavLink>
<div className="hidden md:flex md:w-64 md:flex-col md:fixed md:inset-y-0">
<div className="flex-1 flex flex-col min-h-0 bg-gray-800">
<div className="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
<div className="flex items-center flex-shrink-0 px-4">
<img
src="/lovable-uploads/7149adf3-440a-491e-83c2-d964a3348cc9.png"
alt="Finance Home Logo"
className="h-8 w-8"
/>
<span className="ml-2 text-xl font-bold text-white">Finance Home</span>
</div>
<nav className="mt-5 flex-1 px-2 space-y-1">
{navigation.map((item) => {
const Icon = item.icon;
const isActive = location.pathname === item.href;
return (
<Link
key={item.name}
to={item.href}
className={cn(
'group flex items-center px-2 py-2 text-sm font-medium rounded-md transition-colors',
isActive
? 'bg-gray-900 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
)}
>
<Icon className="mr-3 h-5 w-5 flex-shrink-0" />
{item.name}
</Link>
);
})}
</nav>
</div>
</div>
</aside>
</div>
);
}
};
export default Sidebar;

View File

@ -69,6 +69,47 @@ export type Database = {
}
Relationships: []
}
avisos_enviados: {
Row: {
conta_id: string
created_at: string
dados_webhook: Json | null
data_aviso: string
hora_aviso: string
id: string
status_envio: string
tentativas: number
}
Insert: {
conta_id: string
created_at?: string
dados_webhook?: Json | null
data_aviso: string
hora_aviso: string
id?: string
status_envio?: string
tentativas?: number
}
Update: {
conta_id?: string
created_at?: string
dados_webhook?: Json | null
data_aviso?: string
hora_aviso?: string
id?: string
status_envio?: string
tentativas?: number
}
Relationships: [
{
foreignKeyName: "avisos_enviados_conta_id_fkey"
columns: ["conta_id"]
isOneToOne: false
referencedRelation: "contas_recorrentes"
referencedColumns: ["id"]
},
]
}
Bloqueio_Rosana_Seven_zap: {
Row: {
acao: string | null
@ -159,6 +200,48 @@ export type Database = {
}
Relationships: []
}
contas_recorrentes: {
Row: {
ativo: boolean
created_at: string
descricao: string | null
dia_vencimento: number
dias_antecedencia: number
hora_aviso: string
id: string
nome_conta: string
updated_at: string
user_id: string
valor: number | null
}
Insert: {
ativo?: boolean
created_at?: string
descricao?: string | null
dia_vencimento: number
dias_antecedencia?: number
hora_aviso?: string
id?: string
nome_conta: string
updated_at?: string
user_id: string
valor?: number | null
}
Update: {
ativo?: boolean
created_at?: string
descricao?: string | null
dia_vencimento?: number
dias_antecedencia?: number
hora_aviso?: string
id?: string
nome_conta?: string
updated_at?: string
user_id?: string
valor?: number | null
}
Relationships: []
}
contatos: {
Row: {
anexo_url: string | null

View File

@ -0,0 +1,86 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Plus, Bell, Calendar, Clock } from 'lucide-react';
import ContaRecorrenteForm from '@/components/avisos/ContaRecorrenteForm';
import ContasRecorrentesList from '@/components/avisos/ContasRecorrentesList';
const AvisosContas = () => {
const [showForm, setShowForm] = useState(false);
return (
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Avisos de Contas a Pagar</h1>
<p className="text-gray-600 mt-2">Gerencie lembretes automáticos para suas contas recorrentes</p>
</div>
<Button
onClick={() => setShowForm(true)}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
<Plus className="h-4 w-4 mr-2" />
Nova Conta
</Button>
</div>
{/* Cards informativos */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Como funciona</CardTitle>
<Bell className="h-4 w-4 text-blue-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">Automático</div>
<p className="text-xs text-muted-foreground">
Receba lembretes automáticos via WhatsApp
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Antecedência</CardTitle>
<Calendar className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">1-30 dias</div>
<p className="text-xs text-muted-foreground">
Configure quantos dias antes ser avisado
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Horário</CardTitle>
<Clock className="h-4 w-4 text-purple-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-purple-600">Personalizado</div>
<p className="text-xs text-muted-foreground">
Escolha o horário ideal para receber avisos
</p>
</CardContent>
</Card>
</div>
{/* Formulário de nova conta */}
{showForm && (
<ContaRecorrenteForm
onClose={() => setShowForm(false)}
onSuccess={() => {
setShowForm(false);
}}
/>
)}
{/* Lista de contas */}
<ContasRecorrentesList />
</div>
);
};
export default AvisosContas;

View File

@ -0,0 +1,188 @@
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
interface ContaRecorrente {
id: string;
user_id: string;
nome_conta: string;
descricao: string | null;
valor: number | null;
dia_vencimento: number;
hora_aviso: string;
dias_antecedencia: number;
ativo: boolean;
}
interface AvisoData {
conta_id: string;
user_id: string;
nome_conta: string;
descricao: string | null;
valor: number | null;
dia_vencimento: number;
dias_restantes: number;
data_vencimento: string;
webhook_url: string;
}
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders })
}
try {
const supabaseUrl = Deno.env.get('SUPABASE_URL')!
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
const supabase = createClient(supabaseUrl, supabaseKey)
const agora = new Date()
const hoje = new Date(agora.getFullYear(), agora.getMonth(), agora.getDate())
const horaAtual = agora.toTimeString().slice(0, 5) // HH:MM
console.log(`Executando verificação de avisos às ${horaAtual}`)
// Buscar todas as contas ativas
const { data: contas, error: contasError } = await supabase
.from('contas_recorrentes')
.select('*')
.eq('ativo', true)
if (contasError) {
console.error('Erro ao buscar contas:', contasError)
throw contasError
}
console.log(`Encontradas ${contas?.length || 0} contas ativas`)
let avisosEnviados = 0
for (const conta of contas || []) {
// Calcular próxima data de vencimento
const diaVencimento = conta.dia_vencimento
let proximoVencimento = new Date(hoje.getFullYear(), hoje.getMonth(), diaVencimento)
// Se o dia já passou neste mês, usar o próximo mês
if (proximoVencimento <= hoje) {
proximoVencimento = new Date(hoje.getFullYear(), hoje.getMonth() + 1, diaVencimento)
}
// Calcular dias restantes
const diffTime = proximoVencimento.getTime() - hoje.getTime()
const diasRestantes = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
// Verificar se deve enviar aviso
const deveEnviarAviso = (
diasRestantes <= conta.dias_antecedencia &&
diasRestantes >= 1 &&
horaAtual === conta.hora_aviso
)
if (deveEnviarAviso) {
console.log(`Enviando aviso para conta: ${conta.nome_conta}, dias restantes: ${diasRestantes}`)
// Verificar se já foi enviado aviso hoje
const dataAviso = hoje.toISOString().split('T')[0]
const { data: avisoExistente } = await supabase
.from('avisos_enviados')
.select('id')
.eq('conta_id', conta.id)
.eq('data_aviso', dataAviso)
.single()
if (avisoExistente) {
console.log(`Aviso já enviado hoje para conta: ${conta.nome_conta}`)
continue
}
// Preparar dados para o webhook
const avisoData: AvisoData = {
conta_id: conta.id,
user_id: conta.user_id,
nome_conta: conta.nome_conta,
descricao: conta.descricao,
valor: conta.valor,
dia_vencimento: conta.dia_vencimento,
dias_restantes: diasRestantes,
data_vencimento: proximoVencimento.toISOString().split('T')[0],
webhook_url: 'https://webhookn8n.innova1001.com.br/webhook/avisosfinancehome'
}
try {
// Enviar webhook
const webhookResponse = await fetch('https://webhookn8n.innova1001.com.br/webhook/avisosfinancehome', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(avisoData),
})
const statusEnvio = webhookResponse.ok ? 'enviado' : 'erro'
// Registrar envio
await supabase
.from('avisos_enviados')
.insert({
conta_id: conta.id,
data_aviso: dataAviso,
hora_aviso: horaAtual,
dados_webhook: avisoData,
status_envio: statusEnvio,
tentativas: 1
})
if (webhookResponse.ok) {
avisosEnviados++
console.log(`✅ Aviso enviado com sucesso para: ${conta.nome_conta}`)
} else {
console.error(`❌ Erro ao enviar webhook para: ${conta.nome_conta}`)
}
} catch (error) {
console.error(`Erro ao processar conta ${conta.nome_conta}:`, error)
// Registrar erro
await supabase
.from('avisos_enviados')
.insert({
conta_id: conta.id,
data_aviso: dataAviso,
hora_aviso: horaAtual,
dados_webhook: avisoData,
status_envio: 'erro',
tentativas: 1
})
}
}
}
console.log(`Processamento concluído. ${avisosEnviados} avisos enviados.`)
return new Response(
JSON.stringify({
success: true,
avisosEnviados,
timestamp: agora.toISOString()
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
)
} catch (error) {
console.error('Erro na função de avisos:', error)
return new Response(
JSON.stringify({ error: error.message }),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
)
}
})

View File

@ -0,0 +1,67 @@
-- Criar tabela para armazenar as contas recorrentes
CREATE TABLE public.contas_recorrentes (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
nome_conta TEXT NOT NULL,
descricao TEXT,
valor DECIMAL(10,2),
dia_vencimento INTEGER NOT NULL CHECK (dia_vencimento >= 1 AND dia_vencimento <= 31),
hora_aviso TIME NOT NULL DEFAULT '09:00:00',
dias_antecedencia INTEGER NOT NULL DEFAULT 1 CHECK (dias_antecedencia >= 1 AND dias_antecedencia <= 30),
ativo BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Criar tabela para log dos avisos enviados
CREATE TABLE public.avisos_enviados (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
conta_id UUID NOT NULL REFERENCES public.contas_recorrentes(id) ON DELETE CASCADE,
data_aviso DATE NOT NULL,
hora_aviso TIME NOT NULL,
dados_webhook JSONB,
status_envio TEXT NOT NULL DEFAULT 'pendente',
tentativas INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
-- Adicionar RLS (Row Level Security) para segurança
ALTER TABLE public.contas_recorrentes ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.avisos_enviados ENABLE ROW LEVEL SECURITY;
-- Políticas RLS para contas_recorrentes
CREATE POLICY "Users can view their own contas"
ON public.contas_recorrentes
FOR SELECT
USING (user_id::text = COALESCE(current_setting('request.jwt.claims', true)::json->>'sub', ''));
CREATE POLICY "Users can create their own contas"
ON public.contas_recorrentes
FOR INSERT
WITH CHECK (user_id::text = COALESCE(current_setting('request.jwt.claims', true)::json->>'sub', ''));
CREATE POLICY "Users can update their own contas"
ON public.contas_recorrentes
FOR UPDATE
USING (user_id::text = COALESCE(current_setting('request.jwt.claims', true)::json->>'sub', ''));
CREATE POLICY "Users can delete their own contas"
ON public.contas_recorrentes
FOR DELETE
USING (user_id::text = COALESCE(current_setting('request.jwt.claims', true)::json->>'sub', ''));
-- Políticas RLS para avisos_enviados
CREATE POLICY "Users can view avisos of their contas"
ON public.avisos_enviados
FOR SELECT
USING (conta_id IN (
SELECT id FROM public.contas_recorrentes
WHERE user_id::text = COALESCE(current_setting('request.jwt.claims', true)::json->>'sub', '')
));
-- Índices para melhor performance
CREATE INDEX idx_contas_recorrentes_user_id ON public.contas_recorrentes(user_id);
CREATE INDEX idx_contas_recorrentes_ativo ON public.contas_recorrentes(ativo);
CREATE INDEX idx_avisos_enviados_conta_id ON public.avisos_enviados(conta_id);
CREATE INDEX idx_avisos_enviados_data_aviso ON public.avisos_enviados(data_aviso);