Handballbooking/server/controllers/adminController.js
2025-06-04 15:13:40 +02:00

293 lines
11 KiB
JavaScript
Executable file

import { query } from '../utils/database.js';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Helper to calculate timeout date
const calculateTimeoutDate = (matchDate, timeoutMinutes) => {
const date = new Date(matchDate);
date.setMinutes(date.getMinutes() + timeoutMinutes);
return date;
};
// Helper to convert BigInt to Number
const convertBigIntToNumber = (obj) => {
if (obj === null || obj === undefined) return obj;
if (typeof obj === 'bigint') return Number(obj);
if (Array.isArray(obj)) return obj.map(convertBigIntToNumber);
if (typeof obj === 'object') {
const result = {};
for (const key in obj) {
result[key] = convertBigIntToNumber(obj[key]);
}
return result;
}
return obj;
};
// Helper to extract seat info from filename
const extractSeatInfoFromFilename = (filename) => {
// Look for 3 to 5 uppercase letters, followed by an underscore, then numbers
const regex = /^([A-Z]{3,5})_(\d+)\.pdf$/; // Updated regex to match DIRECTION_NUMBER.pdf format
const match = filename.match(regex);
if (match && match[1] && match[2]) {
return {
direction: match[1],
extractedSeatNumber: parseInt(match[2], 10)
};
} else {
return { direction: null, extractedSeatNumber: null };
}
};
export const addMatch = async (req, res) => {
try {
const { name, date, location, totalSeats, price, timeoutDate } = req.body;
const uploadedFiles = req.files; // Access uploaded files via multer
// Parse the seatInfo array sent as a JSON string(s) from the frontend
const seatInfoArray = req.body.seatInfo ? (Array.isArray(req.body.seatInfo) ? req.body.seatInfo.map(info => JSON.parse(info)) : [JSON.parse(req.body.seatInfo)]) : [];
console.log('Uploaded files:', uploadedFiles); // Log uploaded files
console.log('Received seatInfoArray:', seatInfoArray); // Log received seat info array
// Basic validation
if (!name || !date || !location || !totalSeats || !timeoutDate) {
return res.status(400).json({ message: 'Missing required fields' });
}
// Ensure totalSeats is a number
const parsedTotalSeats = parseInt(totalSeats, 10);
const parsedPrice = parseFloat(price || 0);
if (isNaN(parsedTotalSeats) || parsedTotalSeats < 0 || isNaN(parsedPrice) || parsedPrice < 0) {
return res.status(400).json({ message: 'Invalid number format for seats or price' });
}
// Optional: Validate number of uploaded files vs totalSeats
if (uploadedFiles && uploadedFiles.length > 0 && uploadedFiles.length !== parsedTotalSeats) {
console.warn(`Number of uploaded files (${uploadedFiles.length}) does not match total seats (${parsedTotalSeats}). Seat info may not be fully populated.`);
}
// Start a transaction
await query('START TRANSACTION');
try {
// Insert match
const matchResult = await query(
`INSERT INTO matches (
name, date, location, totalSeats, availableSeats,
price, timeoutDate
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
name,
date,
location,
parsedTotalSeats,
parsedTotalSeats,
parsedPrice,
timeoutDate
]
);
const matchId = matchResult.insertId;
// Create seats for the match and store extracted/manual info
const seatValues = Array.from({ length: parsedTotalSeats }, (_, i) => {
// Ensure we process files up to the number of uploaded files or totalSeats, whichever is smaller
if (i >= uploadedFiles.length && i >= seatInfoArray.length) {
return null; // No file or seat info for this seat index
}
const seatNumberDefault = i + 1; // Sequential seat number as default fallback
const file = uploadedFiles && uploadedFiles[i]; // Corresponding uploaded file
const frontendSeatInfo = seatInfoArray[i]; // Get seat info by index (assuming order matches files)
console.log(`Processing seat ${i + 1}: File`, file ? file.originalname : 'No file', 'Frontend Info:', frontendSeatInfo); // Log processing details
let seatNumberToSave = seatNumberDefault; // Use a new variable name for the final seat number
let uploadedPdfPath = null;
let sourceOfDirection = null; // Keep track of where direction came from
// Determine seatDirection and seatNumberToSave based on available data, prioritizing frontend
const determinedSeatInfo = (() => {
if (frontendSeatInfo) {
return {
direction: frontendSeatInfo.direction,
seatNumber: frontendSeatInfo.manualSeatNumber !== null ? frontendSeatInfo.manualSeatNumber : (frontendSeatInfo.extractedSeatNumber !== null ? frontendSeatInfo.extractedSeatNumber : seatNumberDefault),
source: 'frontend'
};
} else if (file) {
// Fallback to backend extraction if no frontend info
const extractedInfo = extractSeatInfoFromFilename(file.originalname);
return {
direction: extractedInfo.direction,
seatNumber: extractedInfo.extractedSeatNumber !== null ? extractedInfo.extractedSeatNumber : seatNumberDefault,
source: 'backend_extraction'
};
} else {
// Default if no frontend info and no file
return {
direction: null,
seatNumber: seatNumberDefault,
source: 'default'
};
}
})();
const seatDirection = determinedSeatInfo.direction; // Assign determined direction
seatNumberToSave = determinedSeatInfo.seatNumber; // Assign determined seat number
sourceOfDirection = determinedSeatInfo.source; // Assign source
if (file) {
// Construct the path assuming multer saves to /public/uploads
uploadedPdfPath = `/uploads/${file.filename}`; // Use file.filename provided by multer
}
// Log values after PDF path assignment, before returning for insertion
console.log(`Seat ${i + 1} - Values after PDF path assignment: Direction = ${seatDirection}, Seat Number = ${seatNumberToSave}, PDF Path = ${uploadedPdfPath}, Source = ${sourceOfDirection}`);
// Log values right before returning for insertion
console.log(`Seat ${i + 1} - Values before return for insertion: Direction = ${seatDirection}, Seat Number = ${seatNumberToSave}, PDF Path = ${uploadedPdfPath}, Source = ${sourceOfDirection}`);
// Return the values in the order expected by the INSERT query
return [matchId, seatNumberToSave, seatDirection, uploadedPdfPath];
}).filter(value => value !== null); // Filter out null values if totalSeats was higher than uploaded files/seatInfo
const seatPlaceholders = seatValues.map(() => '(?, ?, ?, ?)').join(', ');
if (seatValues.length > 0) {
await query(
`INSERT INTO seats (matchId, seatNumber, direction, uploadedPdfPath) VALUES ${seatPlaceholders}`,
seatValues.flat()
);
} else if (parsedTotalSeats > 0 && (uploadedFiles.length === 0 || seatInfoArray.length === 0)) {
// If totalSeats > 0 but no files uploaded or seatInfo processed, create seats with default info
const defaultSeatValues = Array.from({ length: parsedTotalSeats }, (_, i) => [matchId, i + 1, null, null]);
const defaultPlaceholders = defaultSeatValues.map(() => '(?, ?, ?, ?)').join(', ');
await query(
`INSERT INTO seats (matchId, seatNumber, direction, uploadedPdfPath) VALUES ${defaultPlaceholders}`,
defaultSeatValues.flat()
);
}
// Commit transaction
await query('COMMIT');
// Convert BigInt to Number before sending response
const response = convertBigIntToNumber({
message: 'Match added successfully',
matchId: matchId
});
res.status(201).json(response);
} catch (error) {
// Rollback transaction on error
await query('ROLLBACK');
throw error;
}
} catch (error) {
console.error('Error adding match:', error);
res.status(500).json({
message: 'Failed to add match',
error: error.message
});
}
};
export const getMatchSeats = async (req, res) => {
try {
const { matchId } = req.params;
const seats = await query(`
SELECT s.*, t.name as bookedBy, t.email as bookedByEmail
FROM seats s
LEFT JOIN tickets t ON s.ticketId = t.id
WHERE s.matchId = ?
ORDER BY s.seatNumber
`, [matchId]);
res.status(200).json(convertBigIntToNumber(seats));
} catch (error) {
console.error('Error fetching seats:', error);
res.status(500).json({
message: 'Failed to fetch seats',
error: error.message
});
}
};
export const deleteMatch = async (req, res) => {
try {
const { id } = req.params;
if (!id) {
return res.status(400).json({ message: 'Match ID is required' });
}
// Start a transaction
await query('START TRANSACTION');
try {
// Get file paths of associated PDFs before deleting records
const seatPdfs = await query('SELECT uploadedPdfPath FROM seats WHERE matchId = ? AND uploadedPdfPath IS NOT NULL', [id]);
const ticketPdfs = await query('SELECT pdfFile FROM tickets WHERE matchId = ? AND pdfFile IS NOT NULL', [id]);
// Delete associated seats
await query('DELETE FROM seats WHERE matchId = ?', [id]);
// Delete associated tickets
await query('DELETE FROM tickets WHERE matchId = ?', [id]);
// Delete the match
const result = await query('DELETE FROM matches WHERE id = ?', [id]);
// Commit transaction
await query('COMMIT');
// Delete physical PDF files after successful database deletion
const filesToDelete = [
...seatPdfs.map(row => row.uploadedPdfPath),
...ticketPdfs.map(row => row.pdfFile)
];
const publicDir = path.join(__dirname, '..', '..' , 'public');
for (const filePath of filesToDelete) {
if (filePath) {
const absolutePath = path.join(publicDir, filePath);
fs.unlink(absolutePath, (err) => {
if (err) {
console.error(`Error deleting file ${absolutePath}:`, err);
} else {
console.log(`Deleted file: ${absolutePath}`);
}
});
}
}
if (result.affectedRows === 0) {
return res.status(404).json({ message: 'Match not found' });
}
res.status(200).json({ message: 'Match deleted successfully' });
} catch (error) {
// Rollback transaction on error
await query('ROLLBACK');
console.error('Error deleting match:', error);
res.status(500).json({
message: 'Failed to delete match',
error: error.message
});
}
} catch (error) {
console.error('Error processing delete request:', error);
res.status(500).json({ message: 'Failed to delete match', error: error.message });
}
};
// Add other admin controller functions here later (e.g., for status page)