mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Complete WhatsApp CRM application built with Next.js 14 (App Router), Supabase (PostgreSQL + Auth + Realtime), and shadcn/ui. Features: - Auth: Login, signup, forgot password with Supabase Auth - Dashboard: Analytics overview with stats, recent activity - Inbox: Real-time WhatsApp conversations with message threading - Contacts: CRUD with tags, custom fields, notes, CSV import - Broadcasts: 4-step wizard with audience selection, template sending - Pipelines: Kanban board with deal management - Settings: WhatsApp API config, templates, tags - WhatsApp API: Webhook (verify + receive), send, broadcast, media proxy - Database: 16 tables with RLS policies, indexes, realtime subscriptions - Dark theme throughout with emerald accents Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
360 lines
15 KiB
PL/PgSQL
360 lines
15 KiB
PL/PgSQL
-- Enable UUID extension
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
|
-- ============================================================
|
|
-- PROFILES
|
|
-- ============================================================
|
|
CREATE TABLE profiles (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
full_name TEXT NOT NULL,
|
|
email TEXT NOT NULL,
|
|
avatar_url TEXT,
|
|
role TEXT DEFAULT 'user',
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(user_id)
|
|
);
|
|
|
|
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can view own profile" ON profiles FOR SELECT USING (auth.uid() = user_id);
|
|
CREATE POLICY "Users can update own profile" ON profiles FOR UPDATE USING (auth.uid() = user_id);
|
|
CREATE POLICY "Users can insert own profile" ON profiles FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- CONTACTS
|
|
-- ============================================================
|
|
CREATE TABLE contacts (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
phone TEXT NOT NULL,
|
|
name TEXT,
|
|
email TEXT,
|
|
company TEXT,
|
|
avatar_url TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_contacts_user_id ON contacts(user_id);
|
|
CREATE INDEX idx_contacts_phone ON contacts(phone);
|
|
|
|
ALTER TABLE contacts ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage own contacts" ON contacts FOR ALL USING (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- TAGS
|
|
-- ============================================================
|
|
CREATE TABLE tags (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
color TEXT NOT NULL DEFAULT '#3b82f6',
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
ALTER TABLE tags ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage own tags" ON tags FOR ALL USING (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- CONTACT_TAGS (many-to-many)
|
|
-- ============================================================
|
|
CREATE TABLE contact_tags (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(contact_id, tag_id)
|
|
);
|
|
|
|
CREATE INDEX idx_contact_tags_contact ON contact_tags(contact_id);
|
|
CREATE INDEX idx_contact_tags_tag ON contact_tags(tag_id);
|
|
|
|
ALTER TABLE contact_tags ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage contact tags" ON contact_tags FOR ALL
|
|
USING (EXISTS (SELECT 1 FROM contacts WHERE contacts.id = contact_tags.contact_id AND contacts.user_id = auth.uid()));
|
|
|
|
-- ============================================================
|
|
-- CUSTOM_FIELDS
|
|
-- ============================================================
|
|
CREATE TABLE custom_fields (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
field_name TEXT NOT NULL,
|
|
field_type TEXT NOT NULL DEFAULT 'text',
|
|
field_options JSONB,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
ALTER TABLE custom_fields ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage own custom fields" ON custom_fields FOR ALL USING (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- CONTACT_CUSTOM_VALUES
|
|
-- ============================================================
|
|
CREATE TABLE contact_custom_values (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
custom_field_id UUID NOT NULL REFERENCES custom_fields(id) ON DELETE CASCADE,
|
|
value TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(contact_id, custom_field_id)
|
|
);
|
|
|
|
ALTER TABLE contact_custom_values ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage custom values" ON contact_custom_values FOR ALL
|
|
USING (EXISTS (SELECT 1 FROM contacts WHERE contacts.id = contact_custom_values.contact_id AND contacts.user_id = auth.uid()));
|
|
|
|
-- ============================================================
|
|
-- CONTACT_NOTES
|
|
-- ============================================================
|
|
CREATE TABLE contact_notes (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
note_text TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
ALTER TABLE contact_notes ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage own notes" ON contact_notes FOR ALL USING (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- CONVERSATIONS
|
|
-- ============================================================
|
|
CREATE TABLE conversations (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'pending', 'closed')),
|
|
assigned_agent_id UUID,
|
|
last_message_text TEXT,
|
|
last_message_at TIMESTAMPTZ,
|
|
unread_count INTEGER DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_conversations_user_id ON conversations(user_id);
|
|
CREATE INDEX idx_conversations_contact_id ON conversations(contact_id);
|
|
|
|
ALTER TABLE conversations ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage own conversations" ON conversations FOR ALL USING (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- MESSAGES
|
|
-- ============================================================
|
|
CREATE TABLE messages (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
sender_type TEXT NOT NULL CHECK (sender_type IN ('customer', 'agent', 'bot')),
|
|
sender_id UUID,
|
|
content_type TEXT NOT NULL DEFAULT 'text' CHECK (content_type IN ('text', 'image', 'document', 'audio', 'video', 'location', 'template')),
|
|
content_text TEXT,
|
|
media_url TEXT,
|
|
template_name TEXT,
|
|
message_id TEXT,
|
|
status TEXT NOT NULL DEFAULT 'sent' CHECK (status IN ('sending', 'sent', 'delivered', 'read', 'failed')),
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_messages_conversation ON messages(conversation_id);
|
|
CREATE INDEX idx_messages_message_id ON messages(message_id);
|
|
|
|
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can view own messages" ON messages FOR ALL
|
|
USING (EXISTS (SELECT 1 FROM conversations WHERE conversations.id = messages.conversation_id AND conversations.user_id = auth.uid()));
|
|
|
|
-- Service role bypass for webhook inserts
|
|
CREATE POLICY "Service role can insert messages" ON messages FOR INSERT
|
|
WITH CHECK (true);
|
|
|
|
-- ============================================================
|
|
-- WHATSAPP_CONFIG
|
|
-- ============================================================
|
|
CREATE TABLE whatsapp_config (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
phone_number_id TEXT NOT NULL,
|
|
waba_id TEXT,
|
|
access_token TEXT NOT NULL,
|
|
verify_token TEXT,
|
|
status TEXT NOT NULL DEFAULT 'disconnected' CHECK (status IN ('connected', 'disconnected')),
|
|
connected_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(user_id)
|
|
);
|
|
|
|
ALTER TABLE whatsapp_config ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage own config" ON whatsapp_config FOR ALL USING (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- MESSAGE_TEMPLATES
|
|
-- ============================================================
|
|
CREATE TABLE message_templates (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
category TEXT NOT NULL DEFAULT 'Marketing' CHECK (category IN ('Marketing', 'Utility', 'Authentication')),
|
|
language TEXT DEFAULT 'en_US',
|
|
header_type TEXT CHECK (header_type IN ('text', 'image', 'video', 'document')),
|
|
header_content TEXT,
|
|
body_text TEXT NOT NULL,
|
|
footer_text TEXT,
|
|
buttons JSONB,
|
|
status TEXT DEFAULT 'Draft' CHECK (status IN ('Draft', 'Pending', 'Approved', 'Rejected')),
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
ALTER TABLE message_templates ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage own templates" ON message_templates FOR ALL USING (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- PIPELINES
|
|
-- ============================================================
|
|
CREATE TABLE pipelines (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
ALTER TABLE pipelines ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage own pipelines" ON pipelines FOR ALL USING (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- PIPELINE_STAGES
|
|
-- ============================================================
|
|
CREATE TABLE pipeline_stages (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
pipeline_id UUID NOT NULL REFERENCES pipelines(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
color TEXT NOT NULL DEFAULT '#3b82f6',
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_pipeline_stages_pipeline ON pipeline_stages(pipeline_id);
|
|
|
|
ALTER TABLE pipeline_stages ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage pipeline stages" ON pipeline_stages FOR ALL
|
|
USING (EXISTS (SELECT 1 FROM pipelines WHERE pipelines.id = pipeline_stages.pipeline_id AND pipelines.user_id = auth.uid()));
|
|
|
|
-- ============================================================
|
|
-- DEALS
|
|
-- ============================================================
|
|
CREATE TABLE deals (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
pipeline_id UUID NOT NULL REFERENCES pipelines(id) ON DELETE CASCADE,
|
|
stage_id UUID NOT NULL REFERENCES pipeline_stages(id),
|
|
contact_id UUID NOT NULL REFERENCES contacts(id),
|
|
conversation_id UUID REFERENCES conversations(id),
|
|
title TEXT NOT NULL,
|
|
value NUMERIC(12,2) NOT NULL DEFAULT 0,
|
|
currency TEXT DEFAULT 'USD',
|
|
notes TEXT,
|
|
expected_close_date DATE,
|
|
status TEXT DEFAULT 'active',
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_deals_pipeline ON deals(pipeline_id);
|
|
CREATE INDEX idx_deals_stage ON deals(stage_id);
|
|
|
|
ALTER TABLE deals ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage own deals" ON deals FOR ALL USING (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- BROADCASTS
|
|
-- ============================================================
|
|
CREATE TABLE broadcasts (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
template_name TEXT NOT NULL,
|
|
template_language TEXT NOT NULL DEFAULT 'en_US',
|
|
template_variables JSONB,
|
|
audience_filter JSONB,
|
|
scheduled_at TIMESTAMPTZ,
|
|
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'scheduled', 'sending', 'sent', 'failed')),
|
|
total_recipients INTEGER DEFAULT 0,
|
|
sent_count INTEGER DEFAULT 0,
|
|
delivered_count INTEGER DEFAULT 0,
|
|
read_count INTEGER DEFAULT 0,
|
|
replied_count INTEGER DEFAULT 0,
|
|
failed_count INTEGER DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
ALTER TABLE broadcasts ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage own broadcasts" ON broadcasts FOR ALL USING (auth.uid() = user_id);
|
|
|
|
-- ============================================================
|
|
-- BROADCAST_RECIPIENTS
|
|
-- ============================================================
|
|
CREATE TABLE broadcast_recipients (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
broadcast_id UUID NOT NULL REFERENCES broadcasts(id) ON DELETE CASCADE,
|
|
contact_id UUID NOT NULL REFERENCES contacts(id),
|
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'delivered', 'read', 'replied', 'failed')),
|
|
sent_at TIMESTAMPTZ,
|
|
delivered_at TIMESTAMPTZ,
|
|
read_at TIMESTAMPTZ,
|
|
replied_at TIMESTAMPTZ,
|
|
error_message TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_broadcast_recipients_broadcast ON broadcast_recipients(broadcast_id);
|
|
|
|
ALTER TABLE broadcast_recipients ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY "Users can manage broadcast recipients" ON broadcast_recipients FOR ALL
|
|
USING (EXISTS (SELECT 1 FROM broadcasts WHERE broadcasts.id = broadcast_recipients.broadcast_id AND broadcasts.user_id = auth.uid()));
|
|
|
|
-- ============================================================
|
|
-- UPDATED_AT TRIGGER FUNCTION
|
|
-- ============================================================
|
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Apply to tables with updated_at
|
|
CREATE TRIGGER set_updated_at BEFORE UPDATE ON profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
CREATE TRIGGER set_updated_at BEFORE UPDATE ON contacts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
CREATE TRIGGER set_updated_at BEFORE UPDATE ON conversations FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
CREATE TRIGGER set_updated_at BEFORE UPDATE ON whatsapp_config FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
CREATE TRIGGER set_updated_at BEFORE UPDATE ON message_templates FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
CREATE TRIGGER set_updated_at BEFORE UPDATE ON deals FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
CREATE TRIGGER set_updated_at BEFORE UPDATE ON broadcasts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
-- ============================================================
|
|
-- AUTO-CREATE PROFILE ON USER SIGNUP
|
|
-- ============================================================
|
|
CREATE OR REPLACE FUNCTION handle_new_user()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
INSERT INTO profiles (user_id, full_name, email)
|
|
VALUES (NEW.id, COALESCE(NEW.raw_user_meta_data->>'full_name', ''), NEW.email);
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
CREATE TRIGGER on_auth_user_created
|
|
AFTER INSERT ON auth.users
|
|
FOR EACH ROW EXECUTE FUNCTION handle_new_user();
|
|
|
|
-- ============================================================
|
|
-- ENABLE REALTIME for key tables
|
|
-- ============================================================
|
|
ALTER PUBLICATION supabase_realtime ADD TABLE messages;
|
|
ALTER PUBLICATION supabase_realtime ADD TABLE conversations;
|