Files
app/src/services/leave-notification.ts
BOHA 5233db2002 v1.8.0: warehouse module (16 commits), docházka mzda model, leave_type=holiday removal, api.ts race fix
Highlights:
- Warehouse module: receipts, issues, reservations, inventory, reports, dashboard,
  master data (categories, suppliers, locations), FIFO service, integration tests
- Docházka: mzda PDF counting model (Odpracováno / Vč. svátků / Přesčas / Svátek /
  So/Ne / Noc) with Czech weekday names and decimal hours
- AttendanceAdmin/AttendanceHistory KPI cards unified to mzda formula with
  fund bar colored by delta, badges for Práce/Dov/Nem/Sv/Nep
- Remove leave_type=holiday entirely (auto-computed from Czech public holidays)
- Allow multiple work shifts per day (overlap detection only)
- Pre-flight refresh in api.ts eliminates spurious 401s on fresh page loads
- Prisma: company_settings gets 6 nullable columns for warehouse numbering
  (PRI/VYD/INV prefixes, default patterns); migration seeds defaults
2026-06-03 23:13:10 +02:00

125 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { sendMail } from "./mailer";
import { config } from "../config/env";
import { localDateCzStr, localDateTimeCzStr } from "../utils/date";
import { getSystemSettings } from "./system-settings";
const LEAVE_TYPE_LABELS: Record<string, string> = {
vacation: "Dovolená",
sick: "Nemocenská",
unpaid: "Neplacené volno",
};
function formatDate(dateStr: string): string {
try {
const d = new Date(dateStr);
return localDateCzStr(d);
} catch {
return dateStr;
}
}
function escapeHtml(str: string): string {
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
// Strip characters that could break out of the email Subject: header
// (CRLF, NUL) or otherwise pollute the envelope. Without this, a user
// can set their first_name to "attacker\nBcc: victim@x.com" via the
// profile editor and pivot into sending mail as noreply@boha.cz.
function sanitizeHeaderValue(str: string): string {
// eslint-disable-next-line no-control-regex
return str
.replace(/[\r\n\t\0]/g, " ")
.replace(/\s+/g, " ")
.trim();
}
interface LeaveRequestData {
leave_type: string;
date_from: string;
date_to: string;
total_days: number;
total_hours: number;
notes?: string | null;
}
export async function notifyNewLeaveRequest(
request: LeaveRequestData,
employeeName: string,
): Promise<void> {
const settings = await getSystemSettings();
const notifyEmail = settings.leave_notify_email || config.email.leaveNotify;
if (!notifyEmail) return;
const leaveType = LEAVE_TYPE_LABELS[request.leave_type] || request.leave_type;
const dateFrom = formatDate(request.date_from);
const dateTo = formatDate(request.date_to);
const notes = request.notes || "";
const subject = `Nová žádost o nepřítomnost - ${sanitizeHeaderValue(employeeName)} (${sanitizeHeaderValue(leaveType)})`;
const appUrl = config.appUrl || "";
const approvalLink = appUrl
? `<p style="margin-top: 20px;">
<a href="${escapeHtml(appUrl)}/leave-approval"
style="background: #de3a3a; color: #fff; padding: 10px 20px;
text-decoration: none; border-radius: 5px;">
Přejít ke schvalování
</a>
</p>`
: "";
const now = new Date();
const timestamp = localDateTimeCzStr(now);
const html = `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<h2 style="color: #de3a3a;">Nová žádost o nepřítomnost</h2>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr>
<td style="padding: 10px; background: #f5f5f5; font-weight: bold; width: 180px;">Zaměstnanec:</td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">${escapeHtml(employeeName)}</td>
</tr>
<tr>
<td style="padding: 10px; background: #f5f5f5; font-weight: bold;">Typ:</td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">${escapeHtml(leaveType)}</td>
</tr>
<tr>
<td style="padding: 10px; background: #f5f5f5; font-weight: bold;">Období:</td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">${dateFrom} ${dateTo}</td>
</tr>
<tr>
<td style="padding: 10px; background: #f5f5f5; font-weight: bold;">Pracovní dny:</td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">${request.total_days} dní (${request.total_hours} hodin)</td>
</tr>
${
notes
? `<tr>
<td style="padding: 10px; background: #f5f5f5; font-weight: bold;">Poznámka:</td>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">${escapeHtml(notes)}</td>
</tr>`
: ""
}
</table>
${approvalLink}
<hr style="margin: 30px 0; border: none; border-top: 1px solid #ddd;">
<p style="font-size: 12px; color: #999;">
Tato zpráva byla automaticky vygenerována systémem.<br>
Datum: ${timestamp}
</p>
</body>
</html>`;
const sent = await sendMail(notifyEmail, subject, html);
if (!sent) {
console.error(
`LeaveNotification: Failed to send notification to ${notifyEmail}`,
);
}
}