2026-03-07 09:51:57 -06:00
import React , { useEffect , useRef } from 'react' ;
2026-03-07 18:39:01 -06:00
// Minimal Markdown to HTML renderer (headings, bold, inline-code, tables, hr, ul, ol, paragraphs)
2026-03-07 09:51:57 -06:00
function mdToHtml ( md ) {
2026-03-07 18:33:54 -06:00
const lines = md . split ( '\n' ) ;
const out = [ ] ;
let i = 0 , inUl = false , inOl = false , inTable = false ;
const close = ( ) => {
2026-03-07 18:39:01 -06:00
if ( inUl ) { out . push ( '</ul>' ) ; inUl = false ; }
if ( inOl ) { out . push ( '</ol>' ) ; inOl = false ; }
if ( inTable ) { out . push ( '</tbody></table>' ) ; inTable = false ; }
2026-03-07 09:51:57 -06:00
} ;
2026-03-07 18:33:54 -06:00
const inline = s =>
s . replace ( /&/g , '&' ) . replace ( /</g , '<' ) . replace ( />/g , '>' )
. replace ( /\*\*(.+?)\*\*/g , '<strong>$1</strong>' )
. replace ( /`([^`]+)`/g , '<code>$1</code>' ) ;
2026-03-07 09:51:57 -06:00
while ( i < lines . length ) {
const line = lines [ i ] ;
2026-03-07 18:39:01 -06:00
if ( line . startsWith ( '```' ) ) { close ( ) ; i ++ ; while ( i < lines . length && ! lines [ i ] . startsWith ( '```' ) ) i ++ ; i ++ ; continue ; }
2026-03-07 18:33:54 -06:00
if ( /^---+$/ . test ( line . trim ( ) ) ) { close ( ) ; out . push ( '<hr>' ) ; i ++ ; continue ; }
const hm = line . match ( /^(#{1,4})\s+(.+)/ ) ;
2026-03-07 18:39:01 -06:00
if ( hm ) { close ( ) ; const lv = hm [ 1 ] . length , id = hm [ 2 ] . toLowerCase ( ) . replace ( /[^a-z0-9]+/g , '-' ) ; out . push ( ` <h ${ lv } id=" ${ id } "> ${ inline ( hm [ 2 ] ) } </h ${ lv } > ` ) ; i ++ ; continue ; }
2026-03-07 09:51:57 -06:00
if ( line . trim ( ) . startsWith ( '|' ) ) {
2026-03-07 18:33:54 -06:00
const cells = line . trim ( ) . replace ( /^\||\|$/g , '' ) . split ( '|' ) . map ( c => c . trim ( ) ) ;
2026-03-07 18:39:01 -06:00
if ( ! inTable ) { close ( ) ; inTable = true ; out . push ( '<table><thead><tr>' ) ; cells . forEach ( c => out . push ( ` <th> ${ inline ( c ) } </th> ` ) ) ; out . push ( '</tr></thead><tbody>' ) ; i ++ ; if ( i < lines . length && /^[\|\s\-:]+$/ . test ( lines [ i ] ) ) i ++ ; continue ; }
else { out . push ( '<tr>' ) ; cells . forEach ( c => out . push ( ` <td> ${ inline ( c ) } </td> ` ) ) ; out . push ( '</tr>' ) ; i ++ ; continue ; }
2026-03-07 09:51:57 -06:00
}
2026-03-07 18:33:54 -06:00
const ul = line . match ( /^[-*]\s+(.*)/ ) ;
2026-03-07 18:39:01 -06:00
if ( ul ) { if ( inTable ) close ( ) ; if ( ! inUl ) { if ( inOl ) { out . push ( '</ol>' ) ; inOl = false ; } out . push ( '<ul>' ) ; inUl = true ; } out . push ( ` <li> ${ inline ( ul [ 1 ] ) } </li> ` ) ; i ++ ; continue ; }
2026-03-07 18:33:54 -06:00
const ol = line . match ( /^\d+\.\s+(.*)/ ) ;
2026-03-07 18:39:01 -06:00
if ( ol ) { if ( inTable ) close ( ) ; if ( ! inOl ) { if ( inUl ) { out . push ( '</ul>' ) ; inUl = false ; } out . push ( '<ol>' ) ; inOl = true ; } out . push ( ` <li> ${ inline ( ol [ 1 ] ) } </li> ` ) ; i ++ ; continue ; }
2026-03-07 18:33:54 -06:00
if ( line . trim ( ) === '' ) { close ( ) ; i ++ ; continue ; }
2026-03-07 18:39:01 -06:00
close ( ) ; out . push ( ` <p> ${ inline ( line ) } </p> ` ) ; i ++ ;
2026-03-07 09:51:57 -06:00
}
2026-03-07 18:33:54 -06:00
close ( ) ;
2026-03-07 09:51:57 -06:00
return out . join ( '\n' ) ;
}
2026-03-07 18:39:01 -06:00
function buildToc ( md ) {
return md . split ( '\n' ) . reduce ( ( acc , line ) => {
const m = line . match ( /^(#{1,2})\s+(.+)/ ) ;
if ( m ) acc . push ( { level : m [ 1 ] . length , text : m [ 2 ] , id : m [ 2 ] . toLowerCase ( ) . replace ( /[^a-z0-9]+/g , '-' ) } ) ;
return acc ;
} , [ ] ) ;
}
2026-03-07 09:51:57 -06:00
// ─── Styles ───────────────────────────────────────────────────────────────────
2026-03-07 18:33:54 -06:00
const S = {
2026-03-07 18:39:01 -06:00
overlay : { position : 'fixed' , inset : 0 , background : 'rgba(0,0,0,0.75)' , zIndex : 2000 , display : 'flex' , alignItems : 'flex-start' , justifyContent : 'flex-end' } ,
panel : { background : '#111217' , color : '#f8f9fa' , width : '760px' , maxWidth : '95vw' , height : '100vh' , overflowY : 'auto' , boxShadow : '-4px 0 32px rgba(0,0,0,0.85)' , display : 'flex' , flexDirection : 'column' } ,
header : { background : 'linear-gradient(135deg,#000000,#151622)' , color : 'white' , padding : '22px 28px' , position : 'sticky' , top : 0 , zIndex : 10 , borderBottom : '1px solid #222' , display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' } ,
closeBtn : { background : 'none' , border : 'none' , color : 'white' , fontSize : '22px' , cursor : 'pointer' , lineHeight : 1 } ,
toc : { background : '#0d1117' , borderBottom : '1px solid #1e1f2e' , padding : '10px 32px' , display : 'flex' , flexWrap : 'wrap' , gap : '4px 18px' , fontSize : '11px' } ,
body : { padding : '28px 32px' , flex : 1 , fontSize : '13px' , lineHeight : '1.75' } ,
footer : { padding : '14px 32px' , borderTop : '1px solid #1e1f2e' , fontSize : '11px' , color : '#555770' , textAlign : 'center' } ,
2026-03-07 09:51:57 -06:00
} ;
2026-03-07 18:33:54 -06:00
const CSS = `
2026-03-07 18:39:01 -06:00
. adm h1 { font - size : 21 px ; font - weight : 800 ; color : # f8f9fa ; margin : 28 px 0 10 px ; border - bottom : 1 px solid # 2 a2b3a ; padding - bottom : 8 px }
. adm h2 { font - size : 16 px ; font - weight : 700 ; color : # d4af37 ; margin : 28 px 0 6 px ; letter - spacing : .2 px }
. adm h3 { font - size : 12 px ; font - weight : 700 ; color : # 90 caf9 ; margin : 18 px 0 4 px ; text - transform : uppercase ; letter - spacing : .5 px }
. adm h4 { font - size : 13 px ; font - weight : 600 ; color : # b0b8d0 ; margin : 14 px 0 4 px }
. adm p { color : # c8ccd8 ; margin : 5 px 0 10 px }
. adm hr { border : none ; border - top : 1 px solid # 2 a2b3a ; margin : 22 px 0 }
. adm strong { color : # f8f9fa }
. adm code { background : # 0 d1117 ; color : # 79 c0ff ; border : 1 px solid # 2 a2b3a ; border - radius : 4 px ; padding : 1 px 6 px ; font - family : 'Consolas' , 'Fira Code' , monospace ; font - size : 12 px }
. adm ul { padding - left : 20 px ; margin : 5 px 0 10 px ; color : # c8ccd8 }
. adm ol { padding - left : 20 px ; margin : 5 px 0 10 px ; color : # c8ccd8 }
. adm li { margin : 4 px 0 }
. adm table { width : 100 % ; border - collapse : collapse ; font - size : 12 px ; background : # 181924 ; border - radius : 6 px ; overflow : hidden ; border : 1 px solid # 2 a2b3a ; margin : 10 px 0 16 px }
. adm th { background : # 050608 ; padding : 8 px 12 px ; text - align : left ; color : # f8f9fa ; font - weight : 600 ; font - size : 11 px ; text - transform : uppercase ; border - bottom : 1 px solid # 2 a2b3a }
. adm td { padding : 8 px 12 px ; border - bottom : 1 px solid # 202231 ; color : # c8ccd8 }
. adm tr : last - child td { border - bottom : none }
. adm tr : hover td { background : # 1 e1f2e }
2026-03-07 18:33:54 -06:00
` ;
2026-03-07 09:51:57 -06:00
2026-03-07 18:39:01 -06:00
// ─── Admin guide content (no install / Docker content) ────────────────────────
2026-03-07 18:33:54 -06:00
const GUIDE _MD = ` # CPAS Tracker — Admin Guide
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
Internal tool for CPAS violation documentation , workforce standing management , and audit compliance . All data is stored locally in the Docker container volume — there is no external dependency .
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
-- -
2026-03-07 09:51:57 -06:00
2026-03-07 18:39:01 -06:00
# # How Scoring Works
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
Every violation carries a * * point value * * set at the time of submission . Points count toward an employee ' s score only within a * * rolling 90 - day window * * — once a violation is older than 90 days it automatically drops off and the score recalculates .
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
Negated ( voided ) violations are excluded from scoring immediately . Hard - deleted violations are removed from the record entirely .
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
# # Tier Reference
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
| Points | Tier | Label |
| -- -- -- -- | -- -- -- | -- -- -- - |
| 0 – 4 | 0 – 1 | Elite Standing |
| 5 – 9 | 1 | Realignment |
| 10 – 14 | 2 | Administrative Lockdown |
| 15 – 19 | 3 | Verification |
| 20 – 24 | 4 | Risk Mitigation |
| 25 – 29 | 5 | Final Decision |
| 30 + | 6 | Separation |
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
The * * at - risk badge * * on the dashboard flags anyone within 2 points of the next tier threshold so supervisors can act before escalation occurs .
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
-- -
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
# # Feature Map
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
# # # Dashboard
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
The main view . Employees are sorted by active CPAS points , highest first .
2026-03-07 09:51:57 -06:00
2026-03-07 18:39:01 -06:00
- * * Stat cards * * — live counts : total employees , zero - point ( elite ) , with active points , at - risk , highest score
2026-03-07 18:33:54 -06:00
- * * Search / filter * * — by name , department , or supervisor ; narrows the table in real time
- * * At - risk badge * * — gold flag on rows where the employee is within 2 pts of the next tier
- * * Audit Log button * * — opens the filterable , paginated write - action log ( top right of the dashboard toolbar )
- * * Click any name * * — opens that employee ' s full profile modal
2026-03-07 09:51:57 -06:00
-- -
2026-03-07 18:33:54 -06:00
# # # Logging a Violation
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
Use the * * + New Violation * * tab .
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
1. Select an existing employee from the dropdown , or type a new name to create a record on - the - fly .
2. The * * employee intelligence panel * * loads their current tier badge and 90 - day violation count before you commit .
3. Choose a violation type . The dropdown is grouped by category and shows prior 90 - day counts inline for each type .
4. If the employee has a prior violation of the same type , the * * recidivist auto - escalation * * rule triggers — the points slider jumps to the maximum allowed for that violation type .
5. The * * tier crossing warning * * previews what tier the submission would land the employee in . Review before submitting .
6. Adjust points using the slider if discretionary reduction is warranted ( within the violation ' s allowed min / max range ) .
7. Submit . A * * PDF download link * * appears immediately — download it for the employee ' s file .
-- -
2026-03-07 09:51:57 -06:00
# # # Employee Profile Modal
2026-03-07 18:33:54 -06:00
Click any name on the dashboard to open their profile .
# # # # Overview section
Shows current tier badge , active points , and 90 - day violation count .
# # # # Notes & Flags
Free - text field for HR context ( e . g . "On PIP" , "Union member" , "Pending investigation" , "FMLA" ) . Quick - add tag buttons pre - fill common statuses . Notes are visible to anyone who opens the profile but * * do not affect CPAS scoring * * . Edit inline ; saves on blur .
# # # # Point Expiration Timeline
Visible when the employee has active points . Shows each active violation as a progress bar indicating how far through its 90 - day window it is , days remaining until roll - off , and a * * tier - drop indicator * * for violations whose expiration would move the employee down a tier .
# # # # Violation History
Full record of all submissions — active , negated , and resolved .
- * * Amend * * — edit non - scoring fields ( location , details , witness , submitted - by , incident time ) on any active violation . Every change is logged as a field - level diff ( old → new ) with timestamp . Points , type , and incident date are immutable .
- * * Negate * * — soft - delete a violation with a resolution type and notes . The record is preserved in history ; the points are immediately removed from the score . Fully reversible via * * Restore * * .
- * * Hard delete * * — permanent removal . Use only for genuine data entry errors .
- * * PDF * * — download the formal violation document for any historical record .
# # # # Edit Employee
Update name , department , or supervisor . Changes are logged to the audit trail .
# # # # Merge Duplicate
If the same employee exists under two names , use Merge to reassign all violations from the duplicate to the canonical record . The duplicate is then deleted . This cannot be undone .
-- -
2026-03-07 09:51:57 -06:00
# # # Audit Log
2026-03-07 18:33:54 -06:00
Accessible from the dashboard toolbar ( 🔍 button ) . Append - only log of every write action in the system .
- Filter by entity type : * * employee * * or * * violation * *
- Filter by action : created , edited , merged , negated , restored , amended , deleted , notes updated
- Paginated with load - more ; most recent entries first
The audit log is the authoritative record for compliance review . Nothing in it can be edited or deleted through the UI .
2026-03-07 09:51:57 -06:00
-- -
2026-03-07 18:33:54 -06:00
# # # Violation Amendment
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
Amendments allow corrections to a violation ' s non - scoring fields without deleting and re - submitting , which would disrupt the audit trail and the prior - points snapshot .
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
* * Amendable fields : * * incident time , location , details , submitted - by , witness name .
* * Immutable fields : * * violation type , incident date , point value .
Each amendment stores a before / after diff for every changed field . Amendment history is accessible from the violation card in the employee ' s history .
2026-03-07 09:51:57 -06:00
-- -
2026-03-07 18:33:54 -06:00
# # Immutability Rules — Quick Reference
| Action | Allowed ? | Notes |
| -- -- -- -- | -- -- -- -- -- | -- -- -- - |
| Edit violation type | No | Immutable after submission |
| Edit incident date | No | Immutable after submission |
| Edit point value | No | Immutable after submission |
| Edit location / details / witness | Yes | Via Amend |
| Negate ( void ) a violation | Yes | Soft delete ; reversible |
| Hard delete a violation | Yes | Permanent ; use sparingly |
| Edit employee name / dept / supervisor | Yes | Logged to audit trail |
| Merge duplicate employees | Yes | Irreversible |
| Add / edit employee notes | Yes | Does not affect score |
2026-03-07 09:51:57 -06:00
-- -
2026-03-07 18:33:54 -06:00
# # Roadmap
2026-03-07 09:51:57 -06:00
2026-03-07 18:39:01 -06:00
# # # Shipped
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
- Container scaffold , violation form , employee intelligence
- Recidivist auto - escalation , tier crossing warning
- PDF generation with prior - points snapshot
- Company dashboard , stat cards , at - risk badges
- Employee profile modal — full history , negate / restore , hard delete
- Employee edit and duplicate merge
- Violation amendment with field - level diff log
- Audit log — filterable , paginated , append - only
- Employee notes and flags with quick - add HR tags
- Point expiration timeline with tier - drop projections
- In - app admin guide ( this panel )
2026-03-07 09:51:57 -06:00
-- -
2026-03-07 18:39:01 -06:00
# # # Near - term
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
These are well - scoped additions that fit the current architecture without major changes .
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
- * * Acknowledgment signature field * * — "received by employee" name + date on the violation form ; prints on the PDF in place of the blank signature line . Addresses the most common field workflow gap .
- * * CSV export * * — one endpoint returning violations or dashboard data as a downloadable CSV for payroll or external reporting .
- * * Supervisor - scoped view * * — filter the dashboard to a single supervisor ' s team via URL param ; useful in multi - supervisor environments without requiring full auth .
2026-03-07 09:51:57 -06:00
-- -
2026-03-07 18:39:01 -06:00
# # # Planned
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
Larger features that require more design work or infrastructure .
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
- * * Violation trends chart * * — line / bar chart of violations over time , filterable by department or supervisor . Useful for identifying systemic patterns vs . isolated incidents . Recharts is already available in the frontend bundle .
- * * Department heat map * * — grid showing violation density and average CPAS score per department . Helps identify team - level risk early .
- * * Draft / pending violations * * — save a violation as a draft before it ' s officially logged . Useful when incidents need supervisor review or HR sign - off before they count toward the score .
- * * At - risk threshold configuration * * — make the 2 - point at - risk warning threshold configurable per deployment rather than hardcoded .
-- -
2026-03-07 09:51:57 -06:00
2026-03-07 18:39:01 -06:00
# # # Future Considerations
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
These require meaningful infrastructure additions and should be evaluated against actual operational need before committing .
2026-03-07 09:51:57 -06:00
2026-03-07 18:33:54 -06:00
- * * Multi - user auth * * — role - based login ( admin , supervisor , read - only ) . Currently the app assumes a trusted internal network with no authentication layer .
- * * Tier escalation alerts * * — email or in - app notification when an employee crosses into Tier 2 + , automatically routed to their supervisor .
- * * Scheduled digest * * — weekly email summary to supervisors showing their employees ' current standings and any approaching thresholds .
2026-03-07 18:39:01 -06:00
- * * Automated DB backup * * — scheduled snapshot of the database to a mounted backup volume or remote destination .
2026-03-07 18:33:54 -06:00
- * * Bulk CSV import * * — migrate historical violation records from paper logs or a prior system .
- * * Dark / light theme toggle * * — UI is currently dark - only .
2026-03-07 09:51:57 -06:00
` ;
// ─── Component ────────────────────────────────────────────────────────────────
export default function ReadmeModal ( { onClose } ) {
2026-03-07 18:33:54 -06:00
const bodyRef = useRef ( null ) ;
const html = mdToHtml ( GUIDE _MD ) ;
const toc = buildToc ( GUIDE _MD ) ;
2026-03-07 09:51:57 -06:00
useEffect ( ( ) => {
2026-03-07 18:33:54 -06:00
const h = e => { if ( e . key === 'Escape' ) onClose ( ) ; } ;
window . addEventListener ( 'keydown' , h ) ;
return ( ) => window . removeEventListener ( 'keydown' , h ) ;
2026-03-07 09:51:57 -06:00
} , [ onClose ] ) ;
2026-03-07 18:33:54 -06:00
const scrollTo = id => {
2026-03-07 09:51:57 -06:00
const el = bodyRef . current ? . querySelector ( ` # ${ id } ` ) ;
if ( el ) el . scrollIntoView ( { behavior : 'smooth' , block : 'start' } ) ;
} ;
return (
2026-03-07 18:33:54 -06:00
< div style = { S . overlay } onClick = { e => { if ( e . target === e . currentTarget ) onClose ( ) ; } } >
2026-03-07 09:51:57 -06:00
< style > { CSS } < / style >
2026-03-07 18:33:54 -06:00
< div style = { S . panel } onClick = { e => e . stopPropagation ( ) } >
2026-03-07 09:51:57 -06:00
{ /* Header */ }
2026-03-07 18:33:54 -06:00
< div style = { S . header } >
2026-03-07 09:51:57 -06:00
< div >
2026-03-07 18:33:54 -06:00
< div style = { { fontSize : '17px' , fontWeight : 800 , letterSpacing : '.3px' } } >
📋 CPAS Tracker — Admin Guide
2026-03-07 09:51:57 -06:00
< / div >
2026-03-07 18:33:54 -06:00
< div style = { { fontSize : '11px' , color : '#9ca0b8' , marginTop : '3px' } } >
2026-03-07 18:39:01 -06:00
Feature map · workflows · roadmap · Esc or click outside to close
2026-03-07 09:51:57 -06:00
< / div >
< / div >
2026-03-07 18:33:54 -06:00
< button style = { S . closeBtn } onClick = { onClose } aria - label = "Close" > ✕ < / button >
2026-03-07 09:51:57 -06:00
< / div >
{ /* TOC strip */ }
2026-03-07 18:33:54 -06:00
< div style = { S . toc } >
{ toc . map ( h => (
2026-03-07 18:39:01 -06:00
< button key = { h . id } onClick = { ( ) => scrollTo ( h . id ) } style = { {
background : 'none' , border : 'none' , cursor : 'pointer' , padding : '3px 0' ,
color : h . level === 1 ? '#f8f9fa' : '#d4af37' ,
fontWeight : h . level === 1 ? 700 : 500 ,
fontSize : '11px' ,
} } >
2026-03-07 09:51:57 -06:00
{ h . level === 2 ? '↳ ' : '' } { h . text }
< / button >
) ) }
< / div >
{ /* Body */ }
< div
ref = { bodyRef }
2026-03-07 18:33:54 -06:00
style = { S . body }
className = "adm"
2026-03-07 09:51:57 -06:00
dangerouslySetInnerHTML = { { _ _html : html } }
/ >
{ /* Footer */ }
2026-03-07 18:33:54 -06:00
< div style = { S . footer } >
2026-03-07 09:51:57 -06:00
CPAS Violation Tracker · internal admin use only
< / div >
< / div >
< / div >
) ;
}