p4-hotfixes #13

Merged
jason merged 2 commits from p4-hotfixes into master 2026-03-06 14:38:57 -06:00
Showing only changes of commit 60e9da488c - Show all commits

View File

@@ -1,5 +1,3 @@
/** PDF template with MPM logo from /static/mpm-logo.png */
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' },
@@ -10,17 +8,12 @@ const TIERS = [
{ 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', timeZone: 'America/Chicago' });
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
timeZone: 'America/Chicago'
});
} }
function formatDateTime(d, t) { function formatDateTime(d, t) {
@@ -43,10 +36,7 @@ function buildHtml(v, score) {
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', dateStyle: 'full', timeStyle: 'short' });
timeZone: 'America/Chicago',
dateStyle: 'full', timeStyle: 'short'
});
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html lang="en"> <html lang="en">
@@ -55,86 +45,33 @@ function buildHtml(v, score) {
<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, #000000, #151622); color: white; padding: 22px 32px; display: flex; align-items: center; justify-content: space-between; }
.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; } .header-left { display: flex; align-items: center; }
.logo { .logo { height: 28px; margin-right: 12px; }
height: 28px;
margin-right: 12px;
}
.header h1 { font-size: 20px; letter-spacing: 0.5px; } .header h1 { font-size: 20px; letter-spacing: 0.5px; }
.header p { font-size: 11px; opacity: 0.85; margin-top: 3px; } .header p { font-size: 11px; opacity: 0.85; margin-top: 3px; }
.doc-id { text-align: right; font-size: 11px; opacity: 0.8; } .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; background: #000000; padding: 8px 14px; border-radius: 4px 4px 0 0; margin-bottom: 0; }
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; } 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-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-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 { display: inline-block; padding: 5px 14px; border-radius: 14px; font-size: 12px; font-weight: 700; border: 2px solid currentColor; }
.tier-badge { .tier-change { background: #fff3cd; border: 2px solid #ffc107; border-radius: 6px; padding: 12px 16px; margin: 16px 0; font-size: 12px; color: #856404; }
display: inline-block; padding: 5px 14px; .points-display { background: #fff3cd; border: 2px solid #ffc107; border-radius: 6px; padding: 16px; margin: 16px 0; text-align: center; }
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 .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: 50px; page-break-inside: avoid; }
.sig-section { margin-top: 40px; page-break-inside: avoid; } .sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; margin-top: 32px; }
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin-top: 24px; } .sig-block { border-top: 1.5px solid #333; padding-top: 10px; min-height: 80px; }
.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: 10px; min-height: 60px; margin-top: 36px; }
.footer-bar { margin-top: 40px; padding: 10px 0; border-top: 2px solid #000000; font-size: 10px; color: #888; text-align: center; }
.footer-bar { .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; }
margin-top: 40px; padding: 10px 0; .notice { background: #e7f3ff; border-left: 4px solid #2196F3; padding: 10px 14px; margin: 16px 0; font-size: 12px; }
border-top: 2px solid #000000; .policy-context { background: #f8f9fa; border-left: 3px solid #667eea; padding: 12px 16px; margin: 12px 0; font-size: 12px; color: #444; border-radius: 4px; }
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> </style>
</head> </head>
<body> <body>
@@ -155,9 +92,7 @@ function buildHtml(v, score) {
<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</div>
⚠ CONFIDENTIAL — For authorized HR and management use only
</div>
<div class="section"> <div class="section">
<div class="section-title">Employee Information</div> <div class="section-title">Employee Information</div>
@@ -179,26 +114,17 @@ function buildHtml(v, score) {
${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"><strong>Incident Details:</strong><br />${v.details}</div>` : ''}
<div class="policy-context">
<strong>Incident Details:</strong><br />
${v.details}
</div>` : ''}
</div> </div>
<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="lbl">Points Assessed — This Violation</div></div>
<div class="pts">${v.points}</div>
<div class="lbl">Points Assessed — This Violation</div>
</div>
<div class="score-box"> <div class="score-box">
<div class="score-cell"> <div class="score-cell">
<div class="score-num" style="color:${tier.color};">${activePts}</div> <div class="score-num" style="color:${tier.color};">${activePts}</div>
<div class="score-lbl">Active Points (Prior)</div> <div class="score-lbl">Active Points (Prior)</div>
<div style="margin-top:6px;"> <div style="margin-top:6px;"><span class="tier-badge" style="color:${tier.color};">${tier.label}</span></div>
<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-cell" style="font-size:28px; font-weight:300; color:#ccc; line-height:1.8;">+</div>
<div class="score-cell"> <div class="score-cell">
@@ -209,46 +135,26 @@ function buildHtml(v, score) {
<div class="score-cell"> <div class="score-cell">
<div class="score-num" style="color:${newTier.color};">${newTotal}</div> <div class="score-num" style="color:${newTier.color};">${newTotal}</div>
<div class="score-lbl">New Active Total</div> <div class="score-lbl">New Active Total</div>
<div style="margin-top:6px;"> <div style="margin-top:6px;"><span class="tier-badge" style="color:${newTier.color};">${newTier.label}</span></div>
<span class="tier-badge" style="color:${newTier.color};">${newTier.label}</span>
</div> </div>
</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>` : ''}
${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>
<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;">Tier</th></tr>
<th style="padding:7px 12px; text-align:left; font-size:12px;">Points</th> ${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('')}
<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> </table>
</div> </div>
<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. Accumulation of points may result in tier escalation and associated consequences as outlined in the Employee Handbook.</div>
<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="sig-section">
<div class="section-title" style="background:#000000;">Acknowledgement & Signatures</div> <div class="section-title" style="background:#000000;">Acknowledgement & Signatures</div>
<div style="padding: 16px 0;"> <div style="padding: 20px 0;">
<p style="font-size:12px; color:#555; margin-bottom:28px; line-height:1.6;"> <p style="font-size:12px; color:#555; margin-bottom:32px; 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>
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 class="sig-grid">
<div> <div>
<div class="sig-block"><div class="sig-label">Employee Signature</div></div> <div class="sig-block"><div class="sig-label">Employee Signature</div></div>
@@ -262,11 +168,7 @@ function buildHtml(v, score) {
</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; ${v.employee_name} &nbsp;|&nbsp; Incident: ${v.incident_date} &nbsp;|&nbsp; Message Point Media Internal Use Only</div>
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> </div>
</body> </body>