AI Insights: a daily Claude-generated wage cost briefing covering month-to-date pace vs budget, prior-month/prior-year comparison, rota-vs-actual variance by department, a rota-informed forecast to month-end, employee-level anomalies, and wage cost as a % of revenue. Runs on a configurable daily schedule or on demand, gated by a manual 5-minute rate limit and a daily token budget. Uses the Anthropic key configured centrally in Portal → Settings → Integrations. Also includes the rota-vs-repeat-pattern forecast method (published/ draft rota tiers with same-weekday fallback) already built into the Weekly/Monthly views, and adds a .gitignore for node_modules/dist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
37 lines
1.4 KiB
TypeScript
37 lines
1.4 KiB
TypeScript
export function formatAge(iso: string): string {
|
|
const ms = Date.now() - new Date(iso).getTime()
|
|
const mins = Math.floor(ms / 60000)
|
|
if (mins < 1) return 'just now'
|
|
if (mins < 60) return `${mins}m ago`
|
|
const hours = Math.floor(mins / 60)
|
|
if (hours < 24) return `${hours}h ago`
|
|
return `${Math.floor(hours / 24)}d ago`
|
|
}
|
|
|
|
function escHtml(s: string): string {
|
|
return s
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
}
|
|
|
|
export function renderContent(text: string) {
|
|
return text.split('\n').map((line, i) => {
|
|
const safe = escHtml(line)
|
|
const processed = safe.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
if (line.startsWith('- ') || line.startsWith('* ')) {
|
|
return (
|
|
<div key={i} style={{ display: 'flex', gap: 8, marginBottom: 4 }}>
|
|
<span style={{ color: 'var(--gold)', flexShrink: 0 }}>•</span>
|
|
<span dangerouslySetInnerHTML={{ __html: processed.slice(2) }} />
|
|
</div>
|
|
)
|
|
}
|
|
if (line.startsWith('## ') || line.startsWith('# ')) {
|
|
const txt = line.replace(/^#+\s*/, '')
|
|
return <p key={i} style={{ fontWeight: 600, marginTop: 12, marginBottom: 6, color: 'var(--text-primary)' }}>{txt}</p>
|
|
}
|
|
if (line.trim() === '') return <div key={i} style={{ height: 8 }} />
|
|
return <p key={i} style={{ marginBottom: 4 }} dangerouslySetInnerHTML={{ __html: processed }} />
|
|
})
|
|
}
|