This commit is contained in:
@@ -62,73 +62,89 @@ const s = {
|
||||
};
|
||||
|
||||
export default function ExpirationTimeline({ employeeId, currentPoints }) {
|
||||
const [items, setItems] = useState([]);
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
axios.get(`/api/employees/${employeeId}/expiration`)
|
||||
.then(r => setItems(r.data))
|
||||
.then(r => setData(r.data))
|
||||
.finally(() => setLoading(false));
|
||||
}, [employeeId]);
|
||||
|
||||
if (loading) return (
|
||||
<div style={s.wrapper}>
|
||||
<div style={s.sectionHd}>Point Expiration Timeline</div>
|
||||
<div style={s.sectionHd}>Point Roll-Off Timeline</div>
|
||||
<div style={{ ...s.empty }}>Loading…</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (items.length === 0) return (
|
||||
const schedule = data?.schedule || [];
|
||||
|
||||
if (schedule.length === 0) return (
|
||||
<div style={s.wrapper}>
|
||||
<div style={s.sectionHd}>Point Expiration Timeline</div>
|
||||
<div style={s.empty}>No active violations — nothing to expire.</div>
|
||||
<div style={s.sectionHd}>Point Roll-Off Timeline</div>
|
||||
<div style={s.empty}>No active points — nothing to roll off.</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Build running totals: after each violation expires, what's the remaining score?
|
||||
let running = currentPoints || 0;
|
||||
const projected = items.map(item => {
|
||||
const before = running;
|
||||
running = Math.max(0, running - item.points);
|
||||
// Points roll off 5 at a time (oldest first) after each 90 consecutive clean
|
||||
// days. Walk the schedule to project the score and tier after each event.
|
||||
let running = data.active_points ?? currentPoints ?? 0;
|
||||
const projected = schedule.map(ev => {
|
||||
const before = running;
|
||||
running = ev.points_after;
|
||||
const tierBefore = getTier(before);
|
||||
const tierAfter = getTier(running);
|
||||
const dropped = tierAfter.min < tierBefore.min;
|
||||
return { ...item, pointsBefore: before, pointsAfter: running, tierBefore, tierAfter, tierDropped: dropped };
|
||||
return {
|
||||
...ev,
|
||||
pointsBefore: before,
|
||||
tierBefore,
|
||||
tierAfter,
|
||||
tierDropped: tierAfter.min < tierBefore.min,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={s.wrapper}>
|
||||
<div style={s.sectionHd}>Point Expiration Timeline</div>
|
||||
<div style={s.sectionHd}>Point Roll-Off Timeline</div>
|
||||
|
||||
{projected.map((item) => {
|
||||
const color = urgencyColor(item.days_remaining);
|
||||
const pct = (item.days_remaining / 90) * 100;
|
||||
<div style={{ ...s.empty, fontStyle: 'normal', marginBottom: '10px', color: '#9ca0b8' }}>
|
||||
5 points roll off after every 90 consecutive days with no new violation.
|
||||
Any new violation resets the countdown.
|
||||
{data.last_violation_date && (
|
||||
<> Clean since <strong style={{ color: '#f8f9fa' }}>{data.last_violation_date}</strong>.</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{projected.map((ev, i) => {
|
||||
const color = urgencyColor(ev.days_remaining);
|
||||
const pct = (ev.days_remaining / 90) * 100;
|
||||
return (
|
||||
<div key={item.id} style={s.row}>
|
||||
{/* Violation name */}
|
||||
<div style={s.name} title={item.violation_name}>{item.violation_name}</div>
|
||||
<div key={ev.date} style={s.row}>
|
||||
{/* Roll-off event label */}
|
||||
<div style={s.name} title={`Roll-off #${i + 1}`}>Roll-off #{i + 1}</div>
|
||||
|
||||
{/* Points badge */}
|
||||
<div style={s.pts}>−{item.points}</div>
|
||||
{/* Points retired */}
|
||||
<div style={s.pts}>−{ev.points_off}</div>
|
||||
|
||||
{/* Progress bar: how much of the 90 days has elapsed */}
|
||||
{/* Progress bar: how much of the 90-day cycle has elapsed */}
|
||||
<div style={s.bar(pct, color)}>
|
||||
<div style={s.barFill(pct, color)} />
|
||||
</div>
|
||||
|
||||
{/* Days remaining pill */}
|
||||
<div style={s.pill(color)}>
|
||||
{item.days_remaining <= 0 ? 'Expiring today' : `${item.days_remaining}d`}
|
||||
{ev.days_remaining <= 0 ? 'Today' : `${ev.days_remaining}d`}
|
||||
</div>
|
||||
|
||||
{/* Expiry date */}
|
||||
<div style={s.date}>{item.expires_on}</div>
|
||||
{/* Roll-off date */}
|
||||
<div style={s.date}>{ev.date}</div>
|
||||
|
||||
{/* Tier drop indicator */}
|
||||
{item.tierDropped && (
|
||||
{ev.tierDropped && (
|
||||
<div style={{ fontSize: '10px', color: '#69f0ae', whiteSpace: 'nowrap' }}>
|
||||
↓ {item.tierAfter.label}
|
||||
↓ {ev.tierAfter.label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -138,16 +154,16 @@ export default function ExpirationTimeline({ employeeId, currentPoints }) {
|
||||
{/* Projection summary */}
|
||||
<div style={s.projBox}>
|
||||
<div style={{ fontWeight: 700, color: '#f8f9fa', marginBottom: '8px', fontSize: '12px' }}>
|
||||
Projected score after each expiration
|
||||
Projected score after each roll-off
|
||||
</div>
|
||||
{projected.map((item, i) => (
|
||||
<div key={item.id} style={s.projRow}>
|
||||
<span style={{ color: '#9ca0b8' }}>{item.expires_on} — {item.violation_name}</span>
|
||||
{projected.map((ev) => (
|
||||
<div key={ev.date} style={s.projRow}>
|
||||
<span style={{ color: '#9ca0b8' }}>{ev.date} — −{ev.points_off} pts</span>
|
||||
<span>
|
||||
<span style={{ color: '#f8f9fa', fontWeight: 700 }}>{item.pointsAfter} pts</span>
|
||||
{item.tierDropped && (
|
||||
<span style={{ marginLeft: '8px', color: item.tierAfter.color, fontWeight: 700 }}>
|
||||
→ {item.tierAfter.label}
|
||||
<span style={{ color: '#f8f9fa', fontWeight: 700 }}>{ev.points_after} pts</span>
|
||||
{ev.tierDropped && (
|
||||
<span style={{ marginLeft: '8px', color: ev.tierAfter.color, fontWeight: 700 }}>
|
||||
→ {ev.tierAfter.label}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -215,7 +215,7 @@ export default function ViolationForm() {
|
||||
<span style={{ fontSize: '13px', color: '#d1d3e0', fontWeight: 600 }}>Current Standing:</span>
|
||||
<CpasBadge points={intel.score.active_points} />
|
||||
<span style={{ fontSize: '12px', color: '#9ca0b8' }}>
|
||||
{intel.score.violation_count} violation{intel.score.violation_count !== 1 ? 's' : ''} in last 90 days
|
||||
{intel.score.violation_count} active violation{intel.score.violation_count !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user