chore: sincroniza projeto para gitea
This commit is contained in:
116
scripts/adaptar_tabela_configuracoes.sql
Normal file
116
scripts/adaptar_tabela_configuracoes.sql
Normal file
@@ -0,0 +1,116 @@
|
||||
-- Adaptar a estrutura da tabela configuracoes existente
|
||||
|
||||
-- 1. Ver estrutura atual
|
||||
SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'configuracoes'
|
||||
ORDER BY ordinal_position;
|
||||
|
||||
-- 2. Se a coluna é 'chave' e não 'tipo', vamos renomear
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Verificar se existe a coluna 'chave'
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'configuracoes'
|
||||
AND column_name = 'chave'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'configuracoes'
|
||||
AND column_name = 'tipo'
|
||||
) THEN
|
||||
-- Renomear 'chave' para 'tipo'
|
||||
ALTER TABLE public.configuracoes RENAME COLUMN chave TO tipo;
|
||||
RAISE NOTICE 'Coluna renomeada de chave para tipo';
|
||||
END IF;
|
||||
|
||||
-- Garantir que a coluna tipo existe
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'configuracoes'
|
||||
AND column_name = 'tipo'
|
||||
) THEN
|
||||
ALTER TABLE public.configuracoes ADD COLUMN tipo character varying NOT NULL DEFAULT '';
|
||||
RAISE NOTICE 'Coluna tipo adicionada';
|
||||
END IF;
|
||||
|
||||
-- Garantir que a coluna valor existe e é jsonb
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'configuracoes'
|
||||
AND column_name = 'valor'
|
||||
) THEN
|
||||
ALTER TABLE public.configuracoes ADD COLUMN valor jsonb;
|
||||
RAISE NOTICE 'Coluna valor adicionada';
|
||||
END IF;
|
||||
|
||||
-- Se existe coluna 'valor_str' ou 'valor_string', migrar para 'valor' jsonb
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'configuracoes'
|
||||
AND column_name IN ('valor_str', 'valor_string')
|
||||
) THEN
|
||||
-- Tentar converter os dados
|
||||
UPDATE public.configuracoes
|
||||
SET valor = COALESCE(valor_str::jsonb, valor_string::jsonb)
|
||||
WHERE valor IS NULL;
|
||||
RAISE NOTICE 'Dados migrados para coluna valor';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 3. Remover constraint NOT NULL da coluna chave se ainda existir
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'configuracoes'
|
||||
AND column_name = 'chave'
|
||||
AND is_nullable = 'NO'
|
||||
) THEN
|
||||
ALTER TABLE public.configuracoes ALTER COLUMN chave DROP NOT NULL;
|
||||
RAISE NOTICE 'Constraint NOT NULL removida da coluna chave';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 4. Adicionar constraint UNIQUE na coluna tipo
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Primeiro remover duplicados
|
||||
DELETE FROM public.configuracoes
|
||||
WHERE id IN (
|
||||
SELECT id
|
||||
FROM (
|
||||
SELECT id, tipo,
|
||||
ROW_NUMBER() OVER (PARTITION BY tipo ORDER BY updated_at DESC NULLS LAST, created_at DESC) as rn
|
||||
FROM public.configuracoes
|
||||
) t
|
||||
WHERE rn > 1
|
||||
);
|
||||
|
||||
-- Adicionar constraint
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'configuracoes_tipo_key'
|
||||
AND conrelid = 'public.configuracoes'::regclass
|
||||
) THEN
|
||||
ALTER TABLE public.configuracoes ADD CONSTRAINT configuracoes_tipo_key UNIQUE (tipo);
|
||||
RAISE NOTICE 'Constraint UNIQUE adicionada em tipo';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 5. Verificar resultado final
|
||||
SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'configuracoes'
|
||||
ORDER BY ordinal_position;
|
||||
|
||||
-- 6. Mostrar dados
|
||||
SELECT * FROM public.configuracoes ORDER BY tipo;
|
||||
123
scripts/aplicar-sistema-parcelas.sql
Normal file
123
scripts/aplicar-sistema-parcelas.sql
Normal file
@@ -0,0 +1,123 @@
|
||||
-- =====================================================
|
||||
-- SCRIPT COMPLETO: SISTEMA DE PARCELAS INDIVIDUAIS
|
||||
-- Execute este script no Supabase SQL Editor
|
||||
-- =====================================================
|
||||
|
||||
-- Verificar se a tabela já existe
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'venda_parcelas') THEN
|
||||
RAISE NOTICE '⚠️ Tabela venda_parcelas já existe. Pulando criação.';
|
||||
ELSE
|
||||
RAISE NOTICE '✅ Criando tabela venda_parcelas...';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Criar tabela de parcelas
|
||||
CREATE TABLE IF NOT EXISTS venda_parcelas (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
venda_id UUID NOT NULL REFERENCES vendas(id) ON DELETE CASCADE,
|
||||
numero_parcela INTEGER NOT NULL,
|
||||
valor DECIMAL(10,2) NOT NULL,
|
||||
data_vencimento DATE NOT NULL,
|
||||
status TEXT DEFAULT 'pendente' CHECK (status IN ('pendente', 'pago', 'vencida', 'cancelada')),
|
||||
data_pagamento TIMESTAMP WITH TIME ZONE,
|
||||
pix_payment_id TEXT,
|
||||
pix_qr_code TEXT,
|
||||
pix_qr_code_base64 TEXT,
|
||||
observacoes TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(venda_id, numero_parcela)
|
||||
);
|
||||
|
||||
-- Índices para performance
|
||||
CREATE INDEX IF NOT EXISTS idx_venda_parcelas_venda ON venda_parcelas(venda_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_venda_parcelas_status ON venda_parcelas(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_venda_parcelas_vencimento ON venda_parcelas(data_vencimento);
|
||||
|
||||
-- Trigger para atualização automática do campo updated_at
|
||||
DROP TRIGGER IF EXISTS update_venda_parcelas_updated_at ON venda_parcelas;
|
||||
CREATE TRIGGER update_venda_parcelas_updated_at
|
||||
BEFORE UPDATE ON venda_parcelas
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Habilitar Row Level Security
|
||||
ALTER TABLE venda_parcelas ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Remover política antiga se existir
|
||||
DROP POLICY IF EXISTS "Enable all operations for authenticated users" ON venda_parcelas;
|
||||
|
||||
-- Criar política de acesso (permitir todas operações)
|
||||
CREATE POLICY "Enable all operations for authenticated users"
|
||||
ON venda_parcelas
|
||||
FOR ALL
|
||||
USING (true);
|
||||
|
||||
-- Comentários para documentação
|
||||
COMMENT ON TABLE venda_parcelas IS 'Armazena as parcelas individuais de cada venda parcelada com controle de status e PIX';
|
||||
COMMENT ON COLUMN venda_parcelas.numero_parcela IS 'Número sequencial da parcela (1, 2, 3, etc)';
|
||||
COMMENT ON COLUMN venda_parcelas.valor IS 'Valor da parcela individual';
|
||||
COMMENT ON COLUMN venda_parcelas.data_vencimento IS 'Data de vencimento da parcela';
|
||||
COMMENT ON COLUMN venda_parcelas.status IS 'Status da parcela: pendente, pago, vencida, cancelada';
|
||||
COMMENT ON COLUMN venda_parcelas.data_pagamento IS 'Data e hora em que a parcela foi paga';
|
||||
COMMENT ON COLUMN venda_parcelas.pix_payment_id IS 'ID do pagamento PIX no MercadoPago';
|
||||
COMMENT ON COLUMN venda_parcelas.pix_qr_code IS 'Código PIX para cópia e cola';
|
||||
COMMENT ON COLUMN venda_parcelas.pix_qr_code_base64 IS 'QR Code em base64 para exibição';
|
||||
|
||||
-- =====================================================
|
||||
-- VERIFICAÇÃO E DIAGNÓSTICO
|
||||
-- =====================================================
|
||||
|
||||
-- Verificar se a tabela foi criada
|
||||
DO $$
|
||||
DECLARE
|
||||
table_count INTEGER;
|
||||
index_count INTEGER;
|
||||
policy_count INTEGER;
|
||||
BEGIN
|
||||
-- Contar tabela
|
||||
SELECT COUNT(*) INTO table_count
|
||||
FROM information_schema.tables
|
||||
WHERE table_name = 'venda_parcelas';
|
||||
|
||||
-- Contar índices
|
||||
SELECT COUNT(*) INTO index_count
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'venda_parcelas';
|
||||
|
||||
-- Contar políticas
|
||||
SELECT COUNT(*) INTO policy_count
|
||||
FROM pg_policies
|
||||
WHERE tablename = 'venda_parcelas';
|
||||
|
||||
-- Exibir resultados
|
||||
RAISE NOTICE '========================================';
|
||||
RAISE NOTICE '✅ VERIFICAÇÃO DO SISTEMA DE PARCELAS';
|
||||
RAISE NOTICE '========================================';
|
||||
RAISE NOTICE 'Tabela venda_parcelas: % %',
|
||||
CASE WHEN table_count > 0 THEN '✅ CRIADA' ELSE '❌ NÃO ENCONTRADA' END,
|
||||
CASE WHEN table_count > 0 THEN '' ELSE ' - Execute o script novamente!' END;
|
||||
RAISE NOTICE 'Índices criados: % índice(s)', index_count;
|
||||
RAISE NOTICE 'Políticas RLS: % política(s)', policy_count;
|
||||
RAISE NOTICE '========================================';
|
||||
|
||||
IF table_count > 0 THEN
|
||||
RAISE NOTICE '🎉 Sistema de parcelas instalado com sucesso!';
|
||||
RAISE NOTICE '📝 Próximo passo: Reinicie o servidor Node.js';
|
||||
RAISE NOTICE '🚀 Depois: Teste criando uma venda parcelada';
|
||||
ELSE
|
||||
RAISE NOTICE '⚠️ Erro na instalação. Verifique as mensagens acima.';
|
||||
END IF;
|
||||
RAISE NOTICE '========================================';
|
||||
END $$;
|
||||
|
||||
-- Verificar estrutura da tabela
|
||||
SELECT
|
||||
column_name as "Coluna",
|
||||
data_type as "Tipo",
|
||||
is_nullable as "Nullable"
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'venda_parcelas'
|
||||
ORDER BY ordinal_position;
|
||||
61
scripts/configurar_permissoes.sql
Normal file
61
scripts/configurar_permissoes.sql
Normal file
@@ -0,0 +1,61 @@
|
||||
-- Configurar permissões para as tabelas configuracoes e mensagens_whatsapp
|
||||
|
||||
-- ============================================
|
||||
-- TABELA: configuracoes
|
||||
-- ============================================
|
||||
|
||||
-- Desabilitar RLS temporariamente para testes
|
||||
ALTER TABLE public.configuracoes DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Ou criar uma política que permite todas as operações
|
||||
-- (use esta opção se precisar manter o RLS ativado)
|
||||
/*
|
||||
ALTER TABLE public.configuracoes ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Remover políticas existentes
|
||||
DROP POLICY IF EXISTS "Permitir tudo em configuracoes" ON public.configuracoes;
|
||||
|
||||
-- Criar política permissiva
|
||||
CREATE POLICY "Permitir tudo em configuracoes"
|
||||
ON public.configuracoes
|
||||
FOR ALL
|
||||
TO public
|
||||
USING (true)
|
||||
WITH CHECK (true);
|
||||
*/
|
||||
|
||||
-- ============================================
|
||||
-- TABELA: mensagens_whatsapp
|
||||
-- ============================================
|
||||
|
||||
-- Desabilitar RLS temporariamente para testes
|
||||
ALTER TABLE public.mensagens_whatsapp DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Ou criar uma política que permite todas as operações
|
||||
/*
|
||||
ALTER TABLE public.mensagens_whatsapp ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS "Permitir tudo em mensagens_whatsapp" ON public.mensagens_whatsapp;
|
||||
|
||||
CREATE POLICY "Permitir tudo em mensagens_whatsapp"
|
||||
ON public.mensagens_whatsapp
|
||||
FOR ALL
|
||||
TO public
|
||||
USING (true)
|
||||
WITH CHECK (true);
|
||||
*/
|
||||
|
||||
-- ============================================
|
||||
-- VERIFICAÇÃO
|
||||
-- ============================================
|
||||
|
||||
-- Verificar se as tabelas existem e estão acessíveis
|
||||
SELECT
|
||||
'configuracoes' as tabela,
|
||||
COUNT(*) as total_registros
|
||||
FROM public.configuracoes
|
||||
UNION ALL
|
||||
SELECT
|
||||
'mensagens_whatsapp' as tabela,
|
||||
COUNT(*) as total_registros
|
||||
FROM public.mensagens_whatsapp;
|
||||
64
scripts/corrigir_constraint_final.sql
Normal file
64
scripts/corrigir_constraint_final.sql
Normal file
@@ -0,0 +1,64 @@
|
||||
-- Corrigir constraints da tabela configuracoes
|
||||
|
||||
-- 1. Remover a constraint incorreta (configuracoes_tipo_key)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'configuracoes_tipo_key'
|
||||
AND conrelid = 'public.configuracoes'::regclass
|
||||
) THEN
|
||||
ALTER TABLE public.configuracoes DROP CONSTRAINT configuracoes_tipo_key;
|
||||
RAISE NOTICE 'Constraint configuracoes_tipo_key removida';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 2. Garantir que existe constraint UNIQUE na coluna 'chave'
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Primeiro remover duplicados na coluna 'chave' (mantendo o mais recente)
|
||||
DELETE FROM public.configuracoes
|
||||
WHERE id IN (
|
||||
SELECT id
|
||||
FROM (
|
||||
SELECT id, chave,
|
||||
ROW_NUMBER() OVER (PARTITION BY chave ORDER BY updated_at DESC NULLS LAST, created_at DESC) as rn
|
||||
FROM public.configuracoes
|
||||
WHERE chave IS NOT NULL
|
||||
) t
|
||||
WHERE rn > 1
|
||||
);
|
||||
RAISE NOTICE 'Duplicados removidos da coluna chave';
|
||||
|
||||
-- Adicionar constraint UNIQUE em 'chave' se não existir
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'configuracoes_chave_key'
|
||||
AND conrelid = 'public.configuracoes'::regclass
|
||||
) THEN
|
||||
ALTER TABLE public.configuracoes ADD CONSTRAINT configuracoes_chave_key UNIQUE (chave);
|
||||
RAISE NOTICE 'Constraint UNIQUE adicionada na coluna chave';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 3. Verificar constraints existentes
|
||||
SELECT
|
||||
conname as constraint_name,
|
||||
contype as constraint_type,
|
||||
a.attname as column_name
|
||||
FROM pg_constraint c
|
||||
JOIN pg_attribute a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
|
||||
WHERE c.conrelid = 'public.configuracoes'::regclass
|
||||
ORDER BY conname;
|
||||
|
||||
-- 4. Verificar estrutura final
|
||||
SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'configuracoes'
|
||||
ORDER BY ordinal_position;
|
||||
|
||||
-- 5. Mostrar dados
|
||||
SELECT id, chave, tipo, descricao, created_at, updated_at
|
||||
FROM public.configuracoes
|
||||
ORDER BY chave;
|
||||
65
scripts/criar-tabela-parcelas-CORRIGIDO.sql
Normal file
65
scripts/criar-tabela-parcelas-CORRIGIDO.sql
Normal file
@@ -0,0 +1,65 @@
|
||||
-- =====================================================
|
||||
-- SCRIPT CORRIGIDO: CRIAR TABELA DE PARCELAS
|
||||
-- Execute este script no Supabase SQL Editor
|
||||
-- =====================================================
|
||||
|
||||
-- Criar tabela de parcelas (se não existir)
|
||||
CREATE TABLE IF NOT EXISTS venda_parcelas (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
venda_id UUID NOT NULL REFERENCES vendas(id) ON DELETE CASCADE,
|
||||
numero_parcela INTEGER NOT NULL,
|
||||
valor DECIMAL(10,2) NOT NULL,
|
||||
data_vencimento DATE NOT NULL,
|
||||
status TEXT DEFAULT 'pendente' CHECK (status IN ('pendente', 'pago', 'vencida', 'cancelada')),
|
||||
data_pagamento TIMESTAMP WITH TIME ZONE,
|
||||
pix_payment_id TEXT,
|
||||
pix_qr_code TEXT,
|
||||
pix_qr_code_base64 TEXT,
|
||||
observacoes TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(venda_id, numero_parcela)
|
||||
);
|
||||
|
||||
-- Criar índices (se não existirem)
|
||||
CREATE INDEX IF NOT EXISTS idx_venda_parcelas_venda ON venda_parcelas(venda_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_venda_parcelas_status ON venda_parcelas(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_venda_parcelas_vencimento ON venda_parcelas(data_vencimento);
|
||||
|
||||
-- Remover trigger antiga se existir e criar nova
|
||||
DROP TRIGGER IF EXISTS update_venda_parcelas_updated_at ON venda_parcelas;
|
||||
CREATE TRIGGER update_venda_parcelas_updated_at
|
||||
BEFORE UPDATE ON venda_parcelas
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Habilitar RLS
|
||||
ALTER TABLE venda_parcelas ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Remover política antiga se existir
|
||||
DROP POLICY IF EXISTS "Enable all operations for authenticated users" ON venda_parcelas;
|
||||
|
||||
-- Criar política de acesso
|
||||
CREATE POLICY "Enable all operations for authenticated users"
|
||||
ON venda_parcelas
|
||||
FOR ALL
|
||||
USING (true);
|
||||
|
||||
-- Verificar se foi criado com sucesso
|
||||
DO $$
|
||||
DECLARE
|
||||
table_exists BOOLEAN;
|
||||
BEGIN
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'venda_parcelas'
|
||||
) INTO table_exists;
|
||||
|
||||
IF table_exists THEN
|
||||
RAISE NOTICE '✅ Tabela venda_parcelas criada/verificada com sucesso!';
|
||||
RAISE NOTICE '📝 Próximo passo: Reinicie o servidor Node.js';
|
||||
RAISE NOTICE '🚀 Depois: Crie uma nova venda parcelada para testar';
|
||||
ELSE
|
||||
RAISE NOTICE '❌ Erro: Tabela não foi criada';
|
||||
END IF;
|
||||
END $$;
|
||||
157
scripts/deploy-servidor.sh
Executable file
157
scripts/deploy-servidor.sh
Executable file
@@ -0,0 +1,157 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 🚀 Script de Deploy Automático - Liberi Kids
|
||||
# Deploy no servidor local com PM2
|
||||
|
||||
set -e # Parar em caso de erro
|
||||
|
||||
# Cores para output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${GREEN}🚀 Deploy Automático - Liberi Kids${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
|
||||
# Função para verificar se comando existe
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# 1. Verificar Node.js
|
||||
echo -e "${YELLOW}📦 Verificando Node.js...${NC}"
|
||||
if ! command_exists node; then
|
||||
echo -e "${RED}❌ Node.js não encontrado!${NC}"
|
||||
echo -e "Instale com: ${BLUE}curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - && sudo apt-get install -y nodejs${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✅ Node.js $(node --version) encontrado${NC}"
|
||||
echo ""
|
||||
|
||||
# 2. Verificar PM2
|
||||
echo -e "${YELLOW}📦 Verificando PM2...${NC}"
|
||||
if ! command_exists pm2; then
|
||||
echo -e "${YELLOW}⚠️ PM2 não encontrado. Instalando...${NC}"
|
||||
sudo npm install -g pm2
|
||||
echo -e "${GREEN}✅ PM2 instalado com sucesso${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✅ PM2 $(pm2 --version) encontrado${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 3. Verificar arquivo .env
|
||||
echo -e "${YELLOW}🔧 Verificando configurações...${NC}"
|
||||
if [ ! -f ".env" ]; then
|
||||
echo -e "${RED}❌ Arquivo .env não encontrado!${NC}"
|
||||
if [ -f ".env.example" ]; then
|
||||
echo -e "${YELLOW}Criando .env a partir do .env.example...${NC}"
|
||||
cp .env.example .env
|
||||
echo -e "${YELLOW}⚠️ ATENÇÃO: Configure o arquivo .env com suas credenciais!${NC}"
|
||||
echo -e "Execute: ${BLUE}nano .env${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${RED}Nem .env nem .env.example encontrados!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN}✅ Arquivo .env encontrado${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 4. Instalar dependências do backend
|
||||
echo -e "${YELLOW}📦 Instalando dependências do backend...${NC}"
|
||||
npm install
|
||||
echo -e "${GREEN}✅ Dependências do backend instaladas${NC}"
|
||||
echo ""
|
||||
|
||||
# 5. Instalar dependências do frontend
|
||||
echo -e "${YELLOW}📦 Instalando dependências do frontend...${NC}"
|
||||
cd client
|
||||
npm install
|
||||
echo -e "${GREEN}✅ Dependências do frontend instaladas${NC}"
|
||||
echo ""
|
||||
|
||||
# 6. Build do frontend
|
||||
echo -e "${YELLOW}🔨 Fazendo build do frontend...${NC}"
|
||||
npm run build
|
||||
cd ..
|
||||
echo -e "${GREEN}✅ Build do frontend concluído${NC}"
|
||||
echo ""
|
||||
|
||||
# 7. Criar diretório de logs
|
||||
echo -e "${YELLOW}📁 Criando diretórios necessários...${NC}"
|
||||
mkdir -p logs
|
||||
mkdir -p uploads
|
||||
echo -e "${GREEN}✅ Diretórios criados${NC}"
|
||||
echo ""
|
||||
|
||||
# 8. Parar processo anterior (se existir)
|
||||
echo -e "${YELLOW}🛑 Parando processos anteriores...${NC}"
|
||||
if pm2 list | grep -q "liberi-kids-estoque"; then
|
||||
pm2 stop liberi-kids-estoque
|
||||
pm2 delete liberi-kids-estoque
|
||||
echo -e "${GREEN}✅ Processo anterior removido${NC}"
|
||||
else
|
||||
echo -e "${BLUE}ℹ️ Nenhum processo anterior encontrado${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 9. Iniciar com PM2
|
||||
echo -e "${YELLOW}🚀 Iniciando aplicação com PM2...${NC}"
|
||||
pm2 start ecosystem.config.js
|
||||
echo -e "${GREEN}✅ Aplicação iniciada${NC}"
|
||||
echo ""
|
||||
|
||||
# 10. Salvar configuração PM2
|
||||
echo -e "${YELLOW}💾 Salvando configuração do PM2...${NC}"
|
||||
pm2 save
|
||||
echo -e "${GREEN}✅ Configuração salva${NC}"
|
||||
echo ""
|
||||
|
||||
# 11. Verificar status
|
||||
echo -e "${YELLOW}📊 Verificando status...${NC}"
|
||||
pm2 status
|
||||
echo ""
|
||||
|
||||
# 12. Configurar auto-start (opcional)
|
||||
echo -e "${YELLOW}🔄 Deseja configurar inicialização automática no boot? (s/n)${NC}"
|
||||
read -r resposta
|
||||
if [ "$resposta" = "s" ] || [ "$resposta" = "S" ]; then
|
||||
echo -e "${YELLOW}Configurando auto-start...${NC}"
|
||||
pm2 startup
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚠️ IMPORTANTE: Execute o comando mostrado acima com sudo!${NC}"
|
||||
echo -e "Depois execute: ${BLUE}pm2 save${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 13. Verificar porta
|
||||
PORT=$(grep "^PORT=" .env | cut -d '=' -f2 || echo "5000")
|
||||
PORT=${PORT:-5000}
|
||||
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${GREEN}✅ Deploy concluído com sucesso!${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}📍 Aplicação rodando em:${NC}"
|
||||
echo -e " ${BLUE}http://localhost:${PORT}${NC}"
|
||||
echo -e " ${BLUE}http://$(hostname -I | awk '{print $1}'):${PORT}${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}📦 Catálogo público em:${NC}"
|
||||
echo -e " ${BLUE}http://localhost:${PORT}/catalogo${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}📊 Comandos úteis:${NC}"
|
||||
echo -e " ${BLUE}pm2 status${NC} - Ver status"
|
||||
echo -e " ${BLUE}pm2 logs${NC} - Ver logs"
|
||||
echo -e " ${BLUE}pm2 monit${NC} - Monitorar"
|
||||
echo -e " ${BLUE}pm2 restart all${NC} - Reiniciar"
|
||||
echo -e " ${BLUE}pm2 stop all${NC} - Parar"
|
||||
echo ""
|
||||
echo -e "${GREEN}📝 Ver logs agora:${NC}"
|
||||
echo -e " ${BLUE}pm2 logs liberi-kids-estoque${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
260
scripts/enviar-alertas-atrasados.js
Normal file
260
scripts/enviar-alertas-atrasados.js
Normal file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script para Enviar Alertas Atrasados
|
||||
*
|
||||
* Este script envia manualmente alertas que não foram enviados
|
||||
* (para quando o cron não estava configurado)
|
||||
*/
|
||||
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const axios = require('axios');
|
||||
const readline = require('readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
function question(query) {
|
||||
return new Promise(resolve => rl.question(query, resolve));
|
||||
}
|
||||
|
||||
// Configurações
|
||||
const SUPABASE_URL = process.env.SUPABASE_URL || 'https://ydhzylfnpqlxnzfcclla.supabase.co';
|
||||
const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_ANON_KEY;
|
||||
|
||||
if (!SUPABASE_KEY) {
|
||||
console.error('❌ SUPABASE_KEY não configurado!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
|
||||
function formatarDataBR(data) {
|
||||
const d = new Date(data);
|
||||
return d.toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' });
|
||||
}
|
||||
|
||||
function formatarMoeda(valor) {
|
||||
return new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL'
|
||||
}).format(valor);
|
||||
}
|
||||
|
||||
async function enviarWhatsApp(telefone, mensagem, evolutionConfig) {
|
||||
try {
|
||||
const url = `${evolutionConfig.apiUrl}/message/sendText/${evolutionConfig.instanceName}`;
|
||||
|
||||
const response = await axios.post(url, {
|
||||
number: telefone,
|
||||
textMessage: {
|
||||
text: mensagem
|
||||
}
|
||||
}, {
|
||||
headers: {
|
||||
'apikey': evolutionConfig.apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function gerarPix(parcela, mercadoPagoToken) {
|
||||
try {
|
||||
const response = await axios.post('https://api.mercadopago.com/v1/payments', {
|
||||
transaction_amount: parseFloat(parcela.valor),
|
||||
description: `Parcela ${parcela.numero_parcela} - Venda #${parcela.venda_id}`,
|
||||
payment_method_id: 'pix',
|
||||
payer: {
|
||||
email: 'cliente@liberi.com.br'
|
||||
}
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${mercadoPagoToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
qrCode: response.data.point_of_interaction?.transaction_data?.qr_code,
|
||||
qrCodeBase64: response.data.point_of_interaction?.transaction_data?.qr_code_base64,
|
||||
paymentId: response.data.id
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('\n🔔 ENVIO MANUAL DE ALERTAS ATRASADOS\n');
|
||||
console.log('Este script enviará alertas que não foram enviados automaticamente.\n');
|
||||
|
||||
try {
|
||||
// Buscar configurações
|
||||
const { data: configData } = await supabase
|
||||
.from('configuracoes')
|
||||
.select('chave, valor')
|
||||
.in('chave', [
|
||||
'evolution_api_url',
|
||||
'evolution_instance_name',
|
||||
'evolution_api_key',
|
||||
'mercadopago_access_token'
|
||||
]);
|
||||
|
||||
const config = {};
|
||||
configData?.forEach(item => {
|
||||
config[item.chave] = item.valor;
|
||||
});
|
||||
|
||||
const evolutionConfig = {
|
||||
apiUrl: config.evolution_api_url,
|
||||
instanceName: config.evolution_instance_name,
|
||||
apiKey: config.evolution_api_key
|
||||
};
|
||||
|
||||
const mercadoPagoToken = config.mercadopago_access_token;
|
||||
|
||||
// Buscar parcelas pendentes com vencimento hoje ou passado
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
|
||||
const { data: parcelas, error } = await supabase
|
||||
.from('venda_parcelas')
|
||||
.select(`
|
||||
*,
|
||||
vendas (
|
||||
id_venda,
|
||||
cliente_id,
|
||||
clientes (
|
||||
nome_completo,
|
||||
whatsapp
|
||||
)
|
||||
)
|
||||
`)
|
||||
.eq('status', 'pendente')
|
||||
.lte('data_vencimento', hoje.toISOString())
|
||||
.order('data_vencimento', { ascending: true });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
if (!parcelas || parcelas.length === 0) {
|
||||
console.log('✅ Não há parcelas vencidas ou vencendo hoje.\n');
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📦 Encontradas ${parcelas.length} parcela(s) vencida(s) ou vencendo hoje:\n`);
|
||||
|
||||
parcelas.forEach((parcela, index) => {
|
||||
const cliente = parcela.vendas?.clientes;
|
||||
console.log(`${index + 1}. ${cliente?.nome_completo || 'N/A'} - Parcela ${parcela.numero_parcela}`);
|
||||
console.log(` Valor: ${formatarMoeda(parcela.valor)}`);
|
||||
console.log(` Vencimento: ${formatarDataBR(parcela.data_vencimento)}`);
|
||||
console.log(` WhatsApp: ${cliente?.whatsapp || 'N/A'}`);
|
||||
console.log('');
|
||||
});
|
||||
|
||||
const resposta = await question('Deseja enviar alertas + PIX para essas parcelas? (s/N): ');
|
||||
|
||||
if (!resposta.match(/^[Ss]$/)) {
|
||||
console.log('\n❌ Operação cancelada.\n');
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\n🚀 Iniciando envio...\n');
|
||||
|
||||
let enviados = 0;
|
||||
let erros = 0;
|
||||
|
||||
for (const parcela of parcelas) {
|
||||
const cliente = parcela.vendas?.clientes;
|
||||
|
||||
if (!cliente || !cliente.whatsapp) {
|
||||
console.log(`⚠️ Parcela ${parcela.numero_parcela}: Cliente sem WhatsApp`);
|
||||
erros++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const nomeCliente = cliente.nome_completo.split(' ')[0];
|
||||
const valorFormatado = formatarMoeda(parcela.valor);
|
||||
|
||||
// Gerar PIX
|
||||
console.log(`📱 Gerando PIX para ${cliente.nome_completo}...`);
|
||||
const pixResult = await gerarPix(parcela, mercadoPagoToken);
|
||||
|
||||
let mensagem = `Olá ${nomeCliente}! 👋\n\n`;
|
||||
mensagem += `Você tem uma parcela no valor de ${valorFormatado} `;
|
||||
mensagem += `com vencimento em ${formatarDataBR(parcela.data_vencimento)}.\n\n`;
|
||||
|
||||
if (pixResult.success && pixResult.qrCode) {
|
||||
mensagem += `📱 *PIX Copia e Cola:*\n\`\`\`${pixResult.qrCode}\`\`\`\n\n`;
|
||||
|
||||
// Atualizar parcela com PIX
|
||||
await supabase
|
||||
.from('venda_parcelas')
|
||||
.update({
|
||||
pix_payment_id: pixResult.paymentId,
|
||||
pix_qr_code: pixResult.qrCode,
|
||||
pix_qr_code_base64: pixResult.qrCodeBase64
|
||||
})
|
||||
.eq('id', parcela.id);
|
||||
|
||||
console.log(` ✅ PIX gerado`);
|
||||
} else {
|
||||
mensagem += `Entre em contato para obter forma de pagamento.\n\n`;
|
||||
console.log(` ⚠️ Não foi possível gerar PIX`);
|
||||
}
|
||||
|
||||
mensagem += `Agradecemos! 😊\n*Liberi Kids - Moda Infantil* 👗👕`;
|
||||
|
||||
// Enviar mensagem
|
||||
console.log(`📤 Enviando para ${cliente.whatsapp}...`);
|
||||
const resultado = await enviarWhatsApp(cliente.whatsapp, mensagem, evolutionConfig);
|
||||
|
||||
if (resultado.success) {
|
||||
enviados++;
|
||||
console.log(` ✅ Enviado com sucesso!\n`);
|
||||
|
||||
// Registrar histórico
|
||||
await supabase
|
||||
.from('mensagens_whatsapp')
|
||||
.insert({
|
||||
telefone_cliente: cliente.whatsapp,
|
||||
cliente_nome: cliente.nome_completo,
|
||||
mensagem: mensagem,
|
||||
tipo: 'enviada',
|
||||
status: 'enviada'
|
||||
});
|
||||
} else {
|
||||
erros++;
|
||||
console.log(` ❌ Erro: ${resultado.error}\n`);
|
||||
}
|
||||
|
||||
// Delay entre mensagens
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(50));
|
||||
console.log('📊 RESUMO');
|
||||
console.log('='.repeat(50));
|
||||
console.log(`✅ Alertas enviados: ${enviados}`);
|
||||
console.log(`❌ Erros: ${erros}`);
|
||||
console.log(`📦 Total processado: ${parcelas.length}`);
|
||||
console.log('='.repeat(50) + '\n');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n💥 Erro:', error);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
326
scripts/enviar-alertas-parcelas.js
Normal file
326
scripts/enviar-alertas-parcelas.js
Normal file
@@ -0,0 +1,326 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script de Envio Automático de Alertas de Vencimento
|
||||
*
|
||||
* Este script deve ser executado diariamente às 09:00 (horário de Brasília)
|
||||
* via cron job para enviar alertas de parcelas vencendo.
|
||||
*
|
||||
* Cron: 0 9 * * * /usr/bin/node /caminho/para/enviar-alertas-parcelas.js
|
||||
*/
|
||||
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const axios = require('axios');
|
||||
|
||||
// Configurações do Supabase
|
||||
const SUPABASE_URL = process.env.SUPABASE_URL || 'https://ydhzylfnpqlxnzfcclla.supabase.co';
|
||||
const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_ANON_KEY;
|
||||
|
||||
if (!SUPABASE_KEY) {
|
||||
console.error('❌ SUPABASE_KEY não configurado!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
|
||||
// Função para formatar data no padrão brasileiro
|
||||
function formatarDataBR(data) {
|
||||
const d = new Date(data);
|
||||
return d.toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' });
|
||||
}
|
||||
|
||||
// Função para formatar moeda
|
||||
function formatarMoeda(valor) {
|
||||
return new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL'
|
||||
}).format(valor);
|
||||
}
|
||||
|
||||
// Função para calcular dias entre datas
|
||||
function calcularDiasEntre(data1, data2) {
|
||||
const umDia = 24 * 60 * 60 * 1000;
|
||||
const d1 = new Date(data1);
|
||||
const d2 = new Date(data2);
|
||||
return Math.round((d2 - d1) / umDia);
|
||||
}
|
||||
|
||||
// Função para enviar mensagem via Evolution API
|
||||
async function enviarWhatsApp(telefone, mensagem, evolutionConfig) {
|
||||
try {
|
||||
const url = `${evolutionConfig.apiUrl}/message/sendText/${evolutionConfig.instanceName}`;
|
||||
|
||||
const response = await axios.post(url, {
|
||||
number: telefone,
|
||||
textMessage: {
|
||||
text: mensagem
|
||||
}
|
||||
}, {
|
||||
headers: {
|
||||
'apikey': evolutionConfig.apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
console.error(`❌ Erro ao enviar WhatsApp para ${telefone}:`, error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Função para gerar PIX via Mercado Pago
|
||||
async function gerarPix(parcela, mercadoPagoToken) {
|
||||
try {
|
||||
const response = await axios.post('https://api.mercadopago.com/v1/payments', {
|
||||
transaction_amount: parseFloat(parcela.valor),
|
||||
description: `Parcela ${parcela.numero_parcela} - Venda #${parcela.venda_id}`,
|
||||
payment_method_id: 'pix',
|
||||
payer: {
|
||||
email: 'cliente@liberi.com.br',
|
||||
identification: {
|
||||
type: 'CPF',
|
||||
number: '00000000000'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${mercadoPagoToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
qrCode: response.data.point_of_interaction?.transaction_data?.qr_code,
|
||||
qrCodeBase64: response.data.point_of_interaction?.transaction_data?.qr_code_base64,
|
||||
paymentId: response.data.id
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`❌ Erro ao gerar PIX para parcela ${parcela.id}:`, error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Função principal
|
||||
async function enviarAlertasVencimento() {
|
||||
console.log('\n🕐 Iniciando envio de alertas de vencimento...');
|
||||
console.log(`⏰ Horário: ${new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })}\n`);
|
||||
|
||||
try {
|
||||
// 1. Buscar configurações WhatsApp
|
||||
const { data: configData } = await supabase
|
||||
.from('configuracoes')
|
||||
.select('chave, valor')
|
||||
.in('chave', [
|
||||
'whatsapp_primeiro_alerta_dias',
|
||||
'whatsapp_segundo_alerta_dias',
|
||||
'whatsapp_alerta_apos_vencimento_dias',
|
||||
'whatsapp_primeiro_alerta_ativo',
|
||||
'whatsapp_segundo_alerta_ativo',
|
||||
'whatsapp_alerta_apos_vencimento_ativo',
|
||||
'whatsapp_primeiro_alerta_mensagem',
|
||||
'whatsapp_segundo_alerta_mensagem',
|
||||
'whatsapp_alerta_apos_vencimento_mensagem',
|
||||
'evolution_api_url',
|
||||
'evolution_instance_name',
|
||||
'evolution_api_key',
|
||||
'mercadopago_access_token'
|
||||
]);
|
||||
|
||||
const config = {};
|
||||
configData?.forEach(item => {
|
||||
config[item.chave] = item.valor;
|
||||
});
|
||||
|
||||
// Configurações padrão
|
||||
const primeiroAlertaDias = parseInt(config.whatsapp_primeiro_alerta_dias) || 3;
|
||||
const segundoAlertaDias = parseInt(config.whatsapp_segundo_alerta_dias) || 0;
|
||||
const alertaAposVencimentoDias = parseInt(config.whatsapp_alerta_apos_vencimento_dias) || 3;
|
||||
|
||||
const primeiroAlertas = config.whatsapp_primeiro_alerta_ativo === 'true';
|
||||
const segundoAlertas = config.whatsapp_segundo_alerta_ativo === 'true';
|
||||
const alertaAposVencimento = config.whatsapp_alerta_apos_vencimento_ativo === 'true';
|
||||
|
||||
const evolutionConfig = {
|
||||
apiUrl: config.evolution_api_url,
|
||||
instanceName: config.evolution_instance_name,
|
||||
apiKey: config.evolution_api_key
|
||||
};
|
||||
|
||||
const mercadoPagoToken = config.mercadopago_access_token;
|
||||
|
||||
console.log('📋 Configurações carregadas:');
|
||||
console.log(` - Primeiro alerta: ${primeiroAlertaDias} dias antes (${primeiroAlertas ? 'ATIVO' : 'INATIVO'})`);
|
||||
console.log(` - Segundo alerta: ${segundoAlertaDias} dias antes (${segundoAlertas ? 'ATIVO' : 'INATIVO'})`);
|
||||
console.log(` - Alerta pós-vencimento: ${alertaAposVencimentoDias} dias após (${alertaAposVencimento ? 'ATIVO' : 'INATIVO'})\n`);
|
||||
|
||||
// 2. Buscar parcelas pendentes
|
||||
const { data: parcelas, error } = await supabase
|
||||
.from('venda_parcelas')
|
||||
.select(`
|
||||
*,
|
||||
vendas (
|
||||
id_venda,
|
||||
cliente_id,
|
||||
clientes (
|
||||
nome_completo,
|
||||
whatsapp
|
||||
)
|
||||
)
|
||||
`)
|
||||
.eq('status', 'pendente')
|
||||
.order('data_vencimento', { ascending: true });
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!parcelas || parcelas.length === 0) {
|
||||
console.log('ℹ️ Nenhuma parcela pendente encontrada.\n');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📦 ${parcelas.length} parcela(s) pendente(s) encontrada(s)\n`);
|
||||
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
|
||||
let alertasEnviados = 0;
|
||||
let erros = 0;
|
||||
|
||||
// 3. Processar cada parcela
|
||||
for (const parcela of parcelas) {
|
||||
const dataVencimento = new Date(parcela.data_vencimento);
|
||||
dataVencimento.setHours(0, 0, 0, 0);
|
||||
|
||||
const diasParaVencimento = calcularDiasEntre(hoje, dataVencimento);
|
||||
const cliente = parcela.vendas?.clientes;
|
||||
|
||||
if (!cliente || !cliente.whatsapp) {
|
||||
console.log(`⚠️ Parcela ${parcela.numero_parcela} (Venda #${parcela.vendas?.id_venda}): Cliente sem WhatsApp`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let deveEnviar = false;
|
||||
let tipoAlerta = '';
|
||||
let mensagemTemplate = '';
|
||||
|
||||
// Verificar qual tipo de alerta enviar
|
||||
if (diasParaVencimento === primeiroAlertaDias && primeiroAlertas) {
|
||||
deveEnviar = true;
|
||||
tipoAlerta = 'primeiro_alerta';
|
||||
mensagemTemplate = config.whatsapp_primeiro_alerta_mensagem ||
|
||||
'Olá {cliente}! 👋\n\nLembramos que você tem uma parcela no valor de {valor} com vencimento {quando}.\n\nAgradecemos a atenção!';
|
||||
} else if (diasParaVencimento === segundoAlertaDias && segundoAlertas) {
|
||||
deveEnviar = true;
|
||||
tipoAlerta = 'segundo_alerta';
|
||||
mensagemTemplate = config.whatsapp_segundo_alerta_mensagem ||
|
||||
'Olá {cliente}! 👋\n\nSua parcela de {valor} vence {quando}.\n\n📱 Gerando PIX para facilitar o pagamento...';
|
||||
} else if (diasParaVencimento < 0 && Math.abs(diasParaVencimento) === alertaAposVencimentoDias && alertaAposVencimento) {
|
||||
deveEnviar = true;
|
||||
tipoAlerta = 'alerta_apos_vencimento';
|
||||
mensagemTemplate = config.whatsapp_alerta_apos_vencimento_mensagem ||
|
||||
'Olá {cliente}! 👋\n\nIdentificamos que a parcela {parcela} no valor de {valor} venceu {quando}.\n\nPor favor, regularize o pagamento.';
|
||||
}
|
||||
|
||||
if (!deveEnviar) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Preparar dados da mensagem
|
||||
const nomeCliente = cliente.nome_completo.split(' ')[0];
|
||||
const valorFormatado = formatarMoeda(parcela.valor);
|
||||
const dataVencFormatada = formatarDataBR(parcela.data_vencimento);
|
||||
|
||||
let quando = '';
|
||||
if (diasParaVencimento > 0) {
|
||||
quando = diasParaVencimento === 1 ? 'amanhã' : `em ${diasParaVencimento} dias (${dataVencFormatada})`;
|
||||
} else if (diasParaVencimento === 0) {
|
||||
quando = 'hoje';
|
||||
} else {
|
||||
quando = `há ${Math.abs(diasParaVencimento)} dias (${dataVencFormatada})`;
|
||||
}
|
||||
|
||||
// Substituir variáveis na mensagem
|
||||
let mensagem = mensagemTemplate
|
||||
.replace(/{cliente}/g, nomeCliente)
|
||||
.replace(/{valor}/g, valorFormatado)
|
||||
.replace(/{quando}/g, quando)
|
||||
.replace(/{parcela}/g, `${parcela.numero_parcela}/${parcela.vendas?.parcelas || '?'}`);
|
||||
|
||||
// Se for no dia do vencimento, gerar PIX
|
||||
if (tipoAlerta === 'segundo_alerta' && mercadoPagoToken) {
|
||||
console.log(`🔄 Gerando PIX para parcela ${parcela.numero_parcela}...`);
|
||||
|
||||
const pixResult = await gerarPix(parcela, mercadoPagoToken);
|
||||
|
||||
if (pixResult.success && pixResult.qrCode) {
|
||||
mensagem += `\n\n📱 *PIX Copia e Cola:*\n\`\`\`${pixResult.qrCode}\`\`\``;
|
||||
|
||||
// Atualizar parcela com dados do PIX
|
||||
await supabase
|
||||
.from('venda_parcelas')
|
||||
.update({
|
||||
pix_payment_id: pixResult.paymentId,
|
||||
pix_qr_code: pixResult.qrCode,
|
||||
pix_qr_code_base64: pixResult.qrCodeBase64
|
||||
})
|
||||
.eq('id', parcela.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Enviar mensagem
|
||||
console.log(`📤 Enviando ${tipoAlerta} para ${cliente.nome_completo} (${cliente.whatsapp})...`);
|
||||
|
||||
const resultado = await enviarWhatsApp(cliente.whatsapp, mensagem, evolutionConfig);
|
||||
|
||||
if (resultado.success) {
|
||||
alertasEnviados++;
|
||||
console.log(` ✅ Enviado com sucesso!\n`);
|
||||
|
||||
// Registrar no histórico
|
||||
await supabase
|
||||
.from('mensagens_whatsapp')
|
||||
.insert({
|
||||
telefone_cliente: cliente.whatsapp,
|
||||
cliente_nome: cliente.nome_completo,
|
||||
mensagem: mensagem,
|
||||
tipo: 'enviada',
|
||||
status: 'enviada'
|
||||
});
|
||||
|
||||
} else {
|
||||
erros++;
|
||||
console.log(` ❌ Falha no envio: ${resultado.error}\n`);
|
||||
}
|
||||
|
||||
// Pequeno delay entre mensagens
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
// Resumo
|
||||
console.log('\n' + '='.repeat(50));
|
||||
console.log('📊 RESUMO DO ENVIO');
|
||||
console.log('='.repeat(50));
|
||||
console.log(`✅ Alertas enviados: ${alertasEnviados}`);
|
||||
console.log(`❌ Erros: ${erros}`);
|
||||
console.log(`📦 Total de parcelas verificadas: ${parcelas.length}`);
|
||||
console.log('='.repeat(50) + '\n');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n💥 Erro fatal:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Executar
|
||||
enviarAlertasVencimento()
|
||||
.then(() => {
|
||||
console.log('✅ Script finalizado com sucesso!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 Erro fatal:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🔧 Script de Correção Google Drive - Liberi Kids"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
echo "📋 Checklist de Configuração:"
|
||||
echo ""
|
||||
echo "1. ✅ Verificar se Google Drive API está ativa"
|
||||
echo " - Acesse: https://console.cloud.google.com/apis/library/drive.googleapis.com"
|
||||
echo " - Clique em 'Ativar' se não estiver ativo"
|
||||
echo ""
|
||||
|
||||
echo "2. ✅ Configurar Tela de Consentimento OAuth"
|
||||
echo " - Acesse: https://console.cloud.google.com/apis/credentials/consent"
|
||||
echo " - Tipo: Externo"
|
||||
echo " - Nome do app: Liberi Kids - Sistema de Estoque"
|
||||
echo " - Adicionar escopos: drive.file e drive.readonly"
|
||||
echo ""
|
||||
|
||||
echo "3. 🎯 CRÍTICO: Adicionar Usuários de Teste"
|
||||
echo " - Na tela de consentimento, seção 'Usuários de teste'"
|
||||
echo " - Adicionar SEU EMAIL (o mesmo que vai usar para autorizar)"
|
||||
echo " - Sem isso, você receberá erro de autorização"
|
||||
echo ""
|
||||
|
||||
echo "4. ✅ Verificar Credenciais OAuth"
|
||||
echo " - Acesse: https://console.cloud.google.com/apis/credentials"
|
||||
echo " - URIs de redirecionamento: http://localhost:5000/auth/google-drive/callback"
|
||||
echo ""
|
||||
|
||||
echo "5. 🔄 Testar Configuração"
|
||||
echo " - Recarregar página de Configurações"
|
||||
echo " - Tentar autorizar novamente"
|
||||
echo " - Usar o email que foi adicionado como usuário de teste"
|
||||
echo ""
|
||||
|
||||
echo "📞 URLs Importantes:"
|
||||
echo " - Google Cloud Console: https://console.cloud.google.com/"
|
||||
echo " - APIs & Services: https://console.cloud.google.com/apis/"
|
||||
echo " - OAuth Consent: https://console.cloud.google.com/apis/credentials/consent"
|
||||
echo " - Credentials: https://console.cloud.google.com/apis/credentials"
|
||||
echo ""
|
||||
|
||||
echo "🚨 Lembre-se: Use o MESMO EMAIL em 'Usuários de teste' e na autorização!"
|
||||
echo ""
|
||||
|
||||
# Verificar se o servidor está rodando
|
||||
if curl -s http://localhost:5000/api/google-drive/status > /dev/null; then
|
||||
echo "✅ Servidor está rodando"
|
||||
echo "🔗 Acesse: http://localhost:3000/configuracoes"
|
||||
else
|
||||
echo "❌ Servidor não está rodando"
|
||||
echo "Execute: npm start"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Após configurar no Google Cloud Console, recarregue a página e tente novamente!"
|
||||
33
scripts/garantir_tabela_configuracoes.sql
Normal file
33
scripts/garantir_tabela_configuracoes.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- Garante que a tabela configuracoes existe e tem a estrutura correta
|
||||
|
||||
-- Se a tabela já existe, vamos garantir que ela tenha a estrutura correta
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Criar tabela se não existir
|
||||
IF NOT EXISTS (SELECT FROM pg_tables WHERE schemaname = 'public' AND tablename = 'configuracoes') THEN
|
||||
CREATE TABLE public.configuracoes (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY,
|
||||
tipo character varying NOT NULL,
|
||||
valor jsonb,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- Garantir que a constraint UNIQUE existe
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'configuracoes_tipo_key'
|
||||
AND conrelid = 'public.configuracoes'::regclass
|
||||
) THEN
|
||||
ALTER TABLE public.configuracoes ADD CONSTRAINT configuracoes_tipo_key UNIQUE (tipo);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Adiciona índice no campo tipo para melhor performance (se não existir)
|
||||
CREATE INDEX IF NOT EXISTS idx_configuracoes_tipo ON public.configuracoes(tipo);
|
||||
|
||||
-- Adiciona comentários
|
||||
COMMENT ON TABLE public.configuracoes IS 'Armazena todas as configurações do sistema';
|
||||
COMMENT ON COLUMN public.configuracoes.tipo IS 'Tipo da configuração (chave única)';
|
||||
COMMENT ON COLUMN public.configuracoes.valor IS 'Valor da configuração em formato JSON';
|
||||
44
scripts/garantir_tabela_mensagens.sql
Normal file
44
scripts/garantir_tabela_mensagens.sql
Normal file
@@ -0,0 +1,44 @@
|
||||
-- Garante que a tabela mensagens_whatsapp exista e tenha as colunas corretas.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.mensagens_whatsapp (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
telefone_cliente character varying,
|
||||
cliente_nome character varying,
|
||||
mensagem text,
|
||||
tipo character varying DEFAULT 'enviada'::character varying,
|
||||
status character varying DEFAULT 'enviada'::character varying,
|
||||
evolution_message_id character varying,
|
||||
venda_id uuid,
|
||||
cobranca_id uuid
|
||||
);
|
||||
|
||||
-- Adiciona colunas que podem estar faltando, sem gerar erro se já existirem.
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.mensagens_whatsapp'::regclass AND attname = 'venda_id') THEN
|
||||
ALTER TABLE public.mensagens_whatsapp ADD COLUMN venda_id uuid;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.mensagens_whatsapp'::regclass AND attname = 'cobranca_id') THEN
|
||||
ALTER TABLE public.mensagens_whatsapp ADD COLUMN cobranca_id uuid;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.mensagens_whatsapp'::regclass AND attname = 'cliente_nome') THEN
|
||||
ALTER TABLE public.mensagens_whatsapp ADD COLUMN cliente_nome character varying;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.mensagens_whatsapp'::regclass AND attname = 'evolution_message_id') THEN
|
||||
ALTER TABLE public.mensagens_whatsapp ADD COLUMN evolution_message_id character varying;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Adiciona relacionamento com a tabela de vendas, se não existir.
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'mensagens_whatsapp_venda_id_fkey') THEN
|
||||
ALTER TABLE public.mensagens_whatsapp ADD CONSTRAINT mensagens_whatsapp_venda_id_fkey FOREIGN KEY (venda_id) REFERENCES public.vendas(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON TABLE public.mensagens_whatsapp IS 'Armazena o histórico de mensagens enviadas e recebidas pelo WhatsApp.';
|
||||
147
scripts/instalar-cron-alertas.sh
Normal file
147
scripts/instalar-cron-alertas.sh
Normal file
@@ -0,0 +1,147 @@
|
||||
#!/bin/bash
|
||||
|
||||
##############################################
|
||||
# Instalador de Cron Job para Alertas de Vencimento
|
||||
#
|
||||
# Este script configura o cron para executar
|
||||
# alertas automáticos às 09:00 (horário de Brasília)
|
||||
##############################################
|
||||
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo " INSTALADOR CRON - ALERTAS PARCELAS"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Cores
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Detectar diretório do projeto
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
NODE_SCRIPT="$PROJECT_DIR/scripts/enviar-alertas-parcelas.js"
|
||||
|
||||
echo "📁 Diretório do projeto: $PROJECT_DIR"
|
||||
echo "📜 Script de alertas: $NODE_SCRIPT"
|
||||
echo ""
|
||||
|
||||
# Verificar se o script existe
|
||||
if [ ! -f "$NODE_SCRIPT" ]; then
|
||||
echo -e "${RED}❌ Script de alertas não encontrado!${NC}"
|
||||
echo " Caminho esperado: $NODE_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Tornar o script executável
|
||||
chmod +x "$NODE_SCRIPT"
|
||||
echo -e "${GREEN}✅ Permissões de execução configuradas${NC}"
|
||||
|
||||
# Detectar path do Node.js
|
||||
NODE_PATH=$(which node)
|
||||
if [ -z "$NODE_PATH" ]; then
|
||||
echo -e "${RED}❌ Node.js não encontrado!${NC}"
|
||||
echo " Instale o Node.js primeiro: https://nodejs.org"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Node.js encontrado: $NODE_PATH${NC}"
|
||||
|
||||
# Criar linha do cron
|
||||
# Executa todos os dias às 09:00 (horário de Brasília - UTC-3)
|
||||
# Nota: Cron usa UTC, então 09:00 BRT = 12:00 UTC
|
||||
CRON_LINE="0 12 * * * TZ='America/Sao_Paulo' $NODE_PATH $NODE_SCRIPT >> $PROJECT_DIR/logs/alertas-cron.log 2>&1"
|
||||
|
||||
echo ""
|
||||
echo "🕐 Configuração do cron:"
|
||||
echo " - Horário: 09:00 (Brasília)"
|
||||
echo " - Frequência: Diariamente"
|
||||
echo " - Timezone: America/Sao_Paulo"
|
||||
echo ""
|
||||
|
||||
# Criar diretório de logs
|
||||
mkdir -p "$PROJECT_DIR/logs"
|
||||
echo -e "${GREEN}✅ Diretório de logs criado${NC}"
|
||||
|
||||
# Verificar se já existe o cron
|
||||
EXISTING_CRON=$(crontab -l 2>/dev/null | grep -F "$NODE_SCRIPT" || true)
|
||||
|
||||
if [ -n "$EXISTING_CRON" ]; then
|
||||
echo -e "${YELLOW}⚠️ Já existe um cron job configurado para este script${NC}"
|
||||
echo ""
|
||||
echo "Cron atual:"
|
||||
echo " $EXISTING_CRON"
|
||||
echo ""
|
||||
read -p "Deseja substituir? (s/N): " -n 1 -r
|
||||
echo ""
|
||||
|
||||
if [[ ! $REPLY =~ ^[Ss]$ ]]; then
|
||||
echo "❌ Instalação cancelada pelo usuário"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Remover linha antiga
|
||||
(crontab -l 2>/dev/null | grep -v -F "$NODE_SCRIPT") | crontab -
|
||||
echo -e "${GREEN}✅ Cron antigo removido${NC}"
|
||||
fi
|
||||
|
||||
# Adicionar novo cron
|
||||
(crontab -l 2>/dev/null; echo "$CRON_LINE") | crontab -
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Cron job instalado com sucesso!${NC}"
|
||||
echo ""
|
||||
|
||||
# Verificar instalação
|
||||
echo "📋 Cron jobs ativos:"
|
||||
echo "────────────────────────────────────────"
|
||||
crontab -l | grep -F "$NODE_SCRIPT" || echo "Nenhum cron encontrado"
|
||||
echo "────────────────────────────────────────"
|
||||
echo ""
|
||||
|
||||
# Teste manual
|
||||
echo "🧪 Deseja executar um teste agora? (s/N): "
|
||||
read -p "" -n 1 -r
|
||||
echo ""
|
||||
|
||||
if [[ $REPLY =~ ^[Ss]$ ]]; then
|
||||
echo ""
|
||||
echo "🚀 Executando teste..."
|
||||
echo "────────────────────────────────────────"
|
||||
TZ='America/Sao_Paulo' $NODE_PATH $NODE_SCRIPT
|
||||
echo "────────────────────────────────────────"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " INSTALAÇÃO CONCLUÍDA! ✅"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "📝 Próximos passos:"
|
||||
echo ""
|
||||
echo "1. Configure as variáveis de ambiente em .env:"
|
||||
echo " - SUPABASE_URL"
|
||||
echo " - SUPABASE_SERVICE_KEY (ou SUPABASE_ANON_KEY)"
|
||||
echo ""
|
||||
echo "2. Configure no painel admin:"
|
||||
echo " - Evolution API (URL, Instance, API Key)"
|
||||
echo " - Mercado Pago (Access Token)"
|
||||
echo " - Mensagens de alerta personalizadas"
|
||||
echo " - Dias de antecedência para alertas"
|
||||
echo ""
|
||||
echo "3. Monitore os logs:"
|
||||
echo " tail -f $PROJECT_DIR/logs/alertas-cron.log"
|
||||
echo ""
|
||||
echo "4. Para testar manualmente:"
|
||||
echo " cd $PROJECT_DIR"
|
||||
echo " node scripts/enviar-alertas-parcelas.js"
|
||||
echo ""
|
||||
echo "5. Para desinstalar o cron:"
|
||||
echo " crontab -e (e remova a linha do script)"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
82
scripts/install-systemd.sh
Executable file
82
scripts/install-systemd.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 🔧 Script de Instalação do Serviço Systemd - Liberi Kids
|
||||
|
||||
set -e
|
||||
|
||||
# Cores
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${GREEN}🔧 Instalação do Serviço Systemd${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
|
||||
# Verificar se está rodando como root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}❌ Este script precisa ser executado como root${NC}"
|
||||
echo -e "Execute: ${BLUE}sudo ./scripts/install-systemd.sh${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. Criar diretórios necessários
|
||||
echo -e "${YELLOW}📁 Criando diretórios...${NC}"
|
||||
mkdir -p logs
|
||||
chown -R tiago:tiago logs
|
||||
echo -e "${GREEN}✅ Diretórios criados${NC}"
|
||||
echo ""
|
||||
|
||||
# 2. Copiar arquivo de serviço
|
||||
echo -e "${YELLOW}📋 Instalando serviço systemd...${NC}"
|
||||
cp liberi-kids.service /etc/systemd/system/
|
||||
echo -e "${GREEN}✅ Arquivo copiado para /etc/systemd/system/${NC}"
|
||||
echo ""
|
||||
|
||||
# 3. Recarregar systemd
|
||||
echo -e "${YELLOW}🔄 Recarregando systemd...${NC}"
|
||||
systemctl daemon-reload
|
||||
echo -e "${GREEN}✅ Systemd recarregado${NC}"
|
||||
echo ""
|
||||
|
||||
# 4. Habilitar serviço
|
||||
echo -e "${YELLOW}✅ Habilitando inicialização automática...${NC}"
|
||||
systemctl enable liberi-kids
|
||||
echo -e "${GREEN}✅ Serviço habilitado para iniciar no boot${NC}"
|
||||
echo ""
|
||||
|
||||
# 5. Iniciar serviço
|
||||
echo -e "${YELLOW}🚀 Iniciando serviço...${NC}"
|
||||
systemctl start liberi-kids
|
||||
sleep 2
|
||||
echo -e "${GREEN}✅ Serviço iniciado${NC}"
|
||||
echo ""
|
||||
|
||||
# 6. Verificar status
|
||||
echo -e "${YELLOW}📊 Status do serviço:${NC}"
|
||||
systemctl status liberi-kids --no-pager
|
||||
echo ""
|
||||
|
||||
# 7. Informações finais
|
||||
PORT=$(grep "^PORT=" .env | cut -d '=' -f2 2>/dev/null || echo "5000")
|
||||
PORT=${PORT:-5000}
|
||||
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${GREEN}✅ Instalação concluída!${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}📍 Aplicação rodando em:${NC}"
|
||||
echo -e " ${BLUE}http://localhost:${PORT}${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}📊 Comandos úteis:${NC}"
|
||||
echo -e " ${BLUE}sudo systemctl status liberi-kids${NC} - Ver status"
|
||||
echo -e " ${BLUE}sudo systemctl restart liberi-kids${NC} - Reiniciar"
|
||||
echo -e " ${BLUE}sudo systemctl stop liberi-kids${NC} - Parar"
|
||||
echo -e " ${BLUE}sudo systemctl start liberi-kids${NC} - Iniciar"
|
||||
echo -e " ${BLUE}sudo journalctl -u liberi-kids -f${NC} - Ver logs"
|
||||
echo ""
|
||||
echo -e "${GREEN}🔄 O serviço irá iniciar automaticamente no boot!${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
43
scripts/limpar_duplicados_configuracoes.sql
Normal file
43
scripts/limpar_duplicados_configuracoes.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
-- Limpar registros duplicados na tabela configuracoes antes de adicionar constraint UNIQUE
|
||||
|
||||
-- 1. Ver quais registros estão duplicados
|
||||
SELECT tipo, COUNT(*) as total
|
||||
FROM public.configuracoes
|
||||
GROUP BY tipo
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
-- 2. Remover duplicados, mantendo apenas o mais recente de cada tipo
|
||||
DELETE FROM public.configuracoes
|
||||
WHERE id IN (
|
||||
SELECT id
|
||||
FROM (
|
||||
SELECT id, tipo,
|
||||
ROW_NUMBER() OVER (PARTITION BY tipo ORDER BY updated_at DESC, created_at DESC) as rn
|
||||
FROM public.configuracoes
|
||||
) t
|
||||
WHERE rn > 1
|
||||
);
|
||||
|
||||
-- 3. Verificar se ainda existem duplicados (deve retornar vazio)
|
||||
SELECT tipo, COUNT(*) as total
|
||||
FROM public.configuracoes
|
||||
GROUP BY tipo
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
-- 4. Agora adicionar a constraint UNIQUE
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'configuracoes_tipo_key'
|
||||
AND conrelid = 'public.configuracoes'::regclass
|
||||
) THEN
|
||||
ALTER TABLE public.configuracoes ADD CONSTRAINT configuracoes_tipo_key UNIQUE (tipo);
|
||||
RAISE NOTICE 'Constraint UNIQUE adicionada com sucesso!';
|
||||
ELSE
|
||||
RAISE NOTICE 'Constraint UNIQUE já existe.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 5. Verificar o resultado final
|
||||
SELECT * FROM public.configuracoes ORDER BY tipo;
|
||||
Reference in New Issue
Block a user