DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_policies ...) THEN CREATE POLICY ... END IF; END $$;. Isso significa que você pode rodar 1, 10 ou 100 vezes a consulta no Supabase que nunca mais dará erro 42710 (policy already exists)!👉 Clique no botão verde "Copiar Código Limpo" em cada parte. O código puro vai para o seu Ctrl+V.Tudo que você precisa para o sistema ficar 100% funcional. Os códigos estão completos abaixo — é só copiar e colar.
Se você já tem o projeto, pule para o passo 2.
src/lib/supabase.ts com o novo URL e a nova chave.No painel do Supabase → SQL Editor → New query → cole as 3 partes abaixo (uma de cada vez ou todas juntas) → clique RUN.
-- ==========================================
-- SCHEMA PROFISSIONAL SUPABASE - MSK UNIFORMES
-- ==========================================
-- Execute este código completo no SQL Editor do Supabase
CREATE TABLE IF NOT EXISTS msk_produtos_estoque (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome TEXT NOT NULL,
sku TEXT UNIQUE,
categoria TEXT,
tamanhos TEXT,
cor TEXT,
tecido TEXT,
quantidade INTEGER DEFAULT 0,
preco DECIMAL(10,2) DEFAULT 0,
preco_custo DECIMAL(10,2) DEFAULT 0,
loja TEXT DEFAULT 'Fábrica',
imagem_url TEXT,
descricao TEXT,
is_kit BOOLEAN DEFAULT false,
kit_quantidade INTEGER DEFAULT 1,
med_comprimento DECIMAL(8,1),
med_cintura DECIMAL(8,1),
med_quadril DECIMAL(8,1),
med_colarinho DECIMAL(8,1),
med_manga DECIMAL(8,1),
peso_kit DECIMAL(8,1),
tipo_tecido TEXT,
composicao_tecido TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS descricao TEXT;
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS is_kit BOOLEAN DEFAULT false;
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS kit_quantidade INTEGER DEFAULT 1;
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS med_comprimento DECIMAL(8,1);
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS med_cintura DECIMAL(8,1);
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS med_quadril DECIMAL(8,1);
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS med_colarinho DECIMAL(8,1);
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS med_manga DECIMAL(8,1);
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS peso_kit DECIMAL(8,1);
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS tipo_tecido TEXT;
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS composicao_tecido TEXT;
CREATE TABLE IF NOT EXISTS msk_estoque_movimentacoes (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
produto_id UUID REFERENCES msk_produtos_estoque(id) ON DELETE CASCADE,
tipo TEXT NOT NULL,
quantidade INTEGER NOT NULL,
motivo TEXT,
loja TEXT,
responsavel TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE IF NOT EXISTS msk_transferencias_estoque (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
produto_id UUID REFERENCES msk_produtos_estoque(id),
sku TEXT,
nome_produto TEXT,
origem TEXT,
destino TEXT,
quantidade INTEGER,
status TEXT DEFAULT 'pendente',
solicitante TEXT,
recebido_por TEXT,
recebido_em TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE IF NOT EXISTS msk_notificacoes_admin (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
titulo TEXT,
mensagem TEXT,
loja_destino TEXT,
tipo TEXT,
lida BOOLEAN DEFAULT false,
link TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_produtos_estoque ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_produtos_estoque' AND policyname = 'allow_all_msk_produtos_estoque') THEN
CREATE POLICY "allow_all_msk_produtos_estoque" ON msk_produtos_estoque FOR ALL USING (true);
END IF;
END $$;
ALTER TABLE msk_estoque_movimentacoes ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_estoque_movimentacoes' AND policyname = 'allow_all_msk_estoque_movimentacoes') THEN
CREATE POLICY "allow_all_msk_estoque_movimentacoes" ON msk_estoque_movimentacoes FOR ALL USING (true);
END IF;
END $$;
ALTER TABLE msk_transferencias_estoque ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_transferencias_estoque' AND policyname = 'allow_all_msk_transferencias') THEN
CREATE POLICY "allow_all_msk_transferencias" ON msk_transferencias_estoque FOR ALL USING (true);
END IF;
END $$;
ALTER TABLE msk_notificacoes_admin ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_notificacoes_admin' AND policyname = 'allow_all_msk_notificacoes') THEN
CREATE POLICY "allow_all_msk_notificacoes" ON msk_notificacoes_admin FOR ALL USING (true);
END IF;
END $$;
CREATE TABLE IF NOT EXISTS msk_conferencia_fotografica (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
op_id UUID,
op_numero TEXT,
produto_nome TEXT,
quantidade_esperada INTEGER DEFAULT 0,
quantidade_conferida INTEGER DEFAULT 0,
conferente TEXT,
fotos JSONB DEFAULT '[]',
status TEXT DEFAULT 'pendente',
observacao TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_conferencia_fotografica ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_msk_conferencia" ON msk_conferencia_fotografica;
CREATE POLICY "allow_all_msk_conferencia" ON msk_conferencia_fotografica FOR ALL USING (true);
CREATE TABLE IF NOT EXISTS categorias (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
descricao TEXT,
imagem_url TEXT
);
CREATE TABLE IF NOT EXISTS parceiros (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome TEXT NOT NULL,
tipo TEXT NOT NULL,
especialidade TEXT,
contato TEXT,
cidade TEXT,
cnpj TEXT,
avaliacao DECIMAL DEFAULT 5.0,
ativo BOOLEAN DEFAULT true,
valor_servico DECIMAL(10,2) DEFAULT 0,
forma_pagamento TEXT DEFAULT 'Pix',
prazo_entrega TEXT,
observacao TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
);
ALTER TABLE parceiros ADD COLUMN IF NOT EXISTS especialidade TEXT;
ALTER TABLE parceiros ADD COLUMN IF NOT EXISTS cnpj TEXT;
ALTER TABLE parceiros ADD COLUMN IF NOT EXISTS valor_servico DECIMAL(10,2) DEFAULT 0;
ALTER TABLE parceiros ADD COLUMN IF NOT EXISTS forma_pagamento TEXT DEFAULT 'Pix';
ALTER TABLE parceiros ADD COLUMN IF NOT EXISTS prazo_entrega TEXT;
ALTER TABLE parceiros ADD COLUMN IF NOT EXISTS observacao TEXT;
CREATE TABLE IF NOT EXISTS produtos (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
sku TEXT UNIQUE NOT NULL,
nome TEXT NOT NULL,
descricao TEXT,
categoria_id UUID REFERENCES categorias(id),
tipo_peca TEXT,
material TEXT,
preco_venda DECIMAL(10,2),
preco_custo DECIMAL(10,2),
tamanhos_disponiveis JSONB,
imagens JSONB,
qr_video_url TEXT,
fornecedor_id UUID REFERENCES parceiros(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
);
CREATE TABLE IF NOT EXISTS estoque (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
produto_id UUID REFERENCES produtos(id) ON DELETE CASCADE,
tamanho TEXT NOT NULL,
localizacao TEXT NOT NULL,
quantidade INTEGER DEFAULT 0,
ultima_atualizacao TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()),
UNIQUE(produto_id, tamanho, localizacao)
);
CREATE TABLE IF NOT EXISTS escolas (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
logo_url TEXT,
cor_primaria TEXT,
percentual_royalties DECIMAL(5,2) DEFAULT 10.00,
email_financeiro TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
);
CREATE TABLE IF NOT EXISTS produtos_escola (
escola_id UUID REFERENCES escolas(id) ON DELETE CASCADE,
produto_id UUID REFERENCES produtos(id) ON DELETE CASCADE,
preco_especial DECIMAL(10,2),
PRIMARY KEY (escola_id, produto_id)
);
CREATE TABLE IF NOT EXISTS ordens_producao (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
numero_op SERIAL,
produto_id UUID REFERENCES produtos(id),
produto_nome TEXT,
cliente TEXT,
prioridade TEXT DEFAULT 'Normal',
responsavel TEXT,
quantidade INTEGER NOT NULL,
tamanho TEXT,
status TEXT DEFAULT 'CORTE',
parceiro_id UUID REFERENCES parceiros(id),
data_inicio TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()),
data_previsao TIMESTAMP WITH TIME ZONE,
data_conclusao TIMESTAMP WITH TIME ZONE,
observacoes TEXT,
anexos TEXT,
comentarios JSONB DEFAULT '[]'::jsonb
);
CREATE TABLE IF NOT EXISTS avaliacoes_biometricas (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome_cliente TEXT NOT NULL,
idade INTEGER,
altura DECIMAL,
peso DECIMAL,
produto_id UUID REFERENCES produtos(id),
tamanho_recomendado TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
);
CREATE TABLE IF NOT EXISTS transacoes_financeiras (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
escola_id UUID REFERENCES escolas(id),
produto_id UUID REFERENCES produtos(id),
valor_total DECIMAL(10,2) NOT NULL,
valor_repasse DECIMAL(10,2) NOT NULL,
metodo_pagamento TEXT,
status_repasse TEXT DEFAULT 'AGUARDANDO',
data_venda TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
);
ALTER TABLE categorias ENABLE ROW LEVEL SECURITY;
ALTER TABLE parceiros ENABLE ROW LEVEL SECURITY;
ALTER TABLE produtos ENABLE ROW LEVEL SECURITY;
ALTER TABLE estoque ENABLE ROW LEVEL SECURITY;
ALTER TABLE escolas ENABLE ROW LEVEL SECURITY;
ALTER TABLE produtos_escola ENABLE ROW LEVEL SECURITY;
ALTER TABLE ordens_producao ENABLE ROW LEVEL SECURITY;
ALTER TABLE avaliacoes_biometricas ENABLE ROW LEVEL SECURITY;
ALTER TABLE transacoes_financeiras ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'categorias' AND policyname = 'Leitura publica de categorias') THEN
CREATE POLICY "Leitura publica de categorias" ON categorias FOR SELECT USING (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'produtos' AND policyname = 'Leitura publica de produtos') THEN
CREATE POLICY "Leitura publica de produtos" ON produtos FOR SELECT USING (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'estoque' AND policyname = 'Leitura publica de estoque') THEN
CREATE POLICY "Leitura publica de estoque" ON estoque FOR SELECT USING (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'escolas' AND policyname = 'Leitura publica de escolas') THEN
CREATE POLICY "Leitura publica de escolas" ON escolas FOR SELECT USING (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'produtos_escola' AND policyname = 'Leitura publica de produtos_escola') THEN
CREATE POLICY "Leitura publica de produtos_escola" ON produtos_escola FOR SELECT USING (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'avaliacoes_biometricas' AND policyname = 'Insercao publica no provador') THEN
CREATE POLICY "Insercao publica no provador" ON avaliacoes_biometricas FOR INSERT WITH CHECK (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'parceiros' AND policyname = 'Acesso total admin parceiros') THEN
CREATE POLICY "Acesso total admin parceiros" ON parceiros FOR ALL USING (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'ordens_producao' AND policyname = 'Acesso total admin OPs') THEN
CREATE POLICY "Acesso total admin OPs" ON ordens_producao FOR ALL USING (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'transacoes_financeiras' AND policyname = 'Acesso total admin financeiro') THEN
CREATE POLICY "Acesso total admin financeiro" ON transacoes_financeiras FOR ALL USING (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'produtos' AND policyname = 'Acesso total admin produtos') THEN
CREATE POLICY "Acesso total admin produtos" ON produtos FOR ALL USING (true);
END IF;
END $$;-- 10. CENTRO DE CUSTOS
CREATE TABLE IF NOT EXISTS msk_centro_custos (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
data DATE NOT NULL,
descricao TEXT NOT NULL,
categoria TEXT NOT NULL,
subcategoria TEXT,
forma_pagamento TEXT NOT NULL,
valor NUMERIC(12,2) NOT NULL,
responsavel TEXT NOT NULL,
loja TEXT DEFAULT 'Santos',
fornecedor TEXT,
nota_fiscal TEXT,
observacao TEXT,
aprovado_por TEXT,
status TEXT DEFAULT 'pendente',
comprovante_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
-- 11. PRECIFICACAO DE PRODUCAO
CREATE TABLE IF NOT EXISTS msk_precificacao (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome_produto TEXT NOT NULL,
tipo_peca TEXT NOT NULL,
segmento TEXT NOT NULL,
unidade TEXT DEFAULT 'Santos',
tecido_nome TEXT,
tecido_gramatura NUMERIC(6,2),
tecido_consumo_metros NUMERIC(6,3),
tecido_preco_metro NUMERIC(10,2),
aviamentos_valor NUMERIC(10,2) DEFAULT 0,
embalagem_valor NUMERIC(10,2) DEFAULT 0,
etiqueta_valor NUMERIC(10,2) DEFAULT 0,
tempo_corte_min NUMERIC(6,2) DEFAULT 0,
tempo_costura_min NUMERIC(6,2) DEFAULT 0,
tempo_acabamento_min NUMERIC(6,2) DEFAULT 0,
custo_hora_corte NUMERIC(10,2) DEFAULT 25,
custo_hora_costura NUMERIC(10,2) DEFAULT 30,
custo_hora_acabamento NUMERIC(10,2) DEFAULT 20,
bordado_valor NUMERIC(10,2) DEFAULT 0,
silk_valor NUMERIC(10,2) DEFAULT 0,
dtf_valor NUMERIC(10,2) DEFAULT 0,
sublimacao_valor NUMERIC(10,2) DEFAULT 0,
overhead_pct NUMERIC(5,2) DEFAULT 15,
comissao_pct NUMERIC(5,2) DEFAULT 5,
impostos_pct NUMERIC(5,2) DEFAULT 6,
marketplace_pct NUMERIC(5,2) DEFAULT 0,
frete_valor NUMERIC(10,2) DEFAULT 0,
motoboy_valor NUMERIC(10,2) DEFAULT 0,
transporte_valor NUMERIC(10,2) DEFAULT 0,
terceirizacao_valor NUMERIC(10,2) DEFAULT 0,
linha_valor NUMERIC(10,2) DEFAULT 0,
botoes_valor NUMERIC(10,2) DEFAULT 0,
ziper_valor NUMERIC(10,2) DEFAULT 0,
insumos JSONB,
insumos_total NUMERIC(12,2) DEFAULT 0,
margem_lucro_pct NUMERIC(5,2) DEFAULT 30,
preco_venda_sugerido NUMERIC(10,2),
preco_venda_praticado NUMERIC(10,2),
observacao TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS motoboy_valor NUMERIC(10,2) DEFAULT 0;
ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS transporte_valor NUMERIC(10,2) DEFAULT 0;
ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS terceirizacao_valor NUMERIC(10,2) DEFAULT 0;
ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS linha_valor NUMERIC(10,2) DEFAULT 0;
ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS botoes_valor NUMERIC(10,2) DEFAULT 0;
ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS ziper_valor NUMERIC(10,2) DEFAULT 0;
ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS insumos JSONB;
ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS insumos_total NUMERIC(12,2) DEFAULT 0;
CREATE TABLE IF NOT EXISTS msk_precificacao_insumos (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
precificacao_id UUID REFERENCES msk_precificacao(id) ON DELETE CASCADE,
materia_prima_id UUID REFERENCES msk_materia_prima(id) ON DELETE SET NULL,
nome_insumo TEXT,
categoria TEXT,
quantidade NUMERIC(12,3) NOT NULL DEFAULT 1,
unidade_medida TEXT,
preco_unitario NUMERIC(12,2) NOT NULL DEFAULT 0,
custo_total NUMERIC(12,2) NOT NULL DEFAULT 0,
ordem INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_centro_custos ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_centro_custos' AND policyname = 'allow_all_centro_custos') THEN
CREATE POLICY "allow_all_centro_custos" ON msk_centro_custos FOR ALL USING (true);
END IF;
END $$;
ALTER TABLE msk_precificacao ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_precificacao' AND policyname = 'allow_all_precificacao') THEN
CREATE POLICY "allow_all_precificacao" ON msk_precificacao FOR ALL USING (true);
END IF;
END $$;
ALTER TABLE msk_precificacao_insumos ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_precificacao_insumos' AND policyname = 'allow_all_precificacao_insumos') THEN
CREATE POLICY "allow_all_precificacao_insumos" ON msk_precificacao_insumos FOR ALL USING (true);
END IF;
END $$;
-- MATERIA-PRIMA MSK
CREATE TABLE IF NOT EXISTS msk_materia_prima (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
codigo TEXT UNIQUE NOT NULL,
nome TEXT NOT NULL,
categoria TEXT NOT NULL,
subcategoria TEXT,
cor TEXT,
unidade_medida TEXT NOT NULL DEFAULT 'kg',
quantidade_atual NUMERIC(12,3) NOT NULL DEFAULT 0,
quantidade_minima NUMERIC(12,3) DEFAULT 0,
preco_custo NUMERIC(12,2),
fornecedor TEXT,
localizacao TEXT DEFAULT 'Fabrica',
status TEXT DEFAULT 'ativo',
observacao TEXT,
lote TEXT,
data_entrada DATE,
imagem_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE IF NOT EXISTS msk_materia_prima_movimentacoes (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
materia_prima_id UUID REFERENCES msk_materia_prima(id) ON DELETE CASCADE,
tipo TEXT NOT NULL,
quantidade NUMERIC(12,3) NOT NULL,
unidade_medida TEXT,
motivo TEXT,
ordem_producao TEXT,
responsavel TEXT,
custo_total NUMERIC(12,2),
observacao TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_materia_prima ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_materia_prima' AND policyname = 'allow_all_materia_prima') THEN
CREATE POLICY "allow_all_materia_prima" ON msk_materia_prima FOR ALL USING (true);
END IF;
END $$;
ALTER TABLE msk_materia_prima_movimentacoes ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_materia_prima_movimentacoes' AND policyname = 'allow_all_materia_prima_mov') THEN
CREATE POLICY "allow_all_materia_prima_mov" ON msk_materia_prima_movimentacoes FOR ALL USING (true);
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_materia_prima_categoria ON msk_materia_prima(categoria);
CREATE INDEX IF NOT EXISTS idx_materia_prima_localizacao ON msk_materia_prima(localizacao);
-- PATRIMONIO MSK
CREATE TABLE IF NOT EXISTS msk_patrimonio (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
codigo TEXT UNIQUE NOT NULL,
nome TEXT NOT NULL,
categoria TEXT NOT NULL,
marca TEXT,
modelo TEXT,
numero_serie TEXT,
ano_aquisicao INTEGER,
data_aquisicao DATE,
valor_aquisicao NUMERIC(12,2),
valor_atual NUMERIC(12,2),
fornecedor TEXT,
localizacao TEXT DEFAULT 'Fabrica',
setor TEXT,
responsavel TEXT,
status TEXT DEFAULT 'ativo',
condicao TEXT DEFAULT 'bom',
data_ultima_manutencao DATE,
data_proxima_manutencao DATE,
vida_util_anos INTEGER DEFAULT 5,
depreciacao_anual NUMERIC(5,2) DEFAULT 20,
observacao TEXT,
imagem_url TEXT,
nota_fiscal TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE IF NOT EXISTS msk_patrimonio_manutencao (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
patrimonio_id UUID REFERENCES msk_patrimonio(id) ON DELETE CASCADE,
tipo TEXT NOT NULL,
descricao TEXT NOT NULL,
custo NUMERIC(12,2),
responsavel TEXT,
fornecedor_servico TEXT,
data_realizacao DATE,
proxima_manutencao DATE,
observacao TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_patrimonio ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_patrimonio' AND policyname = 'allow_all_patrimonio') THEN
CREATE POLICY "allow_all_patrimonio" ON msk_patrimonio FOR ALL USING (true);
END IF;
END $$;
ALTER TABLE msk_patrimonio_manutencao ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_patrimonio_manutencao' AND policyname = 'allow_all_patrimonio_manut') THEN
CREATE POLICY "allow_all_patrimonio_manut" ON msk_patrimonio_manutencao FOR ALL USING (true);
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_patrimonio_categoria ON msk_patrimonio(categoria);
CREATE INDEX IF NOT EXISTS idx_patrimonio_status ON msk_patrimonio(status);
CREATE INDEX IF NOT EXISTS idx_patrimonio_localizacao ON msk_patrimonio(localizacao);
-- AUTOMACOES MSK
CREATE TABLE IF NOT EXISTS msk_automacoes_logs (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
automacao_nome TEXT NOT NULL,
descricao TEXT,
status TEXT DEFAULT 'sucesso',
tempo_economizado_min INTEGER DEFAULT 0,
detalhes JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE IF NOT EXISTS msk_automacoes_templates (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome TEXT NOT NULL UNIQUE,
tipo TEXT NOT NULL,
assunto TEXT,
corpo TEXT NOT NULL,
variaveis JSONB,
ativo BOOLEAN DEFAULT true,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_automacoes_logs ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_automacoes_logs' AND policyname = 'allow_all_automacoes_logs') THEN
CREATE POLICY "allow_all_automacoes_logs" ON msk_automacoes_logs FOR ALL USING (true);
END IF;
END $$;
ALTER TABLE msk_automacoes_templates ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_automacoes_templates' AND policyname = 'allow_all_automacoes_templates') THEN
CREATE POLICY "allow_all_automacoes_templates" ON msk_automacoes_templates FOR ALL USING (true);
END IF;
END $$;-- 12. CONSENTIMENTOS DE FOTOS (LGPD)
CREATE TABLE IF NOT EXISTS consentimentos_fotos (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome_responsavel TEXT,
nome_crianca TEXT,
numero_pedido TEXT,
ip_address TEXT,
data_hora TIMESTAMP WITH TIME ZONE DEFAULT now(),
status_exclusao TEXT DEFAULT 'pendente',
data_exclusao_prevista TIMESTAMP WITH TIME ZONE DEFAULT (now() + interval '48 hours')
);
ALTER TABLE consentimentos_fotos ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'consentimentos_fotos' AND policyname = 'allow_public_insert_consentimentos') THEN
CREATE POLICY "allow_public_insert_consentimentos" ON consentimentos_fotos FOR INSERT WITH CHECK (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'consentimentos_fotos' AND policyname = 'allow_admin_select_consentimentos') THEN
CREATE POLICY "allow_admin_select_consentimentos" ON consentimentos_fotos FOR SELECT USING (true);
END IF;
END $$;
-- 13. HISTORICO DE PRECOS
CREATE TABLE IF NOT EXISTS msk_historico_precos (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
produto_id UUID REFERENCES msk_produtos_estoque(id) ON DELETE CASCADE,
preco_antigo DECIMAL(10,2),
preco_novo DECIMAL(10,2),
custo_antigo DECIMAL(10,2),
custo_novo DECIMAL(10,2),
motivo TEXT,
responsavel TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
-- 14. ORDENS DE COMPRA
CREATE TABLE IF NOT EXISTS msk_ordens_compra (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
numero_pedido TEXT,
fornecedor TEXT,
itens JSONB,
valor_total DECIMAL(10,2),
status TEXT DEFAULT 'pendente',
data_pedido TIMESTAMP WITH TIME ZONE DEFAULT now(),
data_prevista TIMESTAMP WITH TIME ZONE,
observacoes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_historico_precos ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_historico_precos' AND policyname = 'allow_all_historico_precos') THEN
CREATE POLICY "allow_all_historico_precos" ON msk_historico_precos FOR ALL USING (true);
END IF;
END $$;
ALTER TABLE msk_ordens_compra ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_ordens_compra' AND policyname = 'allow_all_ordens_compra') THEN
CREATE POLICY "allow_all_ordens_compra" ON msk_ordens_compra FOR ALL USING (true);
END IF;
END $$;
-- 15. EDITOR VISUAL RECURSOS
CREATE TABLE IF NOT EXISTS msk_editor_logos (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome TEXT,
url TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE IF NOT EXISTS msk_editor_templates (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome TEXT NOT NULL,
configuracao JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE IF NOT EXISTS msk_editor_texturas (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome TEXT NOT NULL,
url TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_editor_logos ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_editor_logos' AND policyname = 'allow_all_editor_logos') THEN
CREATE POLICY "allow_all_editor_logos" ON msk_editor_logos FOR ALL USING (true);
END IF;
END $$;
ALTER TABLE msk_editor_templates ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_editor_templates' AND policyname = 'allow_all_editor_templates') THEN
CREATE POLICY "allow_all_editor_templates" ON msk_editor_templates FOR ALL USING (true);
END IF;
END $$;
ALTER TABLE msk_editor_texturas ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_editor_texturas' AND policyname = 'allow_all_editor_texturas') THEN
CREATE POLICY "allow_all_editor_texturas" ON msk_editor_texturas FOR ALL USING (true);
END IF;
END $$;
-- 16. MOCKUPS PNG (CorelDRAW)
CREATE TABLE IF NOT EXISTS msk_editor_mockups (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
nome TEXT NOT NULL,
url TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_editor_mockups ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_editor_mockups' AND policyname = 'allow_all_editor_mockups') THEN
CREATE POLICY "allow_all_editor_mockups" ON msk_editor_mockups FOR ALL USING (true);
END IF;
END $$;
-- 17. COLUNAS DE INTEGRACAO COM MERCADO LIVRE
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS ml_item_id TEXT;
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS ml_permalink TEXT;
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS ml_status TEXT;
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS ml_published_at TIMESTAMP WITH TIME ZONE;
-- 18. COLUNA DE RESERVA DE ESTOQUE
ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS quantidade_reservada INTEGER DEFAULT 0;
-- 19. FORNECEDORES DE PRODUTOS DE TERCEIROS
CREATE TABLE IF NOT EXISTS msk_fornecedores_produtos (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
fornecedor_nome TEXT NOT NULL,
fornecedor_contato TEXT,
fornecedor_cidade TEXT,
produto_nome TEXT NOT NULL,
sku TEXT,
categoria TEXT DEFAULT 'terceirizado',
preco_custo DECIMAL(10,2) NOT NULL DEFAULT 0,
preco_venda DECIMAL(10,2) NOT NULL DEFAULT 0,
margem_lucro_pct DECIMAL(5,2) DEFAULT 0,
quantidade_estoque INTEGER DEFAULT 0,
unidade TEXT DEFAULT 'Fabrica',
tipo_produto TEXT DEFAULT 'terceirizado',
imagem_url TEXT,
observacao TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_fornecedores_produtos ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_fornecedores_produtos' AND policyname = 'allow_all_fornecedores_produtos') THEN
CREATE POLICY "allow_all_fornecedores_produtos" ON msk_fornecedores_produtos FOR ALL USING (true);
END IF;
END $$;
-- 20. CONSIGNADO
CREATE TABLE IF NOT EXISTS msk_consignado (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
fabrica_parceira TEXT NOT NULL,
produto_nome TEXT NOT NULL,
sku TEXT,
categoria TEXT DEFAULT '3D',
quantidade_consignada INTEGER DEFAULT 0,
quantidade_vendida INTEGER DEFAULT 0,
quantidade_devolvida INTEGER DEFAULT 0,
preco_custo DECIMAL(10,2) DEFAULT 0,
preco_venda DECIMAL(10,2) DEFAULT 0,
margem_lucro_pct DECIMAL(5,2) DEFAULT 0,
data_consignacao DATE,
data_validade_consignacao DATE,
status TEXT DEFAULT 'ativo',
observacao TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_consignado ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_consignado' AND policyname = 'allow_all_consignado') THEN
CREATE POLICY "allow_all_consignado" ON msk_consignado FOR ALL USING (true);
END IF;
END $$;
-- CHAT INTERNO
CREATE TABLE IF NOT EXISTS msk_chat_conversas (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
tipo TEXT NOT NULL DEFAULT 'individual',
nome TEXT,
participante_id TEXT,
participante_tipo TEXT NOT NULL DEFAULT 'parceiro',
participante_avatar TEXT,
op_vinculada TEXT,
ultima_mensagem TEXT,
ultima_mensagem_data TIMESTAMP WITH TIME ZONE DEFAULT now(),
nao_lidas INTEGER DEFAULT 0,
ativo BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE IF NOT EXISTS msk_chat_mensagens (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
conversa_id UUID REFERENCES msk_chat_conversas(id) ON DELETE CASCADE,
remetente TEXT NOT NULL DEFAULT 'MSK',
remetente_role TEXT NOT NULL DEFAULT 'admin',
conteudo TEXT NOT NULL,
anexo_url TEXT,
anexo_tipo TEXT,
lida BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE msk_chat_conversas ADD COLUMN IF NOT EXISTS participante_avatar TEXT;
ALTER TABLE msk_chat_conversas ADD COLUMN IF NOT EXISTS op_vinculada TEXT;
ALTER TABLE msk_chat_conversas ADD COLUMN IF NOT EXISTS nao_lidas INTEGER DEFAULT 0;
ALTER TABLE msk_chat_mensagens ADD COLUMN IF NOT EXISTS anexo_url TEXT;
ALTER TABLE msk_chat_mensagens ADD COLUMN IF NOT EXISTS anexo_tipo TEXT;
CREATE INDEX IF NOT EXISTS idx_chat_conversas_participante ON msk_chat_conversas(participante_id);
CREATE INDEX IF NOT EXISTS idx_chat_mensagens_conversa ON msk_chat_mensagens(conversa_id);
CREATE INDEX IF NOT EXISTS idx_chat_mensagens_created ON msk_chat_mensagens(created_at);
ALTER TABLE msk_chat_conversas REPLICA IDENTITY FULL;
ALTER TABLE msk_chat_mensagens REPLICA IDENTITY FULL;
ALTER TABLE msk_chat_conversas ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_chat_conversas' AND policyname = 'allow_all_chat_conversas') THEN
CREATE POLICY "allow_all_chat_conversas" ON msk_chat_conversas FOR ALL USING (true);
END IF;
END $$;
ALTER TABLE msk_chat_mensagens ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_chat_mensagens' AND policyname = 'allow_all_chat_mensagens') THEN
CREATE POLICY "allow_all_chat_mensagens" ON msk_chat_mensagens FOR ALL USING (true);
END IF;
END $$;
-- CARD LOGOS (posicionamento da logo nos cards da home)
CREATE TABLE IF NOT EXISTS msk_card_logos (
card_key TEXT PRIMARY KEY,
top NUMERIC DEFAULT 50,
left NUMERIC DEFAULT 50,
width NUMERIC DEFAULT 12,
rotate NUMERIC DEFAULT 0,
blend TEXT DEFAULT 'multiply',
opacity NUMERIC DEFAULT 0.9,
enabled BOOLEAN DEFAULT true,
updated_at TIMESTAMPTZ DEFAULT now()
);
ALTER TABLE msk_card_logos ENABLE ROW LEVEL SECURITY;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_card_logos' AND policyname = 'allow_all_card_logos') THEN
CREATE POLICY "allow_all_card_logos" ON msk_card_logos FOR ALL USING (true) WITH CHECK (true);
END IF;
END $$;No painel → Storage → New bucket para cada um:
| Nome do Bucket | Público? | Para que serve |
|---|---|---|
| produtos | Sim | Fotos dos produtos/uniformes |
| mockups | Sim | Mockups PNG do CorelDRAW |
| logos | Sim | Logos dos clientes no editor visual |
| editor-texturas | Sim | Texturas de tecido personalizadas |
| chat-anexos | Sim | Imagens e documentos do chat |
| conferencia-fotos | Sim | Fotos da conferência de peças |
| provador-fotos | Sim | Fotos dos alunos no provador |
| technical-documents | Não | Fichas técnicas em PDF |
| production-qrcodes | Sim | QR Codes das ordens de produção |
Para cada bucket público, cole no SQL Editor:
-- Substitua 'produtos' pelo nome de cada bucket
CREATE POLICY "allow_public_read_produtos" ON storage.objects FOR SELECT USING (bucket_id = 'produtos');
CREATE POLICY "allow_public_upload_produtos" ON storage.objects FOR INSERT WITH CHECK (bucket_id = 'produtos');
CREATE POLICY "allow_public_update_produtos" ON storage.objects FOR UPDATE USING (bucket_id = 'produtos');msk_chat_conversas e msk_chat_mensagensPainel → Edge Functions → Create a new function. Para cada função, digite o nome e cole o código correspondente:
Publica/atualiza produtos no Mercado Livre.
import { serve } from "https://deno.land/std@0.168.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",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
const ML_API_BASE = "https://api.mercadolibre.com";
async function getAccessToken(): Promise<string> {
const clientId = Deno.env.get("ML_CLIENT_ID");
const clientSecret = Deno.env.get("ML_CLIENT_SECRET");
const refreshToken = Deno.env.get("ML_REFRESH_TOKEN");
if (!clientId || !clientSecret || !refreshToken) {
throw new Error("Credenciais do ML ausentes. Configure as secrets.");
}
const res = await fetch(`${ML_API_BASE}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: clientId,
client_secret: clientSecret,
refresh_token: refreshToken,
}),
});
if (!res.ok) {
const body = await res.text();
throw new Error(`Falha ao obter token ML: ${body}`);
}
const data = await res.json();
return data.access_token;
}
serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
try {
const supabaseClient = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
);
const body = await req.json();
const { produto_id, acao } = body;
if (!produto_id) {
return new Response(
JSON.stringify({ error: "produto_id e obrigatorio" }),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
const { data: produto, error: prodError } = await supabaseClient
.from("msk_produtos_estoque")
.select("*")
.eq("id", produto_id)
.single();
if (prodError || !produto) {
return new Response(
JSON.stringify({ error: "Produto nao encontrado" }),
{ status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
const accessToken = await getAccessToken();
const mlItem: any = {
title: String(produto.nome).substring(0, 60),
description: {
plain_text:
(produto.descricao || produto.nome) +
`\n\nCategoria: ${produto.categoria || "N/A"}` +
`\nTecido: ${produto.tipo_tecido || produto.tecido || "N/A"}` +
`\nComposicao: ${produto.composicao_tecido || "N/A"}` +
`\nTamanhos: ${produto.tamanhos || "N/A"}` +
`\nCor: ${produto.cor || "N/A"}`,
},
price: Number(produto.preco) || 0,
currency_id: "BRL",
available_quantity: Number(produto.quantidade) || 1,
buying_mode: "buy_it_now",
condition: "new",
listing_type_id: "gold_special",
shipping: { mode: "me2", local_pick_up: true, free_shipping: false },
attributes: [{ id: "BRAND", value_name: "MSK" }],
};
if (produto.imagem_url) {
mlItem.pictures = [{ source: produto.imagem_url }];
}
const mlItemId = produto.ml_item_id;
let response: Response;
if (mlItemId && acao !== "republicar") {
const res = await fetch(`${ML_API_BASE}/items/${mlItemId}`, {
method: "PUT",
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
body: JSON.stringify({
price: mlItem.price,
available_quantity: mlItem.available_quantity,
title: mlItem.title,
}),
});
const result = await res.json();
if (!res.ok) throw new Error(`Erro ao atualizar ML: ${JSON.stringify(result)}`);
response = new Response(
JSON.stringify({ success: true, action: "atualizado", ml_item_id: mlItemId, ml_result: result }),
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
} else {
const res = await fetch(`${ML_API_BASE}/items`, {
method: "POST",
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
body: JSON.stringify(mlItem),
});
const result = await res.json();
if (!res.ok) throw new Error(`Erro ao publicar ML: ${JSON.stringify(result)}`);
await supabaseClient
.from("msk_produtos_estoque")
.update({
ml_item_id: result.id,
ml_permalink: result.permalink || null,
ml_status: result.status || "active",
ml_published_at: new Date().toISOString(),
})
.eq("id", produto_id);
response = new Response(
JSON.stringify({ success: true, action: "publicado", ml_item_id: result.id, ml_permalink: result.permalink, ml_result: result }),
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
await supabaseClient.from("msk_automacoes_logs").insert([
{
automacao_nome: "Publicacao Mercado Livre",
status: "sucesso",
descricao: `Produto ${produto.nome} (${produto.sku}) publicado/atualizado no ML`,
},
]);
return response;
} catch (error) {
console.error("ERRO ML:", error);
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
});Recebe pedidos do ML/Shopee e salva no banco.
import { serve } from "https://deno.land/std@0.168.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",
};
const GHL_API = "https://services.leadconnectorhq.com";
const GHL_VERSION = "2021-07-28";
serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
try {
const supabaseClient = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""
);
const payload = await req.json();
const { source, order } = payload; // source: 'mercadolivre' | 'shopee'
if (!source || !order) {
throw new Error("Invalid payload: missing source or order data");
}
const externalOrderId = String(order.id);
// 1. IDEMPOTÊNCIA: se o pedido já existe, NÃO recriar oportunidade
const { data: existingOrder } = await supabaseClient
.from("marketplace_orders")
.select("id, ghl_contact_id, ghl_opportunity_id")
.eq("marketplace", source)
.eq("external_order_id", externalOrderId)
.maybeSingle();
if (existingOrder) {
if (order.status) {
await supabaseClient
.from("marketplace_orders")
.update({ status: order.status, updated_at: new Date().toISOString() })
.eq("id", existingOrder.id);
}
return new Response(JSON.stringify({
success: true,
message: "Pedido ja existente - nenhuma oportunidade duplicada criada",
orderId: existingOrder.id,
ghl_opportunity_id: existingOrder.ghl_opportunity_id ?? null,
}), { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 });
}
// 2. Salvar pedido no Supabase
const { data: dbOrder, error: dbError } = await supabaseClient
.from("marketplace_orders")
.insert([{
marketplace: source,
external_order_id: externalOrderId,
customer_name: order.customer.name,
customer_email: order.customer.email || null,
customer_phone: order.customer.phone || null,
total_amount: order.total,
status: order.status,
items: order.items,
}])
.select().single();
if (dbError) throw dbError;
// 3. Integração CRM
const crmApiKey = Deno.env.get("CRM_API_KEY");
const locationId = Deno.env.get("CRM_LOCATION_ID");
const pipelineId = Deno.env.get("CRM_PIPELINE_ID");
const stageId = Deno.env.get("CRM_PIPELINE_STAGE_ID");
let ghlContactId = null;
let ghlOpportunityId = null;
if (crmApiKey && locationId && pipelineId && stageId) {
const fullName = (order.customer.name || "").trim();
const firstName = fullName.split(" ")[0] || fullName;
const lastName = fullName.split(" ").slice(1).join(" ") || "";
const email = order.customer.email || "";
const phone = order.customer.phone || "";
// 3a. DEDUPLICAÇÃO: buscar contato por email
if (email) {
const searchRes = await fetch(
`${GHL_API}/contacts/?locationId=${locationId}&email=${encodeURIComponent(email)}`,
{ headers: { Authorization: `Bearer ${crmApiKey}`, Version: GHL_VERSION, Accept: "application/json" } }
);
if (searchRes.ok) {
const searchData = await searchRes.json();
if (searchData.contacts && searchData.contacts.length > 0) {
ghlContactId = searchData.contacts[0].id;
}
}
}
// 3a2. Se não achou por email, buscar por telefone
if (!ghlContactId && phone) {
const searchRes = await fetch(
`${GHL_API}/contacts/?locationId=${locationId}&phone=${encodeURIComponent(phone)}`,
{ headers: { Authorization: `Bearer ${crmApiKey}`, Version: GHL_VERSION, Accept: "application/json" } }
);
if (searchRes.ok) {
const searchData = await searchRes.json();
if (searchData.contacts && searchData.contacts.length > 0) {
ghlContactId = searchData.contacts[0].id;
}
}
}
// 3b. Se não existe, criar contato
if (!ghlContactId) {
const contactBody = { firstName, lastName, locationId, tags: ["marketplace", source] };
if (email) contactBody.email = email;
if (phone) contactBody.phone = phone;
const contactRes = await fetch(`${GHL_API}/contacts/`, {
method: "POST",
headers: { Authorization: `Bearer ${crmApiKey}`, Version: GHL_VERSION, "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(contactBody),
});
const contactData = await contactRes.json();
if (contactData.contact?.id) ghlContactId = contactData.contact.id;
else if (contactData.id) ghlContactId = contactData.id;
}
// 3c. Criar oportunidade no pipeline
if (ghlContactId) {
const oppRes = await fetch(`${GHL_API}/opportunities/`, {
method: "POST",
headers: { Authorization: `Bearer ${crmApiKey}`, Version: GHL_VERSION, "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({
pipelineId, locationId,
name: `Pedido ${source} - ${externalOrderId}`,
pipelineStageId: stageId,
status: "open",
contactId: ghlContactId,
monetaryValue: Number(order.total) || 0,
}),
});
const oppData = await oppRes.json();
if (oppData.id) ghlOpportunityId = oppData.id;
else if (oppData.opportunity?.id) ghlOpportunityId = oppData.opportunity.id;
}
}
// 4. Salvar IDs no Supabase
if (ghlContactId || ghlOpportunityId) {
await supabaseClient
.from("marketplace_orders")
.update({ ghl_contact_id: ghlContactId, ghl_opportunity_id: ghlOpportunityId, updated_at: new Date().toISOString() })
.eq("id", dbOrder.id);
}
await supabaseClient.from("msk_automacoes_logs").insert([{
automacao_nome: "Webhook Marketplace -> CRM",
status: "sucesso",
descricao: `Pedido ${source} #${externalOrderId} salvo. Contato: ${ghlContactId ?? "n/a"}. Oportunidade: ${ghlOpportunityId ?? "n/a"}.`,
}]);
return new Response(JSON.stringify({
success: true, orderId: dbOrder.id,
ghl_contact_id: ghlContactId, ghl_opportunity_id: ghlOpportunityId,
}), { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 });
} catch (error) {
console.error("ERRO webhook:", error);
return new Response(JSON.stringify({ error: error.message }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 });
}
});Gera PDF e QR Code das ordens de produção.
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.39.3"
import * as qrcode from "https://deno.land/x/qrcode_generator@v1.0.1/mod.ts"
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
const { production_order_id } = await req.json()
if (!production_order_id) {
return new Response(JSON.stringify({ error: 'production_order_id is required' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
const { data: order, error: orderError } = await supabaseClient
.from('production_orders')
.select('*, technical_sheets(*)')
.eq('id', production_order_id)
.single()
if (orderError || !order) {
throw new Error('Production order not found')
}
const technicalSheet = order.technical_sheets[0]
if (!technicalSheet) {
throw new Error('Technical sheet not found for this order')
}
if (technicalSheet.status !== 'approved') {
throw new Error('Technical sheet must be approved before generating package')
}
if (technicalSheet.pdf_url && technicalSheet.technical_image_url) {
return new Response(JSON.stringify({
message: 'Package already generated for this version',
pdf_url: technicalSheet.pdf_url,
qr_code_url: technicalSheet.qr_code_url,
}), {
status: 200,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}
const { data: sizes } = await supabaseClient
.from('production_order_sizes')
.select('*')
.eq('production_order_id', production_order_id)
const { data: personalizations } = await supabaseClient
.from('personalizations')
.select('*')
.eq('technical_sheet_id', technicalSheet.id)
const { error: tasksError } = await supabaseClient.rpc('create_production_tasks', {
p_order_id: production_order_id
})
if (tasksError) {
console.error("Error creating tasks:", tasksError)
}
const pdfContent = `Ficha Tecnica - Ordem: ${order.order_number}\nStatus: Aprovado\n`
const pdfBlob = new Blob([pdfContent], { type: 'application/pdf' })
const pdfFileName = `${production_order_id}/ficha-tecnica-v${technicalSheet.version || 1}.pdf`
const orderUrl = `${Deno.env.get('PUBLIC_SITE_URL') || 'https://mskonline.com.br'}/admin/producao/caixa/${production_order_id}`
const qrCodeBase64 = qrcode.qrcode(4, 'L').addData(orderUrl).make().createDataURL(4)
const qrFetch = await fetch(qrCodeBase64)
const qrBlob = await qrFetch.blob()
const qrFileName = `${production_order_id}/qr-code.png`
const { error: pdfUploadError } = await supabaseClient.storage
.from('technical-documents')
.upload(pdfFileName, pdfBlob, { upsert: true, contentType: 'application/pdf' })
if (pdfUploadError) throw pdfUploadError
const { error: qrUploadError } = await supabaseClient.storage
.from('production-qrcodes')
.upload(qrFileName, qrBlob, { upsert: true, contentType: 'image/png' })
if (qrUploadError) throw qrUploadError
const { data: pdfUrlData } = supabaseClient.storage.from('technical-documents').getPublicUrl(pdfFileName)
const { data: qrUrlData } = supabaseClient.storage.from('production-qrcodes').getPublicUrl(qrFileName)
await supabaseClient
.from('technical_sheets')
.update({
pdf_url: pdfUrlData.publicUrl,
qr_code_url: qrUrlData.publicUrl,
})
.eq('id', technicalSheet.id)
await supabaseClient
.from('production_history')
.insert({
production_order_id: production_order_id,
action: 'package_generated',
details: 'PDF, QR Code e tarefas geradas com sucesso',
created_by: 'system'
})
return new Response(JSON.stringify({
success: true,
pdf_url: pdfUrlData.publicUrl,
qr_code_url: qrUrlData.publicUrl,
}), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
})
} catch (error: any) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
}
})Painel → Project Settings (engrenagem) → Edge Functions → Secrets → Add secret:
| Nome da Secret | Valor |
|---|---|
| ML_CLIENT_ID | (obtido no passo 7) |
| ML_CLIENT_SECRET | (obtido no passo 7) |
| ML_REFRESH_TOKEN | (obtido no passo 7) |
| SUPABASE_URL | https://gcpuoafrpveyrtiifqde.supabase.co |
| SUPABASE_SERVICE_ROLE_KEY | (pegue em Project Settings → API → service_role) |
| CRM_API_KEY | Private Integration Token da sua subconta MSK (ver passo 6b) |
| CRM_LOCATION_ID | Location ID da subconta MSK (ver passo 6b) |
| CRM_PIPELINE_ID | ID do pipeline "MSK | Marketplace" (ver passo 6b) |
| CRM_PIPELINE_STAGE_ID | ID do estágio "Novo Pedido" (ver passo 6b) |
A integração com o CRM já existe no código. Você só precisa obter 4 valores na sua subconta MSK e colocar nas secrets acima.
CRM_API_KEY.../location/LOCATION_ID/...CRM_LOCATION_IDpipelineId=XXX e stageId=YYY.pipelineId → CRM_PIPELINE_IDstageId do estágio "Novo Pedido" → CRM_PIPELINE_STAGE_IDEm vez de criar o pipeline "MSK | Marketplace" e os 8 estágios manualmente no CRM, use a Edge Function setup-crm-pipeline. Ela consulta a API, cria o pipeline com os estágios na ordem correta (se ainda não existir) e devolve os IDs prontos para colar nas secrets.
setup-crm-pipelineCRM_API_KEY e CRM_LOCATION_ID que já estão configuradas.CRM_PIPELINE_ID e CRM_PIPELINE_STAGE_ID — copie esses dois valores.CRM_PIPELINE_ID = valor retornadoCRM_PIPELINE_STAGE_ID = valor retornadoimport { serve } from "https://deno.land/std@0.168.0/http/server.ts";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
const GHL_API = "https://services.leadconnectorhq.com";
const PIPELINE_VERSION = "v3";
const PIPELINE_NAME = "MSK | Marketplace";
const STAGES = [
"Novo Pedido",
"Pagamento Confirmado",
"Em Separação",
"Em Produção",
"Pronto para Envio",
"Enviado",
"Entregue",
"Cancelado",
];
serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });
try {
const apiKey = Deno.env.get("CRM_API_KEY");
const locationId = Deno.env.get("CRM_LOCATION_ID");
if (!apiKey || !locationId) {
return new Response(JSON.stringify({ success: false, error: "Faltam CRM_API_KEY ou CRM_LOCATION_ID" }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 });
}
const authHeaders = { Authorization: `Bearer ${apiKey}`, Accept: "application/json" };
// 1. Consultar pipelines existentes
const listRes = await fetch(`${GHL_API}/opportunities/pipelines?locationId=${encodeURIComponent(locationId)}`,
{ headers: { ...authHeaders, Version: PIPELINE_VERSION } });
if (!listRes.ok) {
const t = await listRes.text();
return new Response(JSON.stringify({ success: false, error: `Erro ao consultar (${listRes.status}): ${t}` }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 502 });
}
const listData = await listRes.json();
const pipelines = listData.pipelines || [];
let target = pipelines.find((p) => p.name && p.name.toLowerCase() === PIPELINE_NAME.toLowerCase());
// 2. Criar se não existir
if (!target) {
const createRes = await fetch(`${GHL_API}/opportunities/pipelines`, {
method: "POST",
headers: { ...authHeaders, Version: PIPELINE_VERSION, "Content-Type": "application/json" },
body: JSON.stringify({
name: PIPELINE_NAME, locationId,
stages: STAGES.map((name, i) => ({ name, position: i + 1, showInFunnel: true })),
showInFunnel: true, showInPieChart: false, colorRenderMode: "dot",
}),
});
if (!createRes.ok) {
const t = await createRes.text();
return new Response(JSON.stringify({ success: false, error: `Erro ao criar (${createRes.status}): ${t}` }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 502 });
}
const created = await createRes.json();
target = { id: created.id, name: created.name || PIPELINE_NAME, stages: created.stages || [] };
}
// 3. Obter estágio "Novo Pedido"
let stage = (target.stages || []).find((s) => s.name && s.name.toLowerCase() === "novo pedido");
if (!stage && target.id) {
const dRes = await fetch(`${GHL_API}/opportunities/pipelines/${target.id}`, { headers: { ...authHeaders, Version: PIPELINE_VERSION } });
if (dRes.ok) {
const d = await dRes.json();
target.stages = d.stages || [];
stage = target.stages.find((s) => s.name && s.name.toLowerCase() === "novo pedido");
}
}
if (!stage) {
return new Response(JSON.stringify({ success: false, error: "Estágio Novo Pedido não encontrado", pipelineId: target.id, stages: target.stages }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 404 });
}
return new Response(JSON.stringify({
success: true, message: "Pipeline configurado.",
CRM_PIPELINE_ID: target.id, CRM_PIPELINE_STAGE_ID: stage.id,
pipelineName: target.name, stages: target.stages,
}), { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 });
} catch (e) {
return new Response(JSON.stringify({ success: false, error: e.message }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 500 });
}
});Como testar (após deploy):
curl -X POST https://gcpuoafrpveyrtiifqde.supabase.co/functions/v1/setup-crm-pipeline \
-H "Authorization: Bearer SUA_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{}'success: true com CRM_PIPELINE_ID e CRM_PIPELINE_STAGE_ID preenchidos. Se o pipeline já existir, ela apenas consulta e retorna os IDs existentes (não duplica).O Portal da Família agora exige login real. Cada cliente vê apenas seus próprios pedidos — protegido no banco por RLS. O admin continua vendo tudo (via papel admin).
supabase/migrations/20260914_portal_auth.sql (cria coluna user_id, tabela app_roles, função is_admin() e RLS por usuário).INSERT INTO public.app_roles (user_id, role)
VALUES ('UID_DO_ADMIN', 'admin')
ON CONFLICT (user_id) DO UPDATE SET role = 'admin';track-order — rastreamento público seguro (aceita só tracking_token, retorna só status/etapa/previsão).create-checkout-order — cria pedido do checkout (visitante = user_id NULL via service_role; autenticado = user_id do JWT).upload-provador-foto — upload seguro de fotos do provador para o bucket privado provador-fotos (o bucket não aceita INSERT público; só esta função grava).cliente1@teste.com e cliente2@teste.com) em Authentication → Users. Faça login com cada um em /portal — cada um verá apenas os próprios pedidos. O admin (com papel admin) vê todos.https://gcpuoafrpveyrtiifqde.supabase.co/functions/v1/ml-publish | Scopes: read, write, offline_accesshttps://auth.mercadolivre.com.br/authorization?response_type=code&client_id=SEU_CLIENT_ID&redirect_uri=SUA_REDIRECT_URICopie o ?code=XXXX da URL de retorno e faça um POST para:
POST https://api.mercadolibre.com/oauth/token
grant_type=authorization_code
client_id=SEU_CLIENT_ID
client_secret=SEU_CLIENT_SECRET
code=CODIGO_COPIADO
redirect_uri=SUA_REDIRECT_URIA resposta contém refresh_token → este é o ML_REFRESH_TOKEN.
https://gcpuoafrpveyrtiifqde.supabase.co/functions/v1/marketplace-webhookSelecione os eventos Orders e Items → Salve.
WHATSAPP_TOKEN e WHATSAPP_PHONE_IDO sistema já gera links wa.me com mensagem pronta. Funciona sem configuração.
Envie um POST para a Edge Function com um pedido de exemplo (sem criar venda real). Pode usar terminal ou o site reqbin.com.
curl -X POST https://gcpuoafrpveyrtiifqde.supabase.co/functions/v1/marketplace-webhook \
-H "Authorization: Bearer SUA_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "mercadolivre",
"order": {
"id": "TESTE-001",
"status": "pago",
"total": 159.90,
"customer": {
"name": "Joao da Silva Teste",
"email": "teste@mskonline.com.br",
"phone": "+5513999999999"
},
"items": [
{ "nome": "Kit Escolar 3 Pecas", "quantidade": 1, "preco": 159.90 }
]
}
}'Resultado esperado: success: true com ghl_contact_id e ghl_opportunity_id preenchidos. No CRM aparecerá o contato com tags marketplace e mercadolivre, e a oportunidade "Pedido mercadolivre - TESTE-001" no estágio "Novo Pedido" com R$ 159,90.
Idempotência: rode o mesmo comando de novo — a resposta dirá "Pedido ja existente - nenhuma oportunidade duplicada criada". O mesmo pedido nunca cria duas oportunidades.
| Onde | Senha / Acesso |
|---|---|
| Painel Admin | /admin → senha: simioni2026 |
| Portal da Transparência | /escola/:slug/financeiro → senha: simioni2026 |
| Portal da Família | /portal |
| Portal do Representante | /representante |
| Escolher Escola | /escolha-sua-escola |
| Editor de Logos | /admin/card-logos |
| Editor Visual | /admin/dashboard → Editor Visual |
Após completar todos os passos, o sistema estará 100% funcional: cadastro de produtos/uniformes/kits, matéria-prima, ordens de serviço, precificação, chat em tempo real, provador virtual com IA, publicação no Mercado Livre, notificações por WhatsApp, conferência fotográfica, transferência de estoque, editor visual, portal da transparência, portal da família e catálogo escolar.
Tempo estimado: 2 a 4 horas.