const Database = require('better-sqlite3'); const path = require('path'); const bcrypt = require('bcrypt'); const DB_PATH = path.join(__dirname, 'marketing.db'); const db = new Database(DB_PATH); // Enable WAL mode and foreign keys db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); function initialize() { // Create tables db.exec(` CREATE TABLE IF NOT EXISTS team_members ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT, role TEXT, avatar_url TEXT, brands TEXT DEFAULT '[]', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS brands ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, priority INTEGER DEFAULT 2, color TEXT, icon TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, brand_id INTEGER REFERENCES brands(id), assigned_to INTEGER REFERENCES team_members(id), status TEXT DEFAULT 'draft', platform TEXT, content_type TEXT, scheduled_date DATETIME, published_date DATETIME, notes TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS assets ( id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT NOT NULL, original_name TEXT, mime_type TEXT, size INTEGER, tags TEXT DEFAULT '[]', brand_id INTEGER REFERENCES brands(id), campaign_id INTEGER REFERENCES campaigns(id), uploaded_by INTEGER REFERENCES team_members(id), folder TEXT DEFAULT 'general', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS campaigns ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, brand_id INTEGER REFERENCES brands(id), start_date DATE NOT NULL, end_date DATE NOT NULL, status TEXT DEFAULT 'planning', color TEXT, budget REAL, goals TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, brand_id INTEGER REFERENCES brands(id), owner_id INTEGER REFERENCES team_members(id), status TEXT DEFAULT 'active', priority TEXT DEFAULT 'medium', due_date DATE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, project_id INTEGER REFERENCES projects(id), assigned_to INTEGER REFERENCES team_members(id), created_by INTEGER REFERENCES team_members(id), status TEXT DEFAULT 'todo', priority TEXT DEFAULT 'medium', due_date DATE, is_personal BOOLEAN DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, completed_at DATETIME ); `); // Budget entries table — tracks money received db.exec(` CREATE TABLE IF NOT EXISTS budget_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT NOT NULL, amount REAL NOT NULL, source TEXT, campaign_id INTEGER REFERENCES campaigns(id), category TEXT DEFAULT 'marketing', date_received DATE NOT NULL, notes TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); // Users table for authentication db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'contributor', avatar TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); // Campaign tracks table db.exec(` CREATE TABLE IF NOT EXISTS campaign_tracks ( id INTEGER PRIMARY KEY AUTOINCREMENT, campaign_id INTEGER NOT NULL REFERENCES campaigns(id) ON DELETE CASCADE, name TEXT, type TEXT NOT NULL DEFAULT 'organic_social', platform TEXT, budget_allocated REAL DEFAULT 0, budget_spent REAL DEFAULT 0, revenue REAL DEFAULT 0, impressions INTEGER DEFAULT 0, clicks INTEGER DEFAULT 0, conversions INTEGER DEFAULT 0, notes TEXT DEFAULT '', status TEXT DEFAULT 'planned', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); // ─── Comments / discussion table ─── db.exec(` CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, entity_type TEXT NOT NULL, entity_id INTEGER NOT NULL, user_id INTEGER NOT NULL REFERENCES users(id), content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); // ─── Post attachments table ─── db.exec(` CREATE TABLE IF NOT EXISTS post_attachments ( id INTEGER PRIMARY KEY AUTOINCREMENT, post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE, filename TEXT NOT NULL, original_name TEXT, mime_type TEXT, size INTEGER, url TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); // ─── Column migrations ─── // Helper: adds a column to a table if it does not already exist. function addColumnIfMissing(table, column, definition) { const cols = db.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name); if (!cols.includes(column)) { db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`); console.log(`Added ${column} column to ${table}`); } } // Ownership columns (link to users table) for (const table of ['posts', 'tasks', 'campaigns', 'projects']) { addColumnIfMissing(table, 'created_by_user_id', 'INTEGER REFERENCES users(id)'); } // team_members additions addColumnIfMissing('team_members', 'phone', 'TEXT'); // campaigns additions addColumnIfMissing('campaigns', 'platforms', "TEXT DEFAULT '[]'"); addColumnIfMissing('campaigns', 'budget_spent', 'REAL DEFAULT 0'); addColumnIfMissing('campaigns', 'revenue', 'REAL DEFAULT 0'); addColumnIfMissing('campaigns', 'impressions', 'INTEGER DEFAULT 0'); addColumnIfMissing('campaigns', 'clicks', 'INTEGER DEFAULT 0'); addColumnIfMissing('campaigns', 'conversions', 'INTEGER DEFAULT 0'); addColumnIfMissing('campaigns', 'cost_per_click', 'REAL DEFAULT 0'); addColumnIfMissing('campaigns', 'notes', "TEXT DEFAULT ''"); // posts additions addColumnIfMissing('posts', 'track_id', 'INTEGER REFERENCES campaign_tracks(id)'); addColumnIfMissing('posts', 'campaign_id', 'INTEGER REFERENCES campaigns(id)'); addColumnIfMissing('posts', 'publication_links', "TEXT DEFAULT '[]'"); // posts.platforms with data migration from single platform field const postCols = db.prepare("PRAGMA table_info(posts)").all().map(c => c.name); if (!postCols.includes('platforms')) { db.exec("ALTER TABLE posts ADD COLUMN platforms TEXT DEFAULT '[]'"); const rows = db.prepare("SELECT id, platform FROM posts WHERE platform IS NOT NULL AND platform != ''").all(); const migrate = db.prepare("UPDATE posts SET platforms = ? WHERE id = ?"); for (const row of rows) { migrate.run(JSON.stringify([row.platform]), row.id); } console.log(`Added platforms column to posts, migrated ${rows.length} rows`); } // assets additions addColumnIfMissing('assets', 'campaign_id', 'INTEGER REFERENCES campaigns(id)'); // users additions addColumnIfMissing('users', 'team_member_id', 'INTEGER REFERENCES team_members(id)'); addColumnIfMissing('users', 'team_role', 'TEXT'); addColumnIfMissing('users', 'brands', "TEXT DEFAULT '[]'"); addColumnIfMissing('users', 'phone', 'TEXT'); addColumnIfMissing('users', 'tutorial_completed', 'INTEGER DEFAULT 0'); // Migrate team_members to users (one-time migration) const teamMembers = db.prepare('SELECT * FROM team_members').all(); const defaultPasswordHash = bcrypt.hashSync('changeme123', 10); for (const tm of teamMembers) { // Skip team_member id=9 (Fahed) - he's already user id=1 if (tm.id === 9) { // Just update his team_role and brands db.prepare('UPDATE users SET team_role = ?, brands = ?, team_member_id = ? WHERE id = 1') .run(tm.role, tm.brands, tm.id); continue; } // Check if user already exists with this team_member_id const existingUser = db.prepare('SELECT id FROM users WHERE team_member_id = ?').get(tm.id); if (existingUser) { // User exists, just update team_role and brands db.prepare('UPDATE users SET team_role = ?, brands = ?, phone = ? WHERE id = ?') .run(tm.role, tm.brands, tm.phone || null, existingUser.id); } else { // Create new user for this team member db.prepare(` INSERT INTO users (name, email, password_hash, role, team_role, brands, phone, team_member_id) VALUES (?, ?, ?, 'contributor', ?, ?, ?, ?) `).run( tm.name, tm.email, defaultPasswordHash, tm.role, tm.brands, tm.phone || null, tm.id ); console.log(`✅ Created user account for team member: ${tm.name}`); } } // Seed data only if tables are empty const memberCount = db.prepare('SELECT COUNT(*) as count FROM team_members').get().count; if (memberCount === 0) { seedData(); } // Seed default superadmin if no users exist const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get().count; if (userCount === 0) { seedDefaultUser(); } } function seedData() { const allBrands = JSON.stringify([ 'Samaya Investment', 'Hira Cultural District', 'Holy Quran Museum', 'Al-Safiya Museum', 'Hayhala', 'Jabal Thawr', 'Coffee Chain', 'Taibah Gifts' ]); const someBrands = JSON.stringify([ 'Samaya Investment', 'Hira Cultural District', 'Holy Quran Museum', 'Al-Safiya Museum' ]); const mostAccounts = JSON.stringify([ 'Samaya Investment', 'Hira Cultural District', 'Holy Quran Museum', 'Al-Safiya Museum', 'Hayhala', 'Jabal Thawr', 'Coffee Chain' ]); const religiousExhibitions = JSON.stringify([ 'Holy Quran Museum', 'Al-Safiya Museum', 'Jabal Thawr' ]); const insertMember = db.prepare(` INSERT INTO team_members (name, email, role, brands) VALUES (?, ?, ?, ?) `); const members = [ ['Dr. Muhammad Al-Sayed', 'muhammad.alsayed@samaya.sa', 'approver', allBrands], ['Dr. Fahd Al-Thumairi', 'fahd.thumairi@samaya.sa', 'approver', someBrands], ['Fahda Abdul Aziz', 'fahda@samaya.sa', 'publisher', mostAccounts], ['Sara Al-Zahrani', 'sara@samaya.sa', 'content_creator', JSON.stringify(['Samaya Investment', 'Hira Cultural District', 'Coffee Chain'])], ['Noura', 'noura@samaya.sa', 'content_creator', JSON.stringify(['Samaya Investment', 'Hira Cultural District', 'Hayhala', 'Taibah Gifts'])], ['Saeed Ghanem', 'saeed@samaya.sa', 'content_creator', religiousExhibitions], ['Anas Mater', 'anas@samaya.sa', 'producer', JSON.stringify(['Samaya Investment', 'Hira Cultural District'])], ['Muhammad Nu\'man', 'numan@samaya.sa', 'manager', JSON.stringify(['Google Maps'])], ['Fahed', 'fahed@samaya.sa', 'manager', allBrands], ]; const insertMembers = db.transaction(() => { for (const m of members) { insertMember.run(...m); } }); insertMembers(); // Seed brands const insertBrand = db.prepare(` INSERT INTO brands (name, priority, color, icon) VALUES (?, ?, ?, ?) `); const brands = [ ['Samaya Investment', 1, '#1E3A5F', '🏢'], ['Hira Cultural District', 1, '#8B4513', '🏛️'], ['Holy Quran Museum', 1, '#2E7D32', '📖'], ['Al-Safiya Museum', 1, '#6A1B9A', '🏺'], ['Hayhala', 1, '#C62828', '🎭'], ['Jabal Thawr', 1, '#4E342E', '⛰️'], ['Coffee Chain', 2, '#795548', '☕'], ['Taibah Gifts', 3, '#E65100', '🎁'], ]; const insertBrands = db.transaction(() => { for (const b of brands) { insertBrand.run(...b); } }); insertBrands(); console.log('✅ Database seeded with team members and brands'); } function seedDefaultUser() { const passwordHash = bcrypt.hashSync('admin123', 10); const insertUser = db.prepare(` INSERT INTO users (name, email, password_hash, role) VALUES (?, ?, ?, ?) `); insertUser.run('Fahed Muhaidi', 'f.mahidi@samayainvest.com', passwordHash, 'superadmin'); console.log('✅ Default superadmin created (email: f.mahidi@samayainvest.com, password: admin123)'); } module.exports = { db, initialize };