the-tip-top-backend/src/controllers/employee.controller.js
soufiane e72923ec86 fix: transform pending tickets data to match frontend expectations
Updated the getPendingTickets endpoint to return nested objects for user
and prize data instead of flat SQL columns. Frontend expects structure like
ticket.user.firstName and ticket.prize.name, which now displays correctly
in the employee verification interface instead of showing N/A.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 14:30:44 +01:00

377 lines
9.2 KiB
JavaScript

/**
* Controller employé
*/
import { pool } from '../../db.js';
import { AppError, asyncHandler } from '../middleware/errorHandler.js';
/**
* Récupérer les tickets en attente de validation
* GET /api/employee/pending-tickets
*/
export const getPendingTickets = asyncHandler(async (req, res) => {
const { page = 1, limit = 20 } = req.query;
const offset = (page - 1) * limit;
const result = await pool.query(
`SELECT
t.id,
t.code,
t.status,
t.played_at,
u.id as user_id,
u.email as user_email,
u.first_name as user_first_name,
u.last_name as user_last_name,
u.phone as user_phone,
p.id as prize_id,
p.name as prize_name,
p.type as prize_type,
p.value as prize_value,
p.description as prize_description
FROM tickets t
JOIN users u ON t.user_id = u.id
JOIN prizes p ON t.prize_id = p.id
WHERE t.status = 'PENDING'
ORDER BY t.played_at ASC
LIMIT $1 OFFSET $2`,
[limit, offset]
);
// Compter le total
const countResult = await pool.query(
'SELECT COUNT(*) FROM tickets WHERE status = $1',
['PENDING']
);
const total = parseInt(countResult.rows[0].count);
// Transform data to match frontend expectations
const transformedData = result.rows.map(row => ({
id: row.id,
code: row.code,
status: row.status,
playedAt: row.played_at,
user: {
id: row.user_id,
email: row.user_email,
firstName: row.user_first_name,
lastName: row.user_last_name,
phone: row.user_phone,
},
prize: {
id: row.prize_id,
name: row.prize_name,
type: row.prize_type,
value: row.prize_value,
description: row.prize_description,
},
}));
res.json({
success: true,
data: transformedData,
pagination: {
total,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(total / limit),
},
});
});
/**
* Rechercher un ticket par code
* GET /api/employee/search-ticket
*/
export const searchTicket = asyncHandler(async (req, res, next) => {
const { code } = req.query;
const result = await pool.query(
`SELECT
t.id,
t.code,
t.status,
t.played_at,
t.claimed_at,
t.validated_at,
t.rejection_reason,
u.id as user_id,
u.email as user_email,
u.first_name || ' ' || u.last_name as user_name,
u.phone as user_phone,
p.id as prize_id,
p.name as prize_name,
p.type as prize_type,
p.value as prize_value,
p.description as prize_description,
v.first_name || ' ' || v.last_name as validated_by_name
FROM tickets t
JOIN users u ON t.user_id = u.id
JOIN prizes p ON t.prize_id = p.id
LEFT JOIN users v ON t.validated_by = v.id
WHERE t.code = $1`,
[code]
);
if (result.rows.length === 0) {
return next(new AppError('Ticket non trouvé', 404));
}
res.json({
success: true,
data: result.rows[0],
});
});
/**
* Valider ou rejeter un ticket
* POST /api/employee/validate-ticket
*/
export const validateTicket = asyncHandler(async (req, res, next) => {
const { ticketId, action, rejectionReason } = req.body;
const employeeId = req.user.id;
// Récupérer le ticket
const ticketResult = await pool.query(
'SELECT id, status, user_id FROM tickets WHERE id = $1',
[ticketId]
);
if (ticketResult.rows.length === 0) {
return next(new AppError('Ticket non trouvé', 404));
}
const ticket = ticketResult.rows[0];
if (ticket.status !== 'PENDING') {
return next(new AppError('Ce ticket a déjà été traité', 400));
}
const client = await pool.connect();
try {
await client.query('BEGIN');
if (action === 'APPROVE') {
// Approuver le ticket
await client.query(
`UPDATE tickets
SET status = 'CLAIMED',
validated_by = $1,
validated_at = CURRENT_TIMESTAMP,
claimed_at = CURRENT_TIMESTAMP
WHERE id = $2`,
[employeeId, ticketId]
);
await client.query('COMMIT');
res.json({
success: true,
message: 'Ticket validé avec succès',
});
} else if (action === 'REJECT') {
// Rejeter le ticket
await client.query(
`UPDATE tickets
SET status = 'REJECTED',
validated_by = $1,
validated_at = CURRENT_TIMESTAMP,
rejection_reason = $2
WHERE id = $3`,
[employeeId, rejectionReason, ticketId]
);
// Incrémenter le stock du prix (le prix est remis en circulation)
await client.query(
'UPDATE prizes SET stock = stock + 1 WHERE id = (SELECT prize_id FROM tickets WHERE id = $1)',
[ticketId]
);
await client.query('COMMIT');
res.json({
success: true,
message: 'Ticket rejeté',
});
}
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
});
/**
* Statistiques pour l'employé
* GET /api/employee/stats
*/
export const getEmployeeStats = asyncHandler(async (req, res) => {
const employeeId = req.user.id;
const result = await pool.query(
`SELECT
COUNT(*) as total_validated,
COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as total_approved,
COUNT(CASE WHEN status = 'REJECTED' THEN 1 END) as total_rejected
FROM tickets
WHERE validated_by = $1`,
[employeeId]
);
// Statistiques pour aujourd'hui
const todayResult = await pool.query(
`SELECT
COUNT(CASE WHEN status = 'CLAIMED' THEN 1 END) as claimed_today
FROM tickets
WHERE validated_by = $1
AND DATE(validated_at) = CURRENT_DATE`,
[employeeId]
);
const pendingResult = await pool.query(
'SELECT COUNT(*) as pending_count FROM tickets WHERE status = $1',
['PENDING']
);
res.json({
success: true,
data: {
...result.rows[0],
claimed_today: parseInt(todayResult.rows[0].claimed_today) || 0,
pending_tickets: parseInt(pendingResult.rows[0].pending_count),
},
});
});
/**
* Rechercher tous les gains d'un client
* GET /api/employee/client-prizes
*/
export const getClientPrizes = asyncHandler(async (req, res) => {
const { email, phone } = req.query;
if (!email && !phone) {
throw new AppError('Email ou téléphone requis', 400);
}
// Chercher l'utilisateur
let userQuery = 'SELECT id, email, first_name, last_name, phone FROM users WHERE role = $1';
const params = ['CLIENT'];
if (email) {
userQuery += ' AND email ILIKE $2';
params.push(`%${email}%`);
} else if (phone) {
userQuery += ' AND phone ILIKE $2';
params.push(`%${phone}%`);
}
const userResult = await pool.query(userQuery, params);
if (userResult.rows.length === 0) {
throw new AppError('Client non trouvé', 404);
}
const user = userResult.rows[0];
// Récupérer tous les tickets gagnants du client
const ticketsResult = await pool.query(
`SELECT
t.id,
t.code,
t.status,
t.played_at,
t.claimed_at,
t.validated_at,
p.id as prize_id,
p.name as prize_name,
p.type as prize_type,
p.value as prize_value,
p.description as prize_description,
v.first_name || ' ' || v.last_name as validated_by_name
FROM tickets t
JOIN prizes p ON t.prize_id = p.id
LEFT JOIN users v ON t.validated_by = v.id
WHERE t.user_id = $1 AND t.played_at IS NOT NULL
ORDER BY t.played_at DESC`,
[user.id]
);
res.json({
success: true,
data: {
client: {
id: user.id,
email: user.email,
firstName: user.first_name,
lastName: user.last_name,
phone: user.phone,
},
prizes: ticketsResult.rows.map((t) => ({
ticketId: t.id,
ticketCode: t.code,
status: t.status,
playedAt: t.played_at,
claimedAt: t.claimed_at,
validatedAt: t.validated_at,
validatedBy: t.validated_by_name,
prize: {
id: t.prize_id,
name: t.prize_name,
type: t.prize_type,
value: t.prize_value,
description: t.prize_description,
},
})),
totalPrizes: ticketsResult.rows.length,
pendingPrizes: ticketsResult.rows.filter((t) => t.status === 'PENDING').length,
claimedPrizes: ticketsResult.rows.filter((t) => t.status === 'CLAIMED').length,
},
});
});
/**
* Historique des validations de l'employé
* GET /api/employee/history
*/
export const getEmployeeHistory = asyncHandler(async (req, res) => {
const employeeId = req.user.id;
const result = await pool.query(
`SELECT
t.id,
t.code,
t.status,
t.played_at,
t.claimed_at,
t.validated_at,
t.rejection_reason,
u.email as user_email,
u.first_name || ' ' || u.last_name as user_name,
p.name as prize_name,
p.value as prize_value
FROM tickets t
JOIN users u ON t.user_id = u.id
JOIN prizes p ON t.prize_id = p.id
WHERE t.validated_by = $1
ORDER BY t.validated_at DESC`,
[employeeId]
);
res.json({
success: true,
data: result.rows,
});
});
export default {
getPendingTickets,
searchTicket,
validateTicket,
getEmployeeStats,
getClientPrizes,
getEmployeeHistory,
};