p4-hotfixes #10
39
Dockerfile
39
Dockerfile
@@ -1,61 +1,28 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Stage 1: Builder
|
||||
# Installs ALL dependencies and compiles the React frontend inside Docker.
|
||||
# Nothing needs to be installed on the host machine except Docker itself.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Install backend deps
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
|
||||
# Install frontend deps and build React app
|
||||
COPY client/package.json ./client/
|
||||
RUN cd client && npm install
|
||||
|
||||
COPY client/ ./client/
|
||||
RUN cd client && npm run build
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Stage 2: Production image
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Chromium for Puppeteer PDF generation
|
||||
RUN apk add --no-cache \
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
harfbuzz \
|
||||
ca-certificates \
|
||||
ttf-freefont
|
||||
|
||||
RUN apk add --no-cache chromium nss freetype harfbuzz ca-certificates ttf-freefont
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3001
|
||||
ENV DB_PATH=/data/cpas.db
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy backend node_modules and compiled frontend from builder
|
||||
COPY --from=builder /build/node_modules ./node_modules
|
||||
COPY --from=builder /build/client/dist ./client/dist
|
||||
|
||||
# Copy all backend source files
|
||||
COPY server.js ./
|
||||
COPY package.json ./
|
||||
COPY db/ ./db/
|
||||
COPY pdf/ ./pdf/
|
||||
|
||||
# Ensure data directory exists
|
||||
COPY client/public/static ./client/dist/static
|
||||
RUN mkdir -p /data
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD wget -qO- http://localhost:3001/api/health || exit 1
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD wget -qO- http://localhost:3001/api/health || exit 1
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
@@ -8,16 +8,19 @@ const tabs = [
|
||||
];
|
||||
|
||||
const s = {
|
||||
app: { minHeight: '100vh', background: '#f5f6fa', fontFamily: "'Segoe UI', Arial, sans-serif" },
|
||||
nav: { background: 'linear-gradient(135deg, #2c3e50, #34495e)', padding: '0 40px', display: 'flex', alignItems: 'center', gap: 0 },
|
||||
logo: { color: 'white', fontWeight: 800, fontSize: '18px', letterSpacing: '0.5px', marginRight: '32px', padding: '18px 0' },
|
||||
app: { minHeight: '100vh', background: '#050608', fontFamily: "'Segoe UI', Arial, sans-serif", color: '#f8f9fa' },
|
||||
nav: { background: '#000000', padding: '0 40px', display: 'flex', alignItems: 'center', gap: 0, borderBottom: '1px solid #333' },
|
||||
logoWrap: { display: 'flex', alignItems: 'center', marginRight: '32px', padding: '14px 0' },
|
||||
logoImg: { height: '28px', marginRight: '10px' },
|
||||
logoText: { color: '#f8f9fa', fontWeight: 800, fontSize: '18px', letterSpacing: '0.5px' },
|
||||
tab: (active) => ({
|
||||
padding: '18px 22px', color: active ? 'white' : 'rgba(255,255,255,0.6)',
|
||||
borderBottom: active ? '3px solid #667eea' : '3px solid transparent',
|
||||
padding: '18px 22px',
|
||||
color: active ? '#f8f9fa' : 'rgba(248,249,250,0.6)',
|
||||
borderBottom: active ? '3px solid #d4af37' : '3px solid transparent',
|
||||
cursor: 'pointer', fontWeight: active ? 700 : 400, fontSize: '14px',
|
||||
background: 'none', border: 'none', borderBottom: active ? '3px solid #667eea' : '3px solid transparent',
|
||||
background: 'none', border: 'none',
|
||||
}),
|
||||
card: { maxWidth: '1100px', margin: '30px auto', background: 'white', borderRadius: '10px', boxShadow: '0 2px 12px rgba(0,0,0,0.08)' },
|
||||
card: { maxWidth: '1100px', margin: '30px auto', background: '#111217', borderRadius: '10px', boxShadow: '0 2px 16px rgba(0,0,0,0.6)', border: '1px solid #222' },
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
@@ -25,7 +28,10 @@ export default function App() {
|
||||
return (
|
||||
<div style={s.app}>
|
||||
<nav style={s.nav}>
|
||||
<div style={s.logo}>CPAS Tracker</div>
|
||||
<div style={s.logoWrap}>
|
||||
<img src="/static/mpm-logo.png" alt="MPM" style={s.logoImg} />
|
||||
<div style={s.logoText}>CPAS Tracker</div>
|
||||
</div>
|
||||
{tabs.map(t => (
|
||||
<button key={t.id} style={s.tab(tab === t.id)} onClick={() => setTab(t.id)}>
|
||||
{t.label}
|
||||
|
||||
@@ -3,7 +3,7 @@ import axios from 'axios';
|
||||
import CpasBadge, { getTier } from './CpasBadge';
|
||||
import EmployeeModal from './EmployeeModal';
|
||||
|
||||
const AT_RISK_THRESHOLD = 2; // points within next tier boundary
|
||||
const AT_RISK_THRESHOLD = 2;
|
||||
|
||||
const TIERS = [
|
||||
{ min: 0, max: 4 },
|
||||
@@ -17,8 +17,7 @@ const TIERS = [
|
||||
|
||||
function nextTierBoundary(points) {
|
||||
for (const t of TIERS) {
|
||||
if (points >= t.min && points <= t.max && t.max < 999)
|
||||
return t.max + 1;
|
||||
if (points >= t.min && points <= t.max && t.max < 999) return t.max + 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -29,22 +28,22 @@ function isAtRisk(points) {
|
||||
}
|
||||
|
||||
const s = {
|
||||
wrap: { padding: '40px' },
|
||||
wrap: { padding: '32px 40px', color: '#f8f9fa' },
|
||||
header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px', flexWrap: 'wrap', gap: '12px' },
|
||||
title: { fontSize: '24px', fontWeight: 700, color: '#2c3e50' },
|
||||
subtitle: { fontSize: '13px', color: '#888', marginTop: '3px' },
|
||||
title: { fontSize: '24px', fontWeight: 700, color: '#f8f9fa' },
|
||||
subtitle: { fontSize: '13px', color: '#b5b5c0', marginTop: '3px' },
|
||||
statsRow: { display: 'flex', gap: '16px', flexWrap: 'wrap', marginBottom: '28px' },
|
||||
statCard: { flex: '1', minWidth: '140px', background: '#f8f9fa', border: '1px solid #dee2e6', borderRadius: '8px', padding: '16px', textAlign: 'center' },
|
||||
statNum: { fontSize: '28px', fontWeight: 800, color: '#2c3e50' },
|
||||
statLbl: { fontSize: '11px', color: '#888', marginTop: '4px' },
|
||||
search: { padding: '10px 14px', border: '1px solid #ddd', borderRadius: '6px', fontSize: '14px', width: '260px' },
|
||||
table: { width: '100%', borderCollapse: 'collapse', background: 'white', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 1px 4px rgba(0,0,0,0.08)' },
|
||||
th: { background: '#34495e', color: 'white', padding: '10px 14px', textAlign: 'left', fontSize: '12px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px' },
|
||||
td: { padding: '11px 14px', borderBottom: '1px solid #f0f0f0', fontSize: '13px', verticalAlign: 'middle' },
|
||||
nameBtn: { background: 'none', border: 'none', cursor: 'pointer', fontWeight: 600, color: '#667eea', fontSize: '14px', padding: 0, textDecoration: 'underline dotted' },
|
||||
atRiskBadge: { display: 'inline-block', marginLeft: '8px', padding: '2px 8px', borderRadius: '10px', fontSize: '10px', fontWeight: 700, background: '#fff3cd', color: '#856404', border: '1px solid #ffc107', verticalAlign: 'middle' },
|
||||
zeroRow: { color: '#aaa', fontStyle: 'italic', fontSize: '12px' },
|
||||
refreshBtn:{ padding: '9px 18px', background: '#667eea', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 600, fontSize: '13px' },
|
||||
statCard: { flex: '1', minWidth: '140px', background: '#181924', border: '1px solid #30313f', borderRadius: '8px', padding: '16px', textAlign: 'center' },
|
||||
statNum: { fontSize: '28px', fontWeight: 800, color: '#f8f9fa' },
|
||||
statLbl: { fontSize: '11px', color: '#b5b5c0', marginTop: '4px' },
|
||||
search: { padding: '10px 14px', border: '1px solid #333544', borderRadius: '6px', fontSize: '14px', width: '260px', background: '#050608', color: '#f8f9fa' },
|
||||
table: { width: '100%', borderCollapse: 'collapse', background: '#111217', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 1px 8px rgba(0,0,0,0.6)', border: '1px solid #222' },
|
||||
th: { background: '#000000', color: '#f8f9fa', padding: '10px 14px', textAlign: 'left', fontSize: '12px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px' },
|
||||
td: { padding: '11px 14px', borderBottom: '1px solid #1c1d29', fontSize: '13px', verticalAlign: 'middle', color: '#f8f9fa' },
|
||||
nameBtn: { background: 'none', border: 'none', cursor: 'pointer', fontWeight: 600, color: '#d4af37', fontSize: '14px', padding: 0, textDecoration: 'underline dotted' },
|
||||
atRiskBadge: { display: 'inline-block', marginLeft: '8px', padding: '2px 8px', borderRadius: '10px', fontSize: '10px', fontWeight: 700, background: '#3b2e00', color: '#ffd666', border: '1px solid #d4af37', verticalAlign: 'middle' },
|
||||
zeroRow: { color: '#77798a', fontStyle: 'italic', fontSize: '12px' },
|
||||
refreshBtn:{ padding: '9px 18px', background: '#d4af37', color: '#000', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 600, fontSize: '13px' },
|
||||
};
|
||||
|
||||
export default function Dashboard() {
|
||||
@@ -90,33 +89,31 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Stat cards ───────────────────────────────────────── */}
|
||||
<div style={s.statsRow}>
|
||||
<div style={s.statCard}>
|
||||
<div style={s.statNum}>{employees.length}</div>
|
||||
<div style={s.statLbl}>Total Employees</div>
|
||||
</div>
|
||||
<div style={{ ...s.statCard, borderTop: '3px solid #28a745' }}>
|
||||
<div style={{ ...s.statNum, color: '#28a745' }}>{cleanCount}</div>
|
||||
<div style={{ ...s.statNum, color: '#6ee7b7' }}>{cleanCount}</div>
|
||||
<div style={s.statLbl}>Elite Standing (0 pts)</div>
|
||||
</div>
|
||||
<div style={{ ...s.statCard, borderTop: '3px solid #856404' }}>
|
||||
<div style={{ ...s.statNum, color: '#856404' }}>{activeCount}</div>
|
||||
<div style={{ ...s.statCard, borderTop: '3px solid #d4af37' }}>
|
||||
<div style={{ ...s.statNum, color: '#ffd666' }}>{activeCount}</div>
|
||||
<div style={s.statLbl}>With Active Points</div>
|
||||
</div>
|
||||
<div style={{ ...s.statCard, borderTop: '3px solid #ffc107' }}>
|
||||
<div style={{ ...s.statNum, color: '#856404' }}>{atRiskCount}</div>
|
||||
<div style={{ ...s.statCard, borderTop: '3px solid #ffb020' }}>
|
||||
<div style={{ ...s.statNum, color: '#ffdf8a' }}>{atRiskCount}</div>
|
||||
<div style={s.statLbl}>At Risk (≤{AT_RISK_THRESHOLD} pts to next tier)</div>
|
||||
</div>
|
||||
<div style={{ ...s.statCard, borderTop: '3px solid #c0392b' }}>
|
||||
<div style={{ ...s.statNum, color: '#c0392b' }}>{maxPoints}</div>
|
||||
<div style={{ ...s.statNum, color: '#ff8a80' }}>{maxPoints}</div>
|
||||
<div style={s.statLbl}>Highest Active Score</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Scoreboard table ─────────────────────────────────── */}
|
||||
{loading ? (
|
||||
<p style={{ color: '#aaa', textAlign: 'center', padding: '40px' }}>Loading…</p>
|
||||
<p style={{ color: '#77798a', textAlign: 'center', padding: '40px' }}>Loading…</p>
|
||||
) : (
|
||||
<table style={s.table}>
|
||||
<thead>
|
||||
@@ -139,8 +136,8 @@ export default function Dashboard() {
|
||||
const tier = getTier(emp.active_points);
|
||||
const boundary = nextTierBoundary(emp.active_points);
|
||||
return (
|
||||
<tr key={emp.id} style={{ background: risk ? '#fffdf0' : i % 2 === 0 ? 'white' : '#fafafa' }}>
|
||||
<td style={{ ...s.td, color: '#aaa', fontSize: '12px' }}>{i + 1}</td>
|
||||
<tr key={emp.id} style={{ background: risk ? '#181200' : i % 2 === 0 ? '#111217' : '#151622' }}>
|
||||
<td style={{ ...s.td, color: '#77798a', fontSize: '12px' }}>{i + 1}</td>
|
||||
<td style={s.td}>
|
||||
<button style={s.nameBtn} onClick={() => setSelectedId(emp.id)}>{emp.name}</button>
|
||||
{risk && (
|
||||
@@ -149,11 +146,11 @@ export default function Dashboard() {
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ ...s.td, color: '#666' }}>{emp.department || '—'}</td>
|
||||
<td style={{ ...s.td, color: '#666' }}>{emp.supervisor || '—'}</td>
|
||||
<td style={{ ...s.td, color: '#c0c2d6' }}>{emp.department || '—'}</td>
|
||||
<td style={{ ...s.td, color: '#c0c2d6' }}>{emp.supervisor || '—'}</td>
|
||||
<td style={s.td}><CpasBadge points={emp.active_points} /></td>
|
||||
<td style={{ ...s.td, fontWeight: 700, color: tier.color, fontSize: '16px' }}>{emp.active_points}</td>
|
||||
<td style={{ ...s.td, color: '#666' }}>{emp.violation_count}</td>
|
||||
<td style={{ ...s.td, color: '#c0c2d6' }}>{emp.violation_count}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@@ -161,7 +158,6 @@ export default function Dashboard() {
|
||||
</table>
|
||||
)}
|
||||
|
||||
{/* ── Employee profile modal ───────────────────────────── */}
|
||||
{selectedId && (
|
||||
<EmployeeModal
|
||||
employeeId={selectedId}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
/**
|
||||
* Builds the full HTML string for a CPAS violation PDF document.
|
||||
* Matches the styling of the original HTML violation form.
|
||||
*/
|
||||
/** PDF template with MPM logo from /static/mpm-logo.png */
|
||||
|
||||
const TIERS = [
|
||||
{ min: 0, max: 4, label: 'Tier 0-1 — Elite Standing', color: '#28a745' },
|
||||
@@ -59,16 +56,28 @@ function buildHtml(v, score) {
|
||||
* { 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, #2c3e50, #34495e); color: white; padding: 28px 32px; }
|
||||
.header h1 { font-size: 22px; letter-spacing: 0.5px; }
|
||||
.header p { font-size: 12px; opacity: 0.8; margin-top: 4px; }
|
||||
.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 { 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-title {
|
||||
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;
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; border: 1px solid #ddd; border-top: none; }
|
||||
@@ -97,7 +106,7 @@ function buildHtml(v, score) {
|
||||
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: #667eea; }
|
||||
.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; }
|
||||
@@ -108,7 +117,7 @@ function buildHtml(v, score) {
|
||||
|
||||
.footer-bar {
|
||||
margin-top: 40px; padding: 10px 0;
|
||||
border-top: 2px solid #2c3e50;
|
||||
border-top: 2px solid #000000;
|
||||
font-size: 10px; color: #888; text-align: center;
|
||||
}
|
||||
.confidential {
|
||||
@@ -130,14 +139,18 @@ function buildHtml(v, score) {
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Header -->
|
||||
<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>
|
||||
<h1>CPAS Individual Violation Record</h1>
|
||||
<p>Message Point Media — Confidential HR Document</p>
|
||||
</div>
|
||||
|
||||
<div style="padding: 0 4px;">
|
||||
@@ -146,7 +159,6 @@ function buildHtml(v, score) {
|
||||
⚠ CONFIDENTIAL — For authorized HR and management use only
|
||||
</div>
|
||||
|
||||
<!-- Employee Information -->
|
||||
<div class="section">
|
||||
<div class="section-title">Employee Information</div>
|
||||
<table>
|
||||
@@ -157,7 +169,6 @@ function buildHtml(v, score) {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Violation Details -->
|
||||
<div class="section">
|
||||
<div class="section-title">Violation Details</div>
|
||||
<table>
|
||||
@@ -168,7 +179,6 @@ function buildHtml(v, score) {
|
||||
${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 />
|
||||
@@ -176,15 +186,12 @@ function buildHtml(v, score) {
|
||||
</div>` : ''}
|
||||
</div>
|
||||
|
||||
<!-- CPAS Point Assessment -->
|
||||
<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>
|
||||
@@ -195,7 +202,7 @@ function buildHtml(v, score) {
|
||||
</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-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>
|
||||
@@ -207,16 +214,13 @@ function buildHtml(v, score) {
|
||||
</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>.
|
||||
Review associated tier consequences per the Employee Handbook.
|
||||
</div>` : ''}
|
||||
</div>
|
||||
|
||||
<!-- CPAS Tier Reference -->
|
||||
<div class="section">
|
||||
<div class="section-title">CPAS Tier Reference</div>
|
||||
<table>
|
||||
@@ -232,16 +236,13 @@ function buildHtml(v, score) {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 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,
|
||||
Chapter 4, Section 5. This document should be reviewed with the employee and signed by all parties.
|
||||
Accumulation of points may result in tier escalation and associated consequences as outlined in the Employee Handbook.
|
||||
</div>
|
||||
|
||||
<!-- Signatures — EXPANDED VERTICAL SPACING -->
|
||||
<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;">
|
||||
<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.
|
||||
@@ -250,20 +251,12 @@ function buildHtml(v, score) {
|
||||
</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 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 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>
|
||||
@@ -275,7 +268,7 @@ function buildHtml(v, score) {
|
||||
Message Point Media Internal Use Only
|
||||
</div>
|
||||
|
||||
</div><!-- /padding -->
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user