the-tip-top-backend/src/controllers/admin.controller.js
soufiane a9035357ec feat: add active/inactive clients count to statistics API
- 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 <noreply@anthropic.com>
2025-12-05 11:49:35 +01:00

918 lines
26 KiB
JavaScript

/**
* Controller admin
*/
import bcrypt from 'bcrypt';
import { pool } from '../../db.js';
import { AppError, asyncHandler } from '../middleware/errorHandler.js';
// ============================================
// STATISTIQUES
// ============================================
/**
* Récupérer les statistiques globales
* GET /api/admin/statistics
*/
export const getStatistics = asyncHandler(async (req, res) => {
// Statistiques des utilisateurs
const usersStats = await pool.query(`
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
FROM users
`);
// Statistiques des tickets (mises à jour avec distributed et used)
const ticketsStats = await pool.query(`
SELECT
COUNT(*) as total_tickets,
COUNT(CASE WHEN user_id IS NOT NULL THEN 1 END) as distributed,
COUNT(CASE WHEN played_at IS NOT NULL THEN 1 END) as used,
COUNT(CASE WHEN status = 'PENDING' THEN 1 END) as pending,
COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as claimed,
COUNT(CASE WHEN status = 'REJECTED' THEN 1 END) as rejected,
COUNT(CASE WHEN validated_at IS NOT NULL THEN 1 END) as validated
FROM tickets
`);
// Statistiques des prix avec répartition par catégorie
const prizesStats = await pool.query(`
SELECT
COUNT(DISTINCT p.id) as total_prizes,
COUNT(DISTINCT CASE WHEN p.is_active = TRUE THEN p.id END) as active_prizes,
SUM(p.stock) as total_stock,
COUNT(t.id) as distributed
FROM prizes p
LEFT JOIN tickets t ON p.id = t.prize_id
`);
// Répartition des lots gagnés par catégorie
const prizeDistribution = await pool.query(`
SELECT
p.id as prize_id,
p.name as prize_name,
p.type as prize_type,
COUNT(t.id) as count,
CASE
WHEN (SELECT COUNT(*) FROM tickets) > 0
THEN ROUND((COUNT(t.id)::DECIMAL / (SELECT COUNT(*) FROM tickets)) * 100, 2)
ELSE 0
END as percentage
FROM prizes p
LEFT JOIN tickets t ON p.id = t.prize_id
WHERE p.type != 'GRAND_PRIZE'
GROUP BY p.id, p.name, p.type
ORDER BY count DESC
`);
// Statistiques démographiques - Genre
const genderStats = await pool.query(`
SELECT
COUNT(CASE WHEN gender = 'MALE' THEN 1 END) as male,
COUNT(CASE WHEN gender = 'FEMALE' THEN 1 END) as female,
COUNT(CASE WHEN gender = 'OTHER' THEN 1 END) as other,
COUNT(CASE WHEN gender = 'NOT_SPECIFIED' OR gender IS NULL THEN 1 END) as not_specified
FROM users
WHERE role = 'CLIENT'
`);
// Statistiques démographiques - Tranches d'âge
const ageStats = await pool.query(`
SELECT
age_range as range,
COUNT(*) as count
FROM (
SELECT
CASE
WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, date_of_birth)) BETWEEN 0 AND 17 THEN '0-17'
WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, date_of_birth)) BETWEEN 18 AND 25 THEN '18-25'
WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, date_of_birth)) BETWEEN 26 AND 35 THEN '26-35'
WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, date_of_birth)) BETWEEN 36 AND 45 THEN '36-45'
WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, date_of_birth)) BETWEEN 46 AND 55 THEN '46-55'
WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, date_of_birth)) BETWEEN 56 AND 65 THEN '56-65'
WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, date_of_birth)) > 65 THEN '65+'
ELSE 'Non spécifié'
END as age_range
FROM users
WHERE role = 'CLIENT'
) subquery
GROUP BY age_range
ORDER BY
CASE age_range
WHEN '0-17' THEN 1
WHEN '18-25' THEN 2
WHEN '26-35' THEN 3
WHEN '36-45' THEN 4
WHEN '46-55' THEN 5
WHEN '56-65' THEN 6
WHEN '65+' THEN 7
ELSE 8
END
`);
// Calculer les pourcentages pour les tranches d'âge
const totalClients = parseInt(usersStats.rows[0].clients) || 1;
const ageRanges = ageStats.rows.map(row => ({
range: row.range,
count: parseInt(row.count),
percentage: parseFloat(((parseInt(row.count) / totalClients) * 100).toFixed(2))
}));
// Statistiques démographiques - Top villes
const topCities = await pool.query(`
SELECT
city,
COUNT(*) as count,
ROUND((COUNT(*)::DECIMAL / (SELECT COUNT(*) FROM users WHERE role = 'CLIENT')) * 100, 2) as percentage
FROM users
WHERE role = 'CLIENT' AND city IS NOT NULL AND city != ''
GROUP BY city
ORDER BY count DESC
LIMIT 10
`);
// Statistiques du jeu
const gameStats = await pool.query(`
SELECT
start_date,
end_date,
is_active,
total_tickets,
tickets_generated
FROM game_settings
ORDER BY created_at DESC
LIMIT 1
`);
// Formater la réponse selon le format attendu par le frontend
res.json({
success: true,
data: {
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)
},
tickets: {
total: parseInt(ticketsStats.rows[0].total_tickets),
distributed: parseInt(ticketsStats.rows[0].distributed),
used: parseInt(ticketsStats.rows[0].used),
pending: parseInt(ticketsStats.rows[0].pending),
validated: parseInt(ticketsStats.rows[0].validated),
rejected: parseInt(ticketsStats.rows[0].rejected),
claimed: parseInt(ticketsStats.rows[0].claimed)
},
prizes: {
total: parseInt(prizesStats.rows[0].total_prizes),
active: parseInt(prizesStats.rows[0].active_prizes),
totalStock: parseInt(prizesStats.rows[0].total_stock),
distributed: parseInt(prizesStats.rows[0].distributed),
byCategory: prizeDistribution.rows.map(prize => ({
prizeId: prize.prize_id,
prizeName: prize.prize_name,
prizeType: prize.prize_type,
count: parseInt(prize.count),
percentage: parseFloat(prize.percentage)
}))
},
demographics: {
gender: {
male: parseInt(genderStats.rows[0].male),
female: parseInt(genderStats.rows[0].female),
other: parseInt(genderStats.rows[0].other),
notSpecified: parseInt(genderStats.rows[0].not_specified)
},
ageRanges: ageRanges,
topCities: topCities.rows.map(city => ({
city: city.city,
count: parseInt(city.count),
percentage: parseFloat(city.percentage)
}))
},
game: gameStats.rows[0] || null
}
});
});
// ============================================
// GESTION DES PRIX
// ============================================
/**
* Récupérer tous les prix
* GET /api/admin/prizes
*/
export const getAllPrizes = asyncHandler(async (req, res) => {
const result = await pool.query(`
SELECT
p.*,
COALESCE(COUNT(t.id), 0) as total_tickets,
COALESCE(COUNT(CASE WHEN t.played_at IS NOT NULL THEN 1 END), 0) as tickets_used,
COALESCE(COUNT(t.id), 0) as initial_stock
FROM prizes p
LEFT JOIN tickets t ON p.id = t.prize_id
GROUP BY p.id
ORDER BY p.created_at DESC
`);
// Transform snake_case to camelCase for frontend
const transformedData = result.rows.map(row => {
// Pour GRAND_PRIZE, utiliser le stock de la table prizes car il n'y a pas de tickets créés
const isGrandPrize = row.type === 'GRAND_PRIZE';
const initialStock = isGrandPrize ? parseInt(row.stock) : parseInt(row.initial_stock) || 0;
return {
...row,
initialStock: initialStock,
ticketsUsed: parseInt(row.tickets_used) || 0,
isActive: row.is_active,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
});
res.json({
success: true,
data: transformedData,
});
});
/**
* Créer un nouveau prix
* POST /api/admin/prizes
*/
export const createPrize = asyncHandler(async (req, res) => {
const { type, name, description, value, stock, probability, isActive } = req.body;
const result = await pool.query(
`INSERT INTO prizes (type, name, description, value, stock, probability, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *`,
[type, name, description, value, stock, probability, isActive ?? true]
);
res.status(201).json({
success: true,
message: 'Prix créé avec succès',
data: result.rows[0],
});
});
/**
* Mettre à jour un prix
* PUT /api/admin/prizes/:id
*/
export const updatePrize = asyncHandler(async (req, res, next) => {
const { id } = req.params;
const { name, description, value, stock, probability, isActive } = req.body;
// Construire la requête dynamiquement
const updates = [];
const values = [];
let paramCount = 1;
if (name !== undefined) {
updates.push(`name = $${paramCount++}`);
values.push(name);
}
if (description !== undefined) {
updates.push(`description = $${paramCount++}`);
values.push(description);
}
if (value !== undefined) {
updates.push(`value = $${paramCount++}`);
values.push(value);
}
if (stock !== undefined) {
updates.push(`stock = $${paramCount++}`);
values.push(stock);
}
if (probability !== undefined) {
updates.push(`probability = $${paramCount++}`);
values.push(probability);
}
if (isActive !== undefined) {
updates.push(`is_active = $${paramCount++}`);
values.push(isActive);
}
if (updates.length === 0) {
return next(new AppError('Aucune modification à apporter', 400));
}
values.push(id);
const query = `
UPDATE prizes
SET ${updates.join(', ')}
WHERE id = $${paramCount}
RETURNING *
`;
const result = await pool.query(query, values);
if (result.rows.length === 0) {
return next(new AppError('Prix non trouvé', 404));
}
res.json({
success: true,
message: 'Prix mis à jour avec succès',
data: result.rows[0],
});
});
/**
* Supprimer un prix (désactiver plutôt que supprimer)
* DELETE /api/admin/prizes/:id
*/
export const deletePrize = asyncHandler(async (req, res, next) => {
const { id } = req.params;
// Désactiver le prix plutôt que le supprimer pour garder l'historique
const result = await pool.query(
'UPDATE prizes SET is_active = FALSE WHERE id = $1 RETURNING *',
[id]
);
if (result.rows.length === 0) {
return next(new AppError('Prix non trouvé', 404));
}
res.json({
success: true,
message: 'Prix désactivé avec succès',
});
});
// ============================================
// GESTION DES UTILISATEURS
// ============================================
/**
* Récupérer tous les utilisateurs
* GET /api/admin/users
*/
export const getAllUsers = asyncHandler(async (req, res) => {
const { page = 1, limit = 10, role, isActive } = req.query;
const offset = (page - 1) * limit;
// Construction de la requête avec filtrage optionnel
let queryText = `
SELECT
u.id,
u.email,
u.first_name,
u.last_name,
u.phone,
u.role,
u.is_verified,
u.is_active,
u.created_at,
COUNT(t.id) as tickets_count
FROM users u
LEFT JOIN tickets t ON u.id = t.user_id
`;
const queryParams = [];
const whereClauses = [];
let paramIndex = 1;
// Ajouter le filtre par rôle si présent
if (role) {
whereClauses.push(`u.role = $${paramIndex}`);
queryParams.push(role);
paramIndex++;
}
// Ajouter le filtre par statut actif si présent
if (isActive !== undefined) {
whereClauses.push(`u.is_active = $${paramIndex}`);
queryParams.push(isActive === 'true');
paramIndex++;
}
if (whereClauses.length > 0) {
queryText += ` WHERE ${whereClauses.join(' AND ')}`;
}
queryText += ` GROUP BY u.id ORDER BY u.created_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
queryParams.push(limit, offset);
const result = await pool.query(queryText, queryParams);
// Compter le total avec les mêmes filtres
let countQuery = 'SELECT COUNT(*) FROM users';
const countWhereClauses = [];
let countParams = [];
let countParamIndex = 1;
if (role) {
countWhereClauses.push(`role = $${countParamIndex}`);
countParams.push(role);
countParamIndex++;
}
if (isActive !== undefined) {
countWhereClauses.push(`is_active = $${countParamIndex}`);
countParams.push(isActive === 'true');
countParamIndex++;
}
if (countWhereClauses.length > 0) {
countQuery += ` WHERE ${countWhereClauses.join(' AND ')}`;
}
const countResult = await pool.query(countQuery, countParams);
const total = parseInt(countResult.rows[0].count);
res.json({
success: true,
data: {
users: result.rows,
pagination: {
total,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(total / limit),
},
},
});
});
/**
* Créer un employé
* POST /api/admin/employees
*/
export const createEmployee = asyncHandler(async (req, res, next) => {
const { email, password, firstName, lastName, phone } = req.body;
// Vérifier si l'email existe déjà
const existingUser = await pool.query('SELECT id FROM users WHERE email = $1', [email]);
if (existingUser.rows.length > 0) {
return next(new AppError('Cet email est déjà utilisé', 400));
}
// Hasher le mot de passe
const hashedPassword = await bcrypt.hash(password, 10);
// Créer l'employé
const result = await pool.query(
`INSERT INTO users (email, password, first_name, last_name, phone, role, is_verified)
VALUES ($1, $2, $3, $4, $5, 'EMPLOYEE', TRUE)
RETURNING id, email, first_name, last_name, role, created_at`,
[email, hashedPassword, firstName, lastName, phone]
);
res.status(201).json({
success: true,
message: 'Employé créé avec succès',
data: result.rows[0],
});
});
/**
* Récupérer un utilisateur par ID
* GET /api/admin/users/:id
*/
export const getUserById = asyncHandler(async (req, res, next) => {
const { id } = req.params;
const result = await pool.query(
`SELECT
u.id,
u.email,
u.first_name,
u.last_name,
u.phone,
u.address,
u.city,
u.postal_code,
u.role,
u.is_verified,
u.is_active,
u.created_at,
u.date_of_birth,
u.gender,
COUNT(t.id) as tickets_count,
COUNT(CASE WHEN t.status = 'PENDING' THEN 1 END) as pending_tickets,
COUNT(CASE WHEN t.status = 'CLAIMED' THEN 1 END) as claimed_tickets
FROM users u
LEFT JOIN tickets t ON u.id = t.user_id
WHERE u.id = $1
GROUP BY u.id`,
[id]
);
if (result.rows.length === 0) {
return next(new AppError('Utilisateur non trouvé', 404));
}
res.json({
success: true,
data: result.rows[0],
});
});
/**
* Mettre à jour un utilisateur
* PUT /api/admin/users/:id
*/
export const updateUser = asyncHandler(async (req, res, next) => {
const { id } = req.params;
const { role, isVerified, isActive } = req.body;
// Construire la requête dynamiquement
const updates = [];
const values = [];
let paramCount = 1;
if (role !== undefined) {
updates.push(`role = $${paramCount++}`);
values.push(role);
}
if (isVerified !== undefined) {
updates.push(`is_verified = $${paramCount++}`);
values.push(isVerified);
}
if (isActive !== undefined) {
updates.push(`is_active = $${paramCount++}`);
values.push(isActive);
}
if (updates.length === 0) {
return next(new AppError('Aucune modification à apporter', 400));
}
values.push(id);
const query = `
UPDATE users
SET ${updates.join(', ')}
WHERE id = $${paramCount}
RETURNING id, email, first_name, last_name, role, is_verified, is_active
`;
const result = await pool.query(query, values);
if (result.rows.length === 0) {
return next(new AppError('Utilisateur non trouvé', 404));
}
res.json({
success: true,
message: 'Utilisateur mis à jour avec succès',
data: result.rows[0],
});
});
/**
* Supprimer un utilisateur
* DELETE /api/admin/users/:id
*/
export const deleteUser = asyncHandler(async (req, res, next) => {
const { id } = req.params;
// Ne pas permettre de supprimer son propre compte
if (id === req.user.id) {
return next(new AppError('Vous ne pouvez pas supprimer votre propre compte', 400));
}
const result = await pool.query('DELETE FROM users WHERE id = $1 RETURNING id', [id]);
if (result.rows.length === 0) {
return next(new AppError('Utilisateur non trouvé', 404));
}
res.json({
success: true,
message: 'Utilisateur supprimé avec succès',
});
});
// ============================================
// GESTION DES TICKETS
// ============================================
/**
* Récupérer tous les tickets
* GET /api/admin/tickets
*/
export const getAllTickets = asyncHandler(async (req, res) => {
console.log('🚀 getAllTickets appelé avec query:', req.query);
const { page = 1, limit = 20, status, prizeType } = req.query;
const offset = (page - 1) * limit;
console.log('📋 Paramètres extraits:', { page, limit, status, prizeType, offset });
let query = `
SELECT
t.id,
t.code,
t.status,
t.played_at,
t.claimed_at,
t.validated_at,
t.created_at,
u.email as user_email,
u.first_name || ' ' || u.last_name as user_name,
p.name as prize_name,
p.type as prize_type,
p.value as prize_value,
v.first_name || ' ' || v.last_name as validated_by_name
FROM tickets t
LEFT JOIN users u ON t.user_id = u.id
LEFT JOIN prizes p ON t.prize_id = p.id
LEFT JOIN users v ON t.validated_by = v.id
`;
const params = [];
let paramCount = 1;
const whereClauses = [];
// Filtre par statut
if (status) {
whereClauses.push(`t.status = $${paramCount++}`);
params.push(status);
}
// Filtre par type de lot
if (prizeType) {
whereClauses.push(`p.type = $${paramCount++}`);
params.push(prizeType);
}
// Ajouter les clauses WHERE si nécessaire
if (whereClauses.length > 0) {
query += ' WHERE ' + whereClauses.join(' AND ');
}
query += ` ORDER BY t.played_at DESC NULLS LAST, t.created_at DESC LIMIT $${paramCount++} OFFSET $${paramCount}`;
params.push(limit, offset);
// Log pour debug
console.log('🔍 SQL Query:', query);
console.log('🔍 Params:', params);
const result = await pool.query(query, params);
console.log('✅ Résultats:', result.rows.length, 'tickets');
if (result.rows.length > 0) {
console.log('📦 Premier ticket:', { code: result.rows[0].code, prize: result.rows[0].prize_name, type: result.rows[0].prize_type });
}
// Compter le total
let countQuery = 'SELECT COUNT(*) FROM tickets t LEFT JOIN prizes p ON t.prize_id = p.id';
const countParams = [];
const countWhereClauses = [];
let countParamCount = 1;
if (status) {
countWhereClauses.push(`t.status = $${countParamCount++}`);
countParams.push(status);
}
if (prizeType) {
countWhereClauses.push(`p.type = $${countParamCount++}`);
countParams.push(prizeType);
}
if (countWhereClauses.length > 0) {
countQuery += ' WHERE ' + countWhereClauses.join(' AND ');
}
const countResult = await pool.query(countQuery, countParams);
const total = parseInt(countResult.rows[0].count);
// Récupérer les stats globales (tous les tickets, sans pagination)
const statsQuery = `
SELECT
COUNT(*) FILTER (WHERE status = 'PENDING') as pending,
COUNT(*) FILTER (WHERE status = 'CLAIMED') as claimed,
COUNT(*) FILTER (WHERE status = 'REJECTED') as rejected
FROM tickets
`;
const statsResult = await pool.query(statsQuery);
const stats = {
pending: parseInt(statsResult.rows[0].pending) || 0,
claimed: parseInt(statsResult.rows[0].claimed) || 0,
rejected: parseInt(statsResult.rows[0].rejected) || 0,
};
console.log('📊 Stats calculées:', stats);
res.json({
success: true,
data: result.rows,
total,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(total / limit),
stats,
});
});
// ============================================
// GÉNÉRATION DE TICKETS EN MASSE
// ============================================
/**
* Générer des tickets en masse
* POST /api/admin/generate-tickets
*/
export const generateTickets = asyncHandler(async (req, res) => {
const { quantity } = req.body;
if (!quantity || quantity < 1 || quantity > 1000000) {
throw new AppError('La quantité doit être entre 1 et 1,000,000', 400);
}
// Récupérer tous les lots actifs avec leurs probabilités
const prizesResult = await pool.query(
`SELECT id, name, probability, stock
FROM prizes
WHERE is_active = TRUE AND stock > 0
ORDER BY probability DESC`
);
const prizes = prizesResult.rows;
if (prizes.length === 0) {
throw new AppError('Aucun lot actif avec du stock disponible', 400);
}
// Calculer la somme totale des probabilités
const totalProbability = prizes.reduce((sum, prize) => sum + parseFloat(prize.probability), 0);
if (totalProbability === 0) {
throw new AppError('La somme des probabilités est 0', 400);
}
// Générer les tickets
const tickets = [];
const codes = new Set();
console.log(`🎫 Génération de ${quantity} tickets...`);
for (let i = 0; i < quantity; i++) {
// Générer un code unique
let code;
do {
code = generateTicketCode();
} while (codes.has(code));
codes.add(code);
// Sélectionner un lot selon la probabilité
const random = Math.random() * totalProbability;
let cumulativeProbability = 0;
let selectedPrize = prizes[0];
for (const prize of prizes) {
cumulativeProbability += parseFloat(prize.probability);
if (random <= cumulativeProbability) {
selectedPrize = prize;
break;
}
}
tickets.push({
code,
prizeId: selectedPrize.id,
});
// Log de progression tous les 10000 tickets
if ((i + 1) % 10000 === 0) {
console.log(`${i + 1}/${quantity} tickets générés`);
}
}
console.log(`📊 Insertion de ${tickets.length} tickets dans la base de données...`);
// Insérer les tickets en batch (par lots de 1000 pour éviter les problèmes de mémoire)
const batchSize = 1000;
let inserted = 0;
for (let i = 0; i < tickets.length; i += batchSize) {
const batch = tickets.slice(i, i + batchSize);
// Construire la requête d'insertion
const values = batch.map((t, idx) => {
return `($${idx * 2 + 1}, $${idx * 2 + 2})`;
}).join(', ');
const params = batch.flatMap(t => [t.code, t.prizeId]);
await pool.query(
`INSERT INTO tickets (code, prize_id) VALUES ${values}`,
params
);
inserted += batch.length;
console.log(`${inserted}/${tickets.length} tickets insérés`);
}
console.log(`${inserted} tickets générés avec succès!`);
res.json({
success: true,
message: `${inserted} tickets générés avec succès`,
data: {
generated: inserted,
},
});
});
/**
* Générer un code ticket aléatoire unique
*/
function generateTicketCode() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Sans I, O, 0, 1 pour éviter confusion
let code = '';
for (let i = 0; i < 10; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
return code;
}
/**
* Exporter les données marketing pour emailing
* GET /api/admin/marketing/export
*/
export const exportMarketingData = asyncHandler(async (req, res) => {
const { segment } = req.query;
// Construction de la requête selon le segment
let whereClause = "WHERE u.role = 'CLIENT'";
if (segment === 'active') {
// Utilisateurs qui ont joué au moins 1 ticket
whereClause += " AND (SELECT COUNT(*) FROM tickets t WHERE t.user_id = u.id AND t.played_at IS NOT NULL) > 0";
} else if (segment === 'winners') {
// Utilisateurs qui ont gagné au moins 1 lot
whereClause += " AND (SELECT COUNT(*) FROM tickets t WHERE t.user_id = u.id AND t.status = 'CLAIMED') > 0";
} else if (segment === 'verified') {
// Utilisateurs vérifiés uniquement
whereClause += " AND u.is_verified = TRUE";
}
const query = `
SELECT
u.id,
u.email,
u.first_name,
u.last_name,
u.phone,
u.city,
u.postal_code,
u.gender,
u.date_of_birth,
u.is_verified,
u.created_at,
COALESCE(COUNT(DISTINCT t.id), 0) as tickets_played,
COALESCE(COUNT(DISTINCT CASE WHEN t.status = 'CLAIMED' THEN t.id END), 0) as prizes_won,
COALESCE(MAX(t.played_at), NULL) as last_activity
FROM users u
LEFT JOIN tickets t ON u.id = t.user_id AND t.played_at IS NOT NULL
${whereClause}
GROUP BY u.id, u.email, u.first_name, u.last_name, u.phone, u.city,
u.postal_code, u.gender, u.date_of_birth, u.is_verified, u.created_at
ORDER BY u.created_at DESC
`;
const result = await pool.query(query);
res.json({
success: true,
data: {
users: result.rows,
total: result.rows.length,
segment: segment || 'all',
exportedAt: new Date().toISOString(),
},
});
});
export default {
getStatistics,
getAllPrizes,
createPrize,
updatePrize,
deletePrize,
getAllUsers,
getUserById,
createEmployee,
updateUser,
deleteUser,
getAllTickets,
generateTickets,
exportMarketingData,
};