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:
parent
f01ed29305
commit
ee2273383b
175
src/App.tsx
175
src/App.tsx
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
194
src/components/avisos/ContaRecorrenteForm.tsx
Normal file
194
src/components/avisos/ContaRecorrenteForm.tsx
Normal 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;
|
||||
197
src/components/avisos/ContasRecorrentesList.tsx
Normal file
197
src/components/avisos/ContasRecorrentesList.tsx
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
86
src/pages/AvisosContas.tsx
Normal file
86
src/pages/AvisosContas.tsx
Normal 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;
|
||||
188
supabase/functions/avisos-financeiros/index.ts
Normal file
188
supabase/functions/avisos-financeiros/index.ts
Normal 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' },
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
@ -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);
|
||||
Loading…
Reference in New Issue
Block a user