Files
hotel-hacienda/backend/hotel_hacienda/src/controllers/mail.controller.js
Consultoria AS 0211bea186 Commit inicial - Sistema de Gestion Hotelera Hacienda San Angel
- Backend Node.js/Express con PostgreSQL
- Frontend React 19 con Vite
- Docker Compose para orquestacion
- Documentacion completa en README.md
- Scripts SQL para base de datos
- Configuracion de ejemplo (.env.example)
2026-01-17 18:52:34 -08:00

651 lines
24 KiB
JavaScript

const pool = require('../db/connection');
const transporter = require('../services/mailService');
const contractsNearToEnd = async (req, res) => {
try{
const countResult = await pool.query('SELECT contratcsneartoend() as total');
const contractsResult = await pool.query('SELECT * FROM contractsneartoenddata()');
const userEmails = await pool.query('SELECT * FROM getusersmailneartoend()');
const totalExpiring = countResult.rows[0].total;
const contracts = contractsResult.rows;
const users = userEmails.rows;
if(totalExpiring >= 1 && users.length > 0){
const recipients = users.map(u => u.email);
const mailOptions = {
from: 'soporte@horuxfin.com',
to: recipients,
subject: 'Contratos próximos a expirar',
html: `
<div style="max-width:600px;margin:0 auto;font-family:Arial,Helvetica,sans-serif;
background-color:#ffffff;border:1px solid #e5e5e5;border-radius:8px;
overflow:hidden;">
<!-- Header -->
<div style="background-color:#7a0c1e;padding:18px 22px;">
<h2 style="margin:0;color:#ffffff;font-size:20px;font-weight:600;">
Notificación de contratos
</h2>
</div>
<!-- Body -->
<div style="padding:22px;color:#333333;font-size:14px;line-height:1.6;">
<p style="margin-top:0;">
Actualmente hay <b>${totalExpiring}</b> contrato(s) por expirar.
</p>
<p style="margin-top:0;">
Los siguientes contratos son los que expiran:
</p>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #ddd;">
<thead>
<tr style="background-color:#f4f4f4;">
<th align="left" style="padding:8px;border:1px solid #ddd;">ID contract</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">RFC</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Nombre</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Fecha inicio contrato</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Fecha fin contrato</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Salario diario</th>
</tr>
</thead>
<tbody>
${contracts.map(c => {
const inicio = formatDateDMY(c.start_contract);
const fin = formatDateDMY(c.end_contract);
return `
<tr>
<td style="padding:8px;border:1px solid #ddd;">${c.id_contract}</td>
<td style="padding:8px;border:1px solid #ddd;">${c.rfc_emp}</td>
<td style="padding:8px;border:1px solid #ddd;">${c.name_employee}</td>
<td style="padding:8px;border:1px solid #ddd;">${inicio}</td>
<td style="padding:8px;border:1px solid #ddd;">${fin}</td>
<td style="padding:8px;border:1px solid #ddd;">${c.dailypay}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
<!-- CTA -->
<div style="text-align:center;margin-top:32px;">
<a href="https://hacienda.consultoria-as.com/"
style="background-color:#7a0c1e;color:#ffffff;
text-decoration:none;font-weight:bold;
padding:14px 28px;border-radius:6px;
display:inline-block;">
Revisar en la web
</a>
</div>
<p style="margin-top:16px;font-size:12px;color:#777777;text-align:center;">
Por favor considere si seran recontratados o finalizados.
</p>
</div>
<!-- Footer -->
<div style="background-color:#f6f6f6;padding:12px 20px;
font-size:12px;color:#888888;text-align:center;">
Sistema de gestión financiera · Consultoría AS
</div>
</div>
`
};
await transporter.sendMail(mailOptions);
res.json({
message: 'Correo enviado con contratos por expirar.',
data:totalExpiring
});
}else{
res.json({
message: 'No hay contratos por expirar',
data:totalExpiring
});
}
}catch(error){
console.error(error);
res.status(500).json({ status: 500, message: 'No se pudo mandar el correo con los contratos por expirar' });
}
};
const paymentDeadLine = async (req, res) => {
try{
const deadLineCount = await pool.query('SELECT countdelaypayments() as total');
const deadLineResult = await pool.query('SELECT * FROM getdelaypayments()');
const usersDeadLine = await pool.query('SELECT * FROM getusersmailpaymentdelay()');
const totalDeadline = deadLineCount.rows[0].total;
const deadLines = deadLineResult.rows;
const users = usersDeadLine.rows;
if(totalDeadline >= 1 && users.length >0){
const recipients = users.map(u => u.email);
const mailOptions = {
from: 'soporte@horuxfin.com',
to: recipients,
subject: 'WARNING: OVERDUE PAYMENTS',
html: `
<div style="max-width:600px;margin:0 auto;font-family:Arial,Helvetica,sans-serif;
background-color:#ffffff;border:1px solid #e5e5e5;border-radius:8px;
overflow:hidden;">
<!-- Header -->
<div style="background-color:#7a0c1e;padding:18px 22px;">
<h2 style="margin:0;color:#ffffff;font-size:20px;font-weight:600;">
Notification regarding overdue payments
</h2>
</div>
<!-- Body -->
<div style="padding:22px;color:#333333;font-size:14px;line-height:1.6;">
<p style="margin-top:0;">
Currently there are <b>${totalDeadline}</b> overdue payment(s).
</p>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #ddd;">
<thead>
<tr style="background-color:#f4f4f4;">
<th align="left" style="padding:8px;border:1px solid #ddd;">ID</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Description</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Deadline</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Total</th>
</tr>
</thead>
<tbody>
${deadLines.map(c => {
const fecha = new Date(c.payment_deadline);
const fechaFormateada = fecha.toLocaleDateString('en-US', {
day: '2-digit',
month: 'long'
});
return `
<tr>
<td style="padding:8px;border:1px solid #ddd;">${c.id_expense}</td>
<td style="padding:8px;border:1px solid #ddd;">${c.description}</td>
<td style="padding:8px;border:1px solid #ddd;">${fechaFormateada}</td>
<td style="padding:8px;border:1px solid #ddd;">${c.total || 'N/A'}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
<!-- CTA -->
<div style="text-align:center;margin-top:32px;">
<a href="https://hacienda.consultoria-as.com/"
style="background-color:#7a0c1e;color:#ffffff;
text-decoration:none;font-weight:bold;
padding:14px 28px;border-radius:6px;
display:inline-block;">
Please review and pay as soon as possible.
</a>
</div>
<p style="margin-top:16px;font-size:12px;color:#777777;text-align:center;">
We recommend that review your overdue expenses as soon as possible.
</p>
</div>
<!-- Footer -->
<div style="background-color:#f6f6f6;padding:12px 20px;
font-size:12px;color:#888888;text-align:center;">
Financial management system · Consultoría AS
</div>
</div>`
};
await transporter.sendMail(mailOptions);
res.json({
message: 'Correo enviado con pagos atrasados.',
data:totalDeadline
});
}else{
res.json({
message: 'No hay pagos con atraso',
data:totalDeadline
});
}
}catch(error){
console.error(error);
res.status(500).json({ status: 500, message: 'No se pudo mandar el correo con con los pagos atrasados' });
}
};
const expensesNearToDeadline = async (req, res) => {
try{
const deadLineCount = await pool.query('SELECT COUNT(*) FROM expensesneartodeadline()');
const deadLineResult = await pool.query('SELECT * FROM expensesneartodeadline()');
const usersDeadLine = await pool.query('SELECT * FROM getusersmailpaymentdelay()');
const totalDeadline = deadLineCount.rows[0].count;
const deadLines = deadLineResult.rows;
const users = usersDeadLine.rows;
console.log(totalDeadline);
if(totalDeadline >= 1 && users.length >0){
const recipients = users.map(u => u.email);
const mailOptions = {
from: 'soporte@horuxfin.com',
to: recipients,
subject: 'WARNING: PAYMENTS DUE SOON FOR LATE PAYMENTS',
html: `
<div style="max-width:600px;margin:0 auto;font-family:Arial,Helvetica,sans-serif;
background-color:#ffffff;border:1px solid #e5e5e5;border-radius:8px;
overflow:hidden;">
<!-- Header -->
<div style="background-color:#7a0c1e;padding:18px 22px;">
<h2 style="margin:0;color:#ffffff;font-size:20px;font-weight:600;">
Notification of payments approaching a deadline
</h2>
</div>
<!-- Body -->
<div style="padding:22px;color:#333333;font-size:14px;line-height:1.6;">
<p style="margin-top:0;">
Currently there are <b>${totalDeadline}</b> payment(s) close to deadline.
</p>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #ddd;">
<thead>
<tr style="background-color:#f4f4f4;">
<th align="left" style="padding:8px;border:1px solid #ddd;">ID</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Description</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Deadline</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Total</th>
</tr>
</thead>
<tbody>
${deadLines.map(c => {
const fecha = new Date(c.payment_deadline);
const fechaFormateada = fecha.toLocaleDateString('en-US', {
day: '2-digit',
month: 'long'
});
return `
<tr>
<td style="padding:8px;border:1px solid #ddd;">${c.id_expense}</td>
<td style="padding:8px;border:1px solid #ddd;">${c.description}</td>
<td style="padding:8px;border:1px solid #ddd;">${fechaFormateada}</td>
<td style="padding:8px;border:1px solid #ddd;">${c.total || 'N/A'}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
<!-- CTA -->
<div style="text-align:center;margin-top:32px;">
<a href="https://hacienda.consultoria-as.com/"
style="background-color:#7a0c1e;color:#ffffff;
text-decoration:none;font-weight:bold;
padding:14px 28px;border-radius:6px;
display:inline-block;">
Review and pay spendings
</a>
</div>
<p style="margin-top:16px;font-size:12px;color:#777777;text-align:center;">
We recommend reviewing this application before the deadline.
</p>
</div>
<!-- Footer -->
<div style="background-color:#f6f6f6;padding:12px 20px;
font-size:12px;color:#888888;text-align:center;">
Financial management system · Consultoría AS
</div>
</div>
`
};
await transporter.sendMail(mailOptions);
res.json({
message: 'Correo enviado con pagos por expirar.',
data:totalDeadline
});
}else{
res.json({
message: 'No hay pagos con atraso',
data:totalDeadline
});
}
}catch(error){
console.error(error);
res.status(500).json({ status: 500, message: 'No se pudo mandar el correo con los pagos prontos a fecha limite' });
}
};
const expensesspecial = async (req, res) => {
try{
const expensesCount = await pool.query('SELECT COUNT(*) AS total FROM getexpensesspecial()');
const expensesResult = await pool.query('SELECT * FROM getexpensesspecial()');
const totalExpenses = expensesCount.rows[0].total;
const expenses = expensesResult.rows;
if(totalExpenses >= 1){
const mailOptions = {
from: 'soporte@horuxfin.com',
to: 'soporte@horuxfin.com',
subject: 'Recordatorio: Hay gastos por aprobar',
html: `
<h2>Notificación de gastos por aprobar</h2>
<p>Actualmente hay <b>${totalExpenses}</b> gasto(s) por aprobar</p>
<ul>Los siguientes gastos faltan por aprobar</ul>
<ul>
${expenses.map(c => {
const fecha = new Date(c.payment_deadline);
const fechaFormateada = fecha.toLocaleDateString('es-MX');
const product = Array.isArray(c.products) && c.products.length > 0
? c.products.map(p => `${p.name_product} ($${p.total})`).join(', ')
: 'N/A';
return `<li>
ID:${c.id_expense} - Descripción: ${c.description}
- Productos: ${product} -
fecha limite de pago: ${fechaFormateada} -
total del gasto: ${c.total || 'N/A'}
</li>`;
}).join('')}
</ul>`
};
await transporter.sendMail(mailOptions);
res.json({
message: 'Correo enviado con gastos por aprobar',
data:totalExpenses
});
}else{
res.json({
message: 'No hay pagos por aprobar',
data:totalExpenses
});
}
}catch(error){
console.error(error);
res.status(500).json({ status: 500, message: 'No se pudo mandar el correo con los contratos por expirar' });
}
};
const emailbirthday = async (req, res) => {
try{
const endpointName = 'emailbirthday';
const current = new Date();
const mes = current.getMonth() + 1;
const anio = current.getFullYear();
const validateEndpoint = await pool.query('SELECT validateendpoint($1,$2::int,$3::int) AS total',[endpointName,mes,anio]);
if (validateEndpoint.rows[0].total > 0) {
return res.status(400).json({
message: `Este endpoint ya fue ejecutado en ${current.toLocaleString('es-MX', { month: 'long' })}.`
});
}else {
await pool.query(`INSERT INTO endpoint_logs (endpoint_name) VALUES ($1)`,[endpointName]);
const birthdayCount = await pool.query('SELECT COUNT(*) AS total FROM getbirthdaysemployees()');
const birthdayResult = await pool.query('SELECT * FROM getbirthdaysemployees()');
const userEmails = await pool.query('SELECT * FROM getusersmailbirthday()');
const totalBirthday = birthdayCount.rows[0].total;
const birthdays = birthdayResult.rows;
const users = userEmails.rows;
const currentMonth = new Date().toLocaleString('en-US', { month: 'long' });
if(totalBirthday >= 1 && users.length >0){
const recipients = users.map(u => u.email);
const mailOptions = {
from: 'soporte@horuxfin.com',
to: recipients,
subject: `Friendly reminder: Birthdays of the month of ${currentMonth}`,
html: `
<div style="max-width:600px;margin:0 auto;font-family:Arial,Helvetica,sans-serif;
background-color:#ffffff;border:1px solid #e5e5e5;border-radius:8px;
overflow:hidden;">
<!-- Header -->
<div style="background-color:#7a0c1e;padding:18px 22px;">
<h2 style="margin:0;color:#ffffff;font-size:20px;font-weight:600;">
In this month there are ${totalBirthday} birthday person(s)
</h2>
</div>
<!-- Body -->
<div style="padding:22px;color:#333333;font-size:14px;line-height:1.6;">
<p style="margin-top:0;">
The following employees are celebrating their birthdays this month:
</p>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #ddd;">
<thead>
<tr style="background-color:#f4f4f4;">
<th align="left" style="padding:8px;border:1px solid #ddd;">RFC</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Name</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Birthday</th>
</tr>
</thead>
<tbody>
${birthdays.map(c => {
const fecha = new Date(c.birthday);
const fechaFormateada = fecha.toLocaleDateString('en-US', {
day: '2-digit',
month: 'long'
});
return `
<tr>
<td style="padding:8px;border:1px solid #ddd;">${c.rfc}</td>
<td style="padding:8px;border:1px solid #ddd;">${c.fullname}</td>
<td style="padding:8px;border:1px solid #ddd;">${fechaFormateada}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
<p style="margin-top:16px;font-size:12px;color:#777777;text-align:center;">
Remember to congratulate them on their special day!
</p>
</div>
<!-- Footer -->
<div style="background-color:#f6f6f6;padding:12px 20px;
font-size:12px;color:#888888;text-align:center;">
Financial management system · Consultoría AS
</div>
</div>`};
await transporter.sendMail(mailOptions);
res.json({
message: 'Correo enviado con los cumpleaños',
data:totalBirthday
});
}else{
res.json({
message: 'No hay cumpleañeros este mes',
data:totalBirthday
});
}}
}catch(error){
console.error(error);
res.status(500).json({ status: 500, message: 'No se pudo enviar correo con los cumpleañeros' });
}
};
const expiredcontracts = async (req, res) => {
try{
const expiredCountTotal = await pool.query('SELECT expiredcontracts() as total');
const expiredResult = await pool.query('SELECT * FROM getexpirecontracts()');
const expiredCountDay = await pool.query('SELECT COUNT(*) AS total FROM getexpirecontracts()');
const totalExpired = expiredCountTotal.rows[0].total;
const totalDayExpired = expiredCountDay.rows[0].total;
const expiredContracts = expiredResult.rows;
if(totalExpired >= 1){
const mailOptions = {
from: 'soporte@horuxfin.com',
to: 'soporte@horuxfin.com',
subject: 'Contratos expirados el día de hoy',
html: `
<h2>Notificación contratos expirados el día de hoy</h2>
<p>Cantidad de contratos <b>${totalDayExpired}</b> que expiraron hoy.</p>
<ul>Los contratos expirados son</ul>
<ul>
${expiredContracts.map(c => {
const fecha = new Date(c.end_contract);
const fechaFormateada = fecha.toLocaleDateString('es-MX');
return `<li>
ID:${c.id_contract} - RFC: ${c.rfc_employee}
- Nombre: ${c.name_employee} -
fecha de expiración: ${fechaFormateada}
</li>`;
}).join('')}
</ul>
<p>Por favor considere si seran recontratados o no para que se inicie el proceso.</p>`
};
await transporter.sendMail(mailOptions);
res.json({
message: 'Correo enviado con contratos expirados hoy',
data:totalDayExpired
});
}else{
res.json({
message: 'Hoy no expiro ningun contrato',
data:totalDayExpired
});
}
}catch(error){
console.error(error);
res.status(500).json({ status: 500, message: 'No se pudo mandar el correo con los contratos por expirar' });
}
};
const expiredContractsMonth = async (req, res) => {
try{
// Verifica si hoy es el último día del mes
if (isLastDayOfMonth()) {
return res.status(200).json({
message: 'El proceso solo se ejecuta el último día del mes',
executed: false
});
}
const endpointName = 'expiredContractsMonth';
const current = new Date();
const mes = current.getMonth() + 1;
const anio = current.getFullYear();
const validateEndpoint = await pool.query('SELECT validateendpoint($1,$2::int,$3::int) AS total',[endpointName,mes,anio]);
if (validateEndpoint.rows[0].total > 0) {
return res.status(400).json({
message: `Este endpoint ya fue ejecutado en ${current.toLocaleString('es-MX', { month: 'long' })}.`
});
}else
{
await pool.query(`INSERT INTO endpoint_logs (endpoint_name) VALUES ($1)`,[endpointName]);
const expiredCountTotal = await pool.query('SELECT COUNT(*) AS total FROM getexpirecontractsmonth()');
const expiredResult = await pool.query('SELECT * FROM getexpirecontractsmonth()');
const userEmails = await pool.query('SELECT * FROM getusersmailneartoend()');
const totalExpired = expiredCountTotal.rows[0].total;
const expiredContracts = expiredResult.rows;
const currentMonth = new Date().toLocaleString('es-MX', { month: 'long' });
const users = userEmails.rows;
if(totalExpired >= 1 && users.length > 0){
const recipients = users.map(u => u.email);
const mailOptions = {
from: 'soporte@horuxfin.com',
to: recipients,
subject: `Contratos expirados en el mes ${currentMonth} `,
html: `
<div style="max-width:600px;margin:0 auto;font-family:Arial,Helvetica,sans-serif;
background-color:#ffffff;border:1px solid #e5e5e5;border-radius:8px;
overflow:hidden;">
<!-- Header -->
<div style="background-color:#7a0c1e;padding:18px 22px;">
<h2 style="margin:0;color:#ffffff;font-size:20px;font-weight:600;">
Notificación contratos expirados del mes de ${currentMonth}
</h2>
</div>
<!-- Body -->
<div style="padding:22px;color:#333333;font-size:14px;line-height:1.6;">
<p style="margin-top:0;">
En este mes expiraron <b>${totalExpired}.
</p>
<p style="margin-top:0;">
Los contratos expirados son en el mes fueron:
</p>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;border:1px solid #ddd;">
<thead>
<tr style="background-color:#f4f4f4;">
<th align="left" style="padding:8px;border:1px solid #ddd;">ID contract</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">RFC</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Nombre</th>
<th align="left" style="padding:8px;border:1px solid #ddd;">Fecha que expiro:</th>
</tr>
</thead>
<tbody>
${expiredContracts.map(c => {
const fin = formatDateDMY(c.end_contract);
return `
<tr>
<td style="padding:8px;border:1px solid #ddd;">${c.id_contract}</td>
<td style="padding:8px;border:1px solid #ddd;">${c.rfc_employee}</td>
<td style="padding:8px;border:1px solid #ddd;">${c.name_employee}</td>
<td style="padding:8px;border:1px solid #ddd;">${fin}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
<!-- CTA -->
<div style="text-align:center;margin-top:32px;">
<a href="https://hacienda.consultoria-as.com/"
style="background-color:#7a0c1e;color:#ffffff;
text-decoration:none;font-weight:bold;
padding:14px 28px;border-radius:6px;
display:inline-block;">
Revisar en la web
</a>
</div>
<p style="margin-top:16px;font-size:12px;color:#777777;text-align:center;">
Le recomendamos generar el contrato en caso de recontratación.
</p>
</div>
<!-- Footer -->
<div style="background-color:#f6f6f6;padding:12px 20px;
font-size:12px;color:#888888;text-align:center;">
Sistema de gestión financiera · Consultoría AS
</div>
</div>`
};
await transporter.sendMail(mailOptions);
res.json({
message: 'Correo enviado con contratos expirados en el mes',
data:totalExpired
});
}else{
res.json({
message: 'Este mes no expiro ningun contrato',
data:totalExpired
});
}}
}catch(error){
console.error(error);
res.status(500).json({ status: 500, message: 'No se pudo mandar el correo con los contratos por expirar' });
}
};
const isLastDayOfMonth = () => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
return tomorrow.getDate() === 1;
};
const formatDateDMY = (value) => {
if (!value) return '';
const date = value instanceof Date ? value : new Date(value);
if (isNaN(date.getTime())) return '';
return date.toLocaleDateString('es-MX', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
module.exports = {contractsNearToEnd,paymentDeadLine,expensesspecial,
emailbirthday,expiredcontracts,expiredContractsMonth,expensesNearToDeadline};