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>
93 lines
3.5 KiB
TypeScript
93 lines
3.5 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
|
import { Bot, Clock, RefreshCw } from 'lucide-react'
|
|
import { getLatestInsight, generateInsight } from '../api'
|
|
import { formatAge, renderContent } from '../lib/aiInsight'
|
|
import type { AIInsight } from '../types'
|
|
|
|
const REFRESH_MS = 5 * 60_000
|
|
|
|
export default function Dashboard() {
|
|
const [insight, setInsight] = useState<AIInsight | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [generating, setGenerating] = useState(false)
|
|
const [genError, setGenError] = useState<string | null>(null)
|
|
|
|
const load = useCallback(() => {
|
|
getLatestInsight()
|
|
.then(setInsight)
|
|
.catch(e => setError(e instanceof Error ? e.message : 'Failed to load insight'))
|
|
.finally(() => setLoading(false))
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
load()
|
|
const id = setInterval(load, REFRESH_MS)
|
|
return () => clearInterval(id)
|
|
}, [load])
|
|
|
|
const handleGenerate = async () => {
|
|
setGenerating(true)
|
|
setGenError(null)
|
|
try {
|
|
await generateInsight()
|
|
load()
|
|
} catch (e) {
|
|
setGenError(e instanceof Error ? e.message : 'Failed to generate insight')
|
|
} finally {
|
|
setGenerating(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="page-header">
|
|
<h1 className="page-title">Dashboard</h1>
|
|
<button className="btn btn-primary" onClick={handleGenerate} disabled={generating}>
|
|
<RefreshCw size={14} strokeWidth={1.75} />
|
|
{generating ? 'Generating…' : 'Generate Now'}
|
|
</button>
|
|
</div>
|
|
|
|
{genError && <div className="state-center" style={{ color: '#dc2626', marginBottom: 16 }}>{genError}</div>}
|
|
|
|
<div className="card">
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
|
<div className="card-title" style={{ display: 'flex', alignItems: 'center', gap: 8, margin: 0 }}>
|
|
<Bot size={16} strokeWidth={1.75} color="var(--gold)" />
|
|
AI Insight
|
|
</div>
|
|
{insight && (
|
|
<span style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 12, color: 'var(--text-muted)' }}>
|
|
<Clock size={12} strokeWidth={1.75} />
|
|
{formatAge(insight.generated_at)}
|
|
{insight.model && (
|
|
<span style={{ marginLeft: 6, background: 'var(--body-bg)', borderRadius: 4, padding: '1px 6px' }}>
|
|
{insight.model}
|
|
</span>
|
|
)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{loading && <div className="state-center">Loading…</div>}
|
|
{!loading && error && <div className="state-center" style={{ color: '#dc2626' }}>{error}</div>}
|
|
{!loading && !error && !insight && (
|
|
<div className="state-center">
|
|
No insight generated yet. Click <strong>Generate Now</strong> to produce a summary.
|
|
</div>
|
|
)}
|
|
{!loading && !error && insight && (
|
|
<div style={{ fontSize: 13.5, lineHeight: 1.65, color: 'var(--text-primary)' }}>
|
|
<div style={{ marginBottom: 16 }}>{renderContent(insight.content)}</div>
|
|
{(insight.input_tokens || insight.output_tokens) && (
|
|
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 16, paddingTop: 12, borderTop: '1px solid var(--border)' }}>
|
|
{insight.input_tokens}↑ / {insight.output_tokens}↓ tokens · {insight.triggered_by}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|