Files
cpas/pdf/template.js
2026-03-06 14:12:00 -06:00

277 lines
9.9 KiB
JavaScript
Executable File
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.
/** PDF template with MPM logo from /static/mpm-logo.png */
const TIERS = [
{ min: 0, max: 4, label: 'Tier 0-1 — Elite Standing', color: '#28a745' },
{ min: 5, max: 9, label: 'Tier 1 — Realignment', color: '#856404' },
{ min: 10, max: 14, label: 'Tier 2 — Administrative Lockdown', color: '#d9534f' },
{ min: 15, max: 19, label: 'Tier 3 — Verification', color: '#d9534f' },
{ min: 20, max: 24, label: 'Tier 4 — Risk Mitigation', color: '#c0392b' },
{ min: 25, max: 29, label: 'Tier 5 — Final Decision', color: '#c0392b' },
{ min: 30, max: 999,label: 'Tier 6 — Separation', color: '#721c24' },
];
function getTier(points) {
return TIERS.find(t => points >= t.min && points <= t.max) || TIERS[0];
}
function formatDate(d) {
if (!d) return '—';
const dt = new Date(d + 'T12:00:00');
return dt.toLocaleDateString('en-US', {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
timeZone: 'America/Chicago'
});
}
function formatDateTime(d, t) {
const date = formatDate(d);
return t ? `${date} at ${t}` : date;
}
function row(label, value) {
return `
<tr>
<td style="font-weight:600; color:#555; width:200px; padding:8px 12px; border-bottom:1px solid #eee; white-space:nowrap;">${label}</td>
<td style="padding:8px 12px; border-bottom:1px solid #eee; color:#222;">${value || '—'}</td>
</tr>`;
}
function buildHtml(v, score) {
const activePts = score.active_points || 0;
const tier = getTier(activePts);
const newTotal = activePts + v.points;
const newTier = getTier(newTotal);
const tierChange = tier.label !== newTier.label;
const generatedAt = new Date().toLocaleString('en-US', {
timeZone: 'America/Chicago',
dateStyle: 'full', timeStyle: 'short'
});
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 13px; color: #222; background: #fff; }
.header {
background: linear-gradient(135deg, #000000, #111217);
color: white;
padding: 22px 32px;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-left { display: flex; align-items: center; }
.logo {
height: 28px;
margin-right: 12px;
}
.header h1 { font-size: 20px; letter-spacing: 0.5px; }
.header p { font-size: 11px; opacity: 0.85; margin-top: 3px; }
.doc-id { text-align: right; font-size: 11px; opacity: 0.8; }
.section { margin: 20px 0; }
.section-title {
font-size: 14px; font-weight: 700; color: white;
background: #000000; padding: 8px 14px;
border-radius: 4px 4px 0 0; margin-bottom: 0;
}
table { width: 100%; border-collapse: collapse; border: 1px solid #ddd; border-top: none; }
.score-box {
display: flex; gap: 20px; flex-wrap: wrap;
background: #f8f9fa; border: 1px solid #ddd;
border-radius: 6px; padding: 16px 20px; margin: 20px 0;
}
.score-cell { flex: 1; min-width: 120px; text-align: center; }
.score-num { font-size: 28px; font-weight: 800; }
.score-lbl { font-size: 11px; color: #666; margin-top: 2px; }
.tier-badge {
display: inline-block; padding: 5px 14px;
border-radius: 14px; font-size: 12px; font-weight: 700;
border: 2px solid currentColor;
}
.tier-change {
background: #fff3cd; border: 2px solid #ffc107;
border-radius: 6px; padding: 12px 16px; margin: 16px 0;
font-size: 12px; color: #856404;
}
.points-display {
background: #fff3cd; border: 2px solid #ffc107;
border-radius: 6px; padding: 16px; margin: 16px 0; text-align: center;
}
.points-display .pts { font-size: 36px; font-weight: 800; color: #d4af37; }
.points-display .lbl { font-size: 12px; color: #666; }
.sig-section { margin-top: 40px; page-break-inside: avoid; }
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin-top: 24px; }
.sig-block { border-top: 1.5px solid #333; padding-top: 8px; min-height: 60px; }
.sig-label { font-size: 11px; color: #555; font-weight: 600; }
.sig-date-block { border-top: 1.5px solid #333; padding-top: 8px; min-height: 50px; margin-top: 32px; }
.footer-bar {
margin-top: 40px; padding: 10px 0;
border-top: 2px solid #000000;
font-size: 10px; color: #888; text-align: center;
}
.confidential {
background: #f8d7da; border: 1px solid #f5c6cb;
border-radius: 4px; padding: 6px 12px;
font-size: 11px; color: #721c24; font-weight: 600;
text-align: center; margin-bottom: 16px;
}
.notice {
background: #e7f3ff; border-left: 4px solid #2196F3;
padding: 10px 14px; margin: 16px 0; font-size: 12px;
}
.policy-context {
background: #f8f9fa; border-left: 3px solid #667eea;
padding: 12px 16px; margin: 12px 0; font-size: 12px; color: #444;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="header">
<div class="header-left">
<img src="/static/mpm-logo.png" class="logo" />
<div>
<h1>CPAS Individual Violation Record</h1>
<p>Message Point Media — Comprehensive Professional Accountability System</p>
</div>
</div>
<div class="doc-id">
Document ID: CPAS-${v.id.toString().padStart(5,'0')}<br />
Generated: ${generatedAt}
</div>
</div>
<div style="padding: 0 4px;">
<div class="confidential" style="margin-top:16px;">
⚠ CONFIDENTIAL — For authorized HR and management use only
</div>
<div class="section">
<div class="section-title">Employee Information</div>
<table>
${row('Employee Name', `<strong>${v.employee_name}</strong>`)}
${row('Department', v.department)}
${row('Supervisor', v.supervisor)}
${row('Witness / Documenting Officer', v.witness_name)}
</table>
</div>
<div class="section">
<div class="section-title">Violation Details</div>
<table>
${row('Violation Type', `<strong>${v.violation_name}</strong>`)}
${row('Category', v.category)}
${row('Policy Reference', 'Chapter 4, Section 5 — Comprehensive Professional Accountability System (CPAS)')}
${row('Incident Date / Time', formatDateTime(v.incident_date, v.incident_time))}
${v.location ? row('Location / Context', v.location) : ''}
${row('Submitted By', v.submitted_by || 'System')}
</table>
${v.details ? `
<div class="policy-context">
<strong>Incident Details:</strong><br />
${v.details}
</div>` : ''}
</div>
<div class="section">
<div class="section-title">CPAS Point Assessment</div>
<div class="points-display">
<div class="pts">${v.points}</div>
<div class="lbl">Points Assessed — This Violation</div>
</div>
<div class="score-box">
<div class="score-cell">
<div class="score-num" style="color:${tier.color};">${activePts}</div>
<div class="score-lbl">Active Points (Prior)</div>
<div style="margin-top:6px;">
<span class="tier-badge" style="color:${tier.color};">${tier.label}</span>
</div>
</div>
<div class="score-cell" style="font-size:28px; font-weight:300; color:#ccc; line-height:1.8;">+</div>
<div class="score-cell">
<div class="score-num" style="color:#d4af37;">${v.points}</div>
<div class="score-lbl">Points — This Violation</div>
</div>
<div class="score-cell" style="font-size:28px; font-weight:300; color:#ccc; line-height:1.8;">=</div>
<div class="score-cell">
<div class="score-num" style="color:${newTier.color};">${newTotal}</div>
<div class="score-lbl">New Active Total</div>
<div style="margin-top:6px;">
<span class="tier-badge" style="color:${newTier.color};">${newTier.label}</span>
</div>
</div>
</div>
${tierChange ? `
<div class="tier-change">
<strong>⚠ Tier Escalation:</strong> This violation advances the employee from
<strong>${tier.label}</strong> to <strong>${newTier.label}</strong>.
</div>` : ''}
</div>
<div class="section">
<div class="section-title">CPAS Tier Reference</div>
<table>
<tr style="background:#f8f9fa;">
<th style="padding:7px 12px; text-align:left; font-size:12px;">Points</th>
<th style="padding:7px 12px; text-align:left; font-size:12px;">Tier</th>
</tr>
${TIERS.map(t => `
<tr style="${newTotal >= t.min && newTotal <= t.max ? 'background:#fff3cd; font-weight:700;' : ''}">
<td style="padding:6px 12px; border-bottom:1px solid #eee; font-size:12px;">${t.min === 30 ? '30+' : t.min + '' + t.max}</td>
<td style="padding:6px 12px; border-bottom:1px solid #eee; font-size:12px; color:${t.color};">${t.label}</td>
</tr>`).join('')}
</table>
</div>
<div class="notice">
<strong>Employee Notice:</strong> CPAS points remain active for a rolling 90-day period from the date of each incident.
Accumulation of points may result in tier escalation and associated consequences as outlined in the Employee Handbook.
</div>
<div class="sig-section">
<div class="section-title" style="background:#000000;">Acknowledgement & Signatures</div>
<div style="padding: 16px 0;">
<p style="font-size:12px; color:#555; margin-bottom:28px; line-height:1.6;">
By signing below, the employee acknowledges receipt of this violation record.
Acknowledgement does not imply agreement. The employee may submit a written
response within 5 business days.
</p>
<div class="sig-grid">
<div>
<div class="sig-block"><div class="sig-label">Employee Signature</div></div>
<div class="sig-date-block"><div class="sig-label">Date</div></div>
</div>
<div>
<div class="sig-block"><div class="sig-label">Supervisor / Documenting Officer Signature</div></div>
<div class="sig-date-block"><div class="sig-label">Date</div></div>
</div>
</div>
</div>
</div>
<div class="footer-bar">
CPAS Violation Record — Document ID: CPAS-${v.id.toString().padStart(5,'0')} &nbsp;|&nbsp;
${v.employee_name} &nbsp;|&nbsp; Incident: ${v.incident_date} &nbsp;|&nbsp;
Message Point Media Internal Use Only
</div>
</div>
</body>
</html>`;
}
module.exports = buildHtml;