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>
377 lines
9.2 KiB
JavaScript
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,
|
|
};
|