p4-hotfixes #10

Merged
jason merged 4 commits from p4-hotfixes into master 2026-03-06 14:13:15 -06:00
Showing only changes of commit 066f95cc88 - Show all commits

View File

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