From a9035357eceb49b3138824d2ee8fd613f964de77 Mon Sep 17 00:00:00 2001 From: soufiane Date: Fri, 5 Dec 2025 11:49:35 +0100 Subject: [PATCH] feat: add active/inactive clients count to statistics API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add activeClients and inactiveClients to /api/admin/statistics response - Count clients with is_active = TRUE/FALSE 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- scripts/compare-db.js | 70 ++++++++++++++ scripts/fix-preprod-schema.js | 140 ++++++++++++++++++++++++++++ src/controllers/admin.controller.js | 4 + 3 files changed, 214 insertions(+) create mode 100644 scripts/compare-db.js create mode 100644 scripts/fix-preprod-schema.js diff --git a/scripts/compare-db.js b/scripts/compare-db.js new file mode 100644 index 00000000..3b40ce19 --- /dev/null +++ b/scripts/compare-db.js @@ -0,0 +1,70 @@ +import pkg from 'pg'; +const { Pool } = pkg; + +const devPool = new Pool({ + host: '51.75.24.29', + port: 5433, + user: 'postgres', + password: 'postgres', + database: 'thetiptop_dev' +}); + +const preprodPool = new Pool({ + host: '51.75.24.29', + port: 5434, + user: 'postgres', + password: 'postgres', + database: 'thetiptop_preprod' +}); + +const tables = ['users', 'prizes', 'tickets', 'game_settings', 'newsletters', 'email_templates', 'email_campaigns', 'email_campaign_recipients', 'grand_prize_draws']; + +console.log('=== COMPARAISON DES COLONNES DEV vs PREPROD ===\n'); + +const missingColumns = []; + +for (const table of tables) { + const devCols = await devPool.query( + "SELECT column_name FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position", + [table] + ); + + const preprodCols = await preprodPool.query( + "SELECT column_name FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position", + [table] + ); + + const devSet = new Set(devCols.rows.map(r => r.column_name)); + const preprodSet = new Set(preprodCols.rows.map(r => r.column_name)); + + const missingInPreprod = [...devSet].filter(c => !preprodSet.has(c)); + const extraInPreprod = [...preprodSet].filter(c => !devSet.has(c)); + + if (missingInPreprod.length > 0 || extraInPreprod.length > 0) { + console.log('❌ ' + table.toUpperCase()); + if (missingInPreprod.length > 0) { + console.log(' Manquantes en preprod: ' + missingInPreprod.join(', ')); + missingColumns.push({ table, columns: missingInPreprod }); + } + if (extraInPreprod.length > 0) { + console.log(' En plus en preprod: ' + extraInPreprod.join(', ')); + } + console.log(''); + } else { + console.log('✅ ' + table.toUpperCase() + ' - OK'); + } +} + +console.log('\n=== RÉSUMÉ ==='); +if (missingColumns.length === 0) { + console.log('✅ Toutes les colonnes sont synchronisées !'); +} else { + console.log('❌ Colonnes manquantes à ajouter en preprod:'); + for (const m of missingColumns) { + console.log(' - ' + m.table + ': ' + m.columns.join(', ')); + } +} + +await devPool.end(); +await preprodPool.end(); +process.exit(0); diff --git a/scripts/fix-preprod-schema.js b/scripts/fix-preprod-schema.js new file mode 100644 index 00000000..b63a2068 --- /dev/null +++ b/scripts/fix-preprod-schema.js @@ -0,0 +1,140 @@ +import pkg from 'pg'; +const { Pool } = pkg; + +const preprodPool = new Pool({ + host: '51.75.24.29', + port: 5434, + user: 'postgres', + password: 'postgres', + database: 'thetiptop_preprod' +}); + +console.log('=== CORRECTION DU SCHÉMA PREPROD ===\n'); + +// 1. TICKETS - ajouter colonnes de livraison +console.log('📦 Correction de TICKETS...'); +await preprodPool.query(` + ALTER TABLE tickets + ADD COLUMN IF NOT EXISTS delivered_at TIMESTAMP, + ADD COLUMN IF NOT EXISTS delivered_by UUID REFERENCES users(id), + ADD COLUMN IF NOT EXISTS delivery_notes TEXT +`); +console.log('✅ tickets: delivered_at, delivered_by, delivery_notes ajoutées\n'); + +// 2. NEWSLETTERS - corriger is_active +console.log('📦 Correction de NEWSLETTERS...'); +await preprodPool.query(`ALTER TABLE newsletters RENAME COLUMN is_subscribed TO is_active`); +console.log('✅ newsletters: is_subscribed renommée en is_active\n'); + +// 3. EMAIL_TEMPLATES - recréer avec bonnes colonnes +console.log('📦 Correction de EMAIL_TEMPLATES...'); +await preprodPool.query(`DROP TABLE IF EXISTS email_templates CASCADE`); +await preprodPool.query(` + CREATE TABLE email_templates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + description TEXT, + subject VARCHAR(500), + html_content TEXT NOT NULL, + text_content TEXT, + category VARCHAR(100), + is_active BOOLEAN DEFAULT TRUE, + created_by UUID REFERENCES users(id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) +`); +console.log('✅ email_templates recréée avec toutes les colonnes\n'); + +// 4. EMAIL_CAMPAIGNS - recréer avec bonnes colonnes +console.log('📦 Correction de EMAIL_CAMPAIGNS...'); +await preprodPool.query(`DROP TABLE IF EXISTS email_campaign_recipients CASCADE`); +await preprodPool.query(`DROP TABLE IF EXISTS email_campaigns CASCADE`); +await preprodPool.query(` + CREATE TABLE email_campaigns ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + subject VARCHAR(500) NOT NULL, + template_html TEXT NOT NULL, + template_text TEXT, + created_by UUID NOT NULL REFERENCES users(id), + status VARCHAR(50) DEFAULT 'DRAFT', + scheduled_at TIMESTAMP, + sent_at TIMESTAMP, + recipient_count INTEGER DEFAULT 0, + opened_count INTEGER DEFAULT 0, + clicked_count INTEGER DEFAULT 0, + criteria JSONB, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) +`); +console.log('✅ email_campaigns recréée avec toutes les colonnes\n'); + +// 5. EMAIL_CAMPAIGN_RECIPIENTS - recréer avec bonnes colonnes +console.log('📦 Correction de EMAIL_CAMPAIGN_RECIPIENTS...'); +await preprodPool.query(` + CREATE TABLE email_campaign_recipients ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + campaign_id UUID NOT NULL REFERENCES email_campaigns(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id), + email VARCHAR(255) NOT NULL, + status VARCHAR(50) DEFAULT 'PENDING', + sent_at TIMESTAMP, + opened_at TIMESTAMP, + clicked_at TIMESTAMP, + unsubscribed_at TIMESTAMP, + error_message TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) +`); +console.log('✅ email_campaign_recipients recréée avec toutes les colonnes\n'); + +// 6. GRAND_PRIZE_DRAWS - recréer avec bonnes colonnes +console.log('📦 Correction de GRAND_PRIZE_DRAWS...'); +await preprodPool.query(`DROP TABLE IF EXISTS grand_prize_draws CASCADE`); +await preprodPool.query(` + CREATE TABLE grand_prize_draws ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + draw_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + conducted_by UUID NOT NULL REFERENCES users(id), + winner_id UUID NOT NULL REFERENCES users(id), + winner_email VARCHAR(255) NOT NULL, + winner_name VARCHAR(255) NOT NULL, + prize_name VARCHAR(255) NOT NULL, + prize_value VARCHAR(100), + total_participants INTEGER NOT NULL, + eligible_participants INTEGER NOT NULL, + criteria JSONB, + status VARCHAR(50) DEFAULT 'COMPLETED', + notified_at TIMESTAMP, + claimed_at TIMESTAMP, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT unique_grand_prize_draw UNIQUE (draw_date) + ) +`); +console.log('✅ grand_prize_draws recréée avec toutes les colonnes\n'); + +// Créer les index +console.log('📦 Création des index...'); +await preprodPool.query(` + CREATE INDEX IF NOT EXISTS idx_email_campaigns_status ON email_campaigns(status); + CREATE INDEX IF NOT EXISTS idx_email_campaigns_created_by ON email_campaigns(created_by); + CREATE INDEX IF NOT EXISTS idx_email_campaigns_scheduled_at ON email_campaigns(scheduled_at); + CREATE INDEX IF NOT EXISTS idx_email_campaign_recipients_campaign ON email_campaign_recipients(campaign_id); + CREATE INDEX IF NOT EXISTS idx_email_campaign_recipients_user ON email_campaign_recipients(user_id); + CREATE INDEX IF NOT EXISTS idx_email_campaign_recipients_status ON email_campaign_recipients(status); + CREATE INDEX IF NOT EXISTS idx_email_templates_category ON email_templates(category); + CREATE INDEX IF NOT EXISTS idx_email_templates_is_active ON email_templates(is_active); + CREATE INDEX IF NOT EXISTS idx_grand_prize_draws_winner ON grand_prize_draws(winner_id); + CREATE INDEX IF NOT EXISTS idx_grand_prize_draws_date ON grand_prize_draws(draw_date); + CREATE INDEX IF NOT EXISTS idx_grand_prize_draws_status ON grand_prize_draws(status); +`); +console.log('✅ Index créés\n'); + +console.log('=== CORRECTION TERMINÉE ==='); + +await preprodPool.end(); +process.exit(0); diff --git a/src/controllers/admin.controller.js b/src/controllers/admin.controller.js index d7725b4d..51e0755a 100644 --- a/src/controllers/admin.controller.js +++ b/src/controllers/admin.controller.js @@ -19,6 +19,8 @@ export const getStatistics = asyncHandler(async (req, res) => { SELECT COUNT(*) as total_users, COUNT(CASE WHEN role = 'CLIENT' THEN 1 END) as clients, + COUNT(CASE WHEN role = 'CLIENT' AND is_active = TRUE THEN 1 END) as active_clients, + COUNT(CASE WHEN role = 'CLIENT' AND is_active = FALSE THEN 1 END) as inactive_clients, COUNT(CASE WHEN role = 'EMPLOYEE' THEN 1 END) as employees, COUNT(CASE WHEN role = 'ADMIN' THEN 1 END) as admins, COUNT(CASE WHEN is_verified = TRUE THEN 1 END) as verified_users @@ -154,6 +156,8 @@ export const getStatistics = asyncHandler(async (req, res) => { users: { total: parseInt(usersStats.rows[0].total_users), clients: parseInt(usersStats.rows[0].clients), + activeClients: parseInt(usersStats.rows[0].active_clients), + inactiveClients: parseInt(usersStats.rows[0].inactive_clients), employees: parseInt(usersStats.rows[0].employees), admins: parseInt(usersStats.rows[0].admins), verifiedEmails: parseInt(usersStats.rows[0].verified_users)