firt commit
This commit is contained in:
commit
c2e63830e1
71 changed files with 9613 additions and 0 deletions
293
server/controllers/adminController.js
Executable file
293
server/controllers/adminController.js
Executable file
|
@ -0,0 +1,293 @@
|
|||
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)
|
63
server/controllers/authController.js
Normal file
63
server/controllers/authController.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { query } from '../utils/database.js';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export const checkPassword = async (req, res) => {
|
||||
try {
|
||||
const [settings] = await query('SELECT id FROM admin_settings WHERE setting_key = ?', ['admin_password']);
|
||||
res.json({ isFirstTime: !settings });
|
||||
} catch (error) {
|
||||
console.error('Error checking password:', error);
|
||||
res.status(500).json({ message: 'Failed to check password status' });
|
||||
}
|
||||
};
|
||||
|
||||
export const setPassword = async (req, res) => {
|
||||
try {
|
||||
const { password } = req.body;
|
||||
if (!password) {
|
||||
return res.status(400).json({ message: 'Password is required' });
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Check if a password already exists
|
||||
const [existingSettings] = await query('SELECT id FROM admin_settings WHERE setting_key = ?', ['admin_password']);
|
||||
|
||||
if (existingSettings) {
|
||||
// Update existing password
|
||||
await query('UPDATE admin_settings SET setting_value = ? WHERE setting_key = ?', [hashedPassword, 'admin_password']);
|
||||
} else {
|
||||
// Insert new password
|
||||
await query('INSERT INTO admin_settings (setting_key, setting_value) VALUES (?, ?)', ['admin_password', hashedPassword]);
|
||||
}
|
||||
|
||||
res.json({ message: 'Password set successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error setting password:', error);
|
||||
res.status(500).json({ message: 'Failed to set password' });
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyPassword = async (req, res) => {
|
||||
try {
|
||||
const { password } = req.body;
|
||||
if (!password) {
|
||||
return res.status(400).json({ message: 'Password is required' });
|
||||
}
|
||||
|
||||
const [settings] = await query('SELECT setting_value FROM admin_settings WHERE setting_key = ?', ['admin_password']);
|
||||
if (!settings) {
|
||||
return res.status(404).json({ message: 'No password set' });
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, settings.setting_value);
|
||||
if (!isValid) {
|
||||
return res.status(401).json({ message: 'Invalid password' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Password verified successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error verifying password:', error);
|
||||
res.status(500).json({ message: 'Failed to verify password' });
|
||||
}
|
||||
};
|
30
server/controllers/matchController.js
Executable file
30
server/controllers/matchController.js
Executable file
|
@ -0,0 +1,30 @@
|
|||
import { query } from '../utils/database.js';
|
||||
|
||||
export const getMatches = async (req, res) => {
|
||||
try {
|
||||
// Fetch all matches, ordered by date
|
||||
const matches = await query('SELECT id, name, date, location, totalSeats, availableSeats, price, timeoutDate FROM matches ORDER BY date');
|
||||
res.status(200).json(matches);
|
||||
} catch (error) {
|
||||
console.error('Error fetching matches:', error);
|
||||
res.status(500).json({ message: 'Failed to fetch matches', error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const getMatchDetails = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Fetch details for a specific match
|
||||
const match = await query('SELECT id, name, date, location, totalSeats, availableSeats, price, timeoutDate FROM matches WHERE id = ?', [id]);
|
||||
|
||||
if (match.length === 0) {
|
||||
return res.status(404).json({ message: 'Match not found' });
|
||||
}
|
||||
|
||||
res.status(200).json(match[0]);
|
||||
} catch (error) {
|
||||
console.error('Error fetching match details:', error);
|
||||
res.status(500).json({ message: 'Failed to fetch match details', error: error.message });
|
||||
}
|
||||
};
|
156
server/controllers/ticketController.js
Executable file
156
server/controllers/ticketController.js
Executable file
|
@ -0,0 +1,156 @@
|
|||
import { query } from '../utils/database.js';
|
||||
import { sendEmail } from '../utils/email.js';
|
||||
import { generateTicketPDF } from '../utils/pdf.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export const createTicket = async (req, res) => {
|
||||
try {
|
||||
const { matchId, name, email, phone, selectedSeats, deliveryMethod } = req.body;
|
||||
|
||||
// Start transaction
|
||||
await query('START TRANSACTION');
|
||||
|
||||
try {
|
||||
// Check if seats are available
|
||||
const seats = await query(
|
||||
'SELECT * FROM seats WHERE matchId = ? AND seatNumber IN (?) AND status = "available"',
|
||||
[matchId, selectedSeats]
|
||||
);
|
||||
|
||||
if (seats.length !== selectedSeats.length) {
|
||||
throw new Error('Some selected seats are not available');
|
||||
}
|
||||
|
||||
// Create ticket
|
||||
const ticketResult = await query(
|
||||
`INSERT INTO tickets (
|
||||
matchId, name, email, phone, seats, deliveryMethod
|
||||
) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[matchId, name, email, phone, selectedSeats.length, deliveryMethod]
|
||||
);
|
||||
|
||||
const ticketId = ticketResult.insertId;
|
||||
|
||||
// Update seats status
|
||||
await query(
|
||||
'UPDATE seats SET status = "booked", ticketId = ? WHERE matchId = ? AND seatNumber IN (?)',
|
||||
[ticketId, matchId, selectedSeats]
|
||||
);
|
||||
|
||||
// Update match available seats
|
||||
await query(
|
||||
'UPDATE matches SET availableSeats = availableSeats - ? WHERE id = ?',
|
||||
[selectedSeats.length, matchId]
|
||||
);
|
||||
|
||||
// Generate PDF
|
||||
const pdfFileName = `${Date.now()}-${name}-${selectedSeats.length}.pdf`;
|
||||
const pdfPath = path.join(__dirname, '..', '..', 'public', 'uploads', pdfFileName);
|
||||
|
||||
const match = await query('SELECT * FROM matches WHERE id = ?', [matchId]);
|
||||
await generateTicketPDF({
|
||||
ticketId,
|
||||
match: match[0],
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
seats: seats,
|
||||
pdfFile: pdfFileName
|
||||
});
|
||||
|
||||
// Update ticket with PDF path
|
||||
await query(
|
||||
'UPDATE tickets SET pdfFile = ? WHERE id = ?',
|
||||
[`/uploads/${pdfFileName}`, ticketId]
|
||||
);
|
||||
|
||||
// Send email if requested
|
||||
if (deliveryMethod === 'email') {
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: `Your tickets for ${match[0].name}`,
|
||||
text: `Thank you for booking tickets for ${match[0].name}. Your seat numbers are: ${selectedSeats.join(', ')}`,
|
||||
attachments: [{
|
||||
filename: pdfFileName,
|
||||
path: pdfPath
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
await query('COMMIT');
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Ticket created successfully',
|
||||
ticketId,
|
||||
pdfUrl: `/uploads/${pdfFileName}`
|
||||
});
|
||||
} catch (error) {
|
||||
// Rollback transaction on error
|
||||
await query('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating ticket:', error);
|
||||
res.status(500).json({
|
||||
message: 'Failed to create ticket',
|
||||
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(seats);
|
||||
} catch (error) {
|
||||
console.error('Error fetching seats:', error);
|
||||
res.status(500).json({
|
||||
message: 'Failed to fetch seats',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getTicketStatus = async (req, res) => {
|
||||
try {
|
||||
const ticketId = req.params.ticketId || req;
|
||||
// If called as a controller (req, res), req.params.ticketId is used
|
||||
// If called as a service (just ticketId), req is the id
|
||||
const id = typeof ticketId === 'object' ? ticketId.ticketId : ticketId;
|
||||
const result = await query('SELECT * FROM tickets WHERE id = ?', [id]);
|
||||
if (!result || result.length === 0) {
|
||||
if (res) {
|
||||
return res.status(404).json({ message: 'Ticket not found' });
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (res) {
|
||||
res.json(result[0]);
|
||||
} else {
|
||||
return result[0];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting ticket status:', error);
|
||||
if (res) {
|
||||
res.status(500).json({ message: 'Failed to get ticket status', error: error.message });
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue