-
getNavLinkClass(isActive)}
- onClick={isMobile ? onClose : undefined}
- >
-
- Configurações
-
+
+
+
+
+

+
Finance Home
+
-
+
);
-}
+};
+
+export default Sidebar;
diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts
index 98f1000..5ca25c4 100644
--- a/src/integrations/supabase/types.ts
+++ b/src/integrations/supabase/types.ts
@@ -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
diff --git a/src/pages/AvisosContas.tsx b/src/pages/AvisosContas.tsx
new file mode 100644
index 0000000..5a0e879
--- /dev/null
+++ b/src/pages/AvisosContas.tsx
@@ -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 (
+
+
+
+
Avisos de Contas a Pagar
+
Gerencie lembretes automáticos para suas contas recorrentes
+
+
+
+
+ {/* Cards informativos */}
+
+
+
+ Como funciona
+
+
+
+ Automático
+
+ Receba lembretes automáticos via WhatsApp
+
+
+
+
+
+
+ Antecedência
+
+
+
+ 1-30 dias
+
+ Configure quantos dias antes ser avisado
+
+
+
+
+
+
+ Horário
+
+
+
+ Personalizado
+
+ Escolha o horário ideal para receber avisos
+
+
+
+
+
+ {/* Formulário de nova conta */}
+ {showForm && (
+
setShowForm(false)}
+ onSuccess={() => {
+ setShowForm(false);
+ }}
+ />
+ )}
+
+ {/* Lista de contas */}
+
+
+ );
+};
+
+export default AvisosContas;
diff --git a/supabase/functions/avisos-financeiros/index.ts b/supabase/functions/avisos-financeiros/index.ts
new file mode 100644
index 0000000..39b8d63
--- /dev/null
+++ b/supabase/functions/avisos-financeiros/index.ts
@@ -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' },
+ }
+ )
+ }
+})
diff --git a/supabase/migrations/20250623213757-707e61ed-0dd2-4677-95c0-10c2053c2ca4.sql b/supabase/migrations/20250623213757-707e61ed-0dd2-4677-95c0-10c2053c2ca4.sql
new file mode 100644
index 0000000..5b840d3
--- /dev/null
+++ b/supabase/migrations/20250623213757-707e61ed-0dd2-4677-95c0-10c2053c2ca4.sql
@@ -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);