mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 10:23:27 +00:00
- Rules engine spec v2 (priority vs automation rules distinction) - Priority Rules settings page with weight sliders, SLA config, campaign/source weights - Collapsible config sections with dynamic headers - Live worklist preview panel with client-side scoring - AI assistant panel (collapsible) with rules-engine-specific system prompt - Worklist panel: score display with SLA status dots, sort by score - Scoring library (scoring.ts) for client-side preview computation - Sidebar: Rules Engine nav item under Configuration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
119 lines
4.9 KiB
TypeScript
119 lines
4.9 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useData } from '@/providers/data-provider';
|
|
import { scoreAndRankItems } from '@/lib/scoring';
|
|
import type { PriorityConfig, ScoreResult } from '@/lib/scoring';
|
|
import { cx } from '@/utils/cx';
|
|
|
|
interface WorklistPreviewProps {
|
|
config: PriorityConfig;
|
|
}
|
|
|
|
const slaColors: Record<string, string> = {
|
|
low: 'bg-success-solid',
|
|
medium: 'bg-warning-solid',
|
|
high: 'bg-error-solid',
|
|
critical: 'bg-error-solid animate-pulse',
|
|
};
|
|
|
|
const slaTextColor: Record<string, string> = {
|
|
low: 'text-success-primary',
|
|
medium: 'text-warning-primary',
|
|
high: 'text-error-primary',
|
|
critical: 'text-error-primary',
|
|
};
|
|
|
|
const shortType: Record<string, string> = {
|
|
missed_call: 'Missed',
|
|
follow_up: 'Follow-up',
|
|
campaign_lead: 'Campaign',
|
|
attempt_2: '2nd Att.',
|
|
attempt_3: '3rd Att.',
|
|
};
|
|
|
|
export const WorklistPreview = ({ config }: WorklistPreviewProps) => {
|
|
const { calls, leads, followUps } = useData();
|
|
|
|
const previewItems = useMemo(() => {
|
|
const items: any[] = [];
|
|
|
|
if (calls) {
|
|
calls
|
|
.filter((c: any) => c.callStatus === 'MISSED')
|
|
.slice(0, 5)
|
|
.forEach((c: any) => items.push({ ...c, type: 'missed', _label: c.callerNumber?.primaryPhoneNumber ?? c.name ?? 'Unknown' }));
|
|
}
|
|
|
|
if (followUps) {
|
|
followUps
|
|
.slice(0, 5)
|
|
.forEach((f: any) => items.push({ ...f, type: 'follow-up', _label: f.name ?? 'Follow-up' }));
|
|
}
|
|
|
|
if (leads) {
|
|
leads
|
|
.filter((l: any) => l.campaignId)
|
|
.slice(0, 5)
|
|
.forEach((l: any) => items.push({
|
|
...l,
|
|
type: 'lead',
|
|
_label: l.contactName ? `${l.contactName.firstName ?? ''} ${l.contactName.lastName ?? ''}`.trim() : l.contactPhone?.primaryPhoneNumber ?? 'Unknown',
|
|
}));
|
|
}
|
|
|
|
return items;
|
|
}, [calls, leads, followUps]);
|
|
|
|
const scored = useMemo(() => scoreAndRankItems(previewItems, config), [previewItems, config]);
|
|
|
|
return (
|
|
<div className="flex flex-col min-h-0">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h3 className="text-sm font-semibold text-primary">Live Preview</h3>
|
|
<span className="text-xs text-tertiary">{scored.length} items</span>
|
|
</div>
|
|
<div className="rounded-xl border border-secondary overflow-hidden bg-primary">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-2 px-3 py-2 bg-secondary text-xs font-medium text-tertiary border-b border-secondary">
|
|
<span className="w-3" />
|
|
<span className="flex-1 min-w-0">Name</span>
|
|
<span className="w-16 text-right">Score</span>
|
|
</div>
|
|
{/* Rows */}
|
|
<div className="divide-y divide-tertiary overflow-y-auto max-h-[320px]">
|
|
{scored.map((item: any & ScoreResult, index: number) => (
|
|
<div
|
|
key={item.id ?? index}
|
|
className="flex items-center gap-2 px-3 py-2 hover:bg-primary_hover transition duration-100 ease-linear"
|
|
>
|
|
<span className={cx('size-2 rounded-full shrink-0', slaColors[item.slaStatus])} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-xs font-medium text-primary truncate">
|
|
{item._label ?? item.name ?? 'Item'}
|
|
</div>
|
|
<div className="flex items-center gap-1.5 mt-0.5">
|
|
<span className="text-[10px] text-tertiary">
|
|
{shortType[item.taskType] ?? item.taskType}
|
|
</span>
|
|
<span className="text-quaternary">·</span>
|
|
<span className={cx('text-[10px] font-medium', slaTextColor[item.slaStatus])}>
|
|
{item.slaElapsedPercent}% SLA
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<span className="w-16 text-right text-sm font-bold tabular-nums text-primary shrink-0">
|
|
{item.score.toFixed(1)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
{scored.length === 0 && (
|
|
<div className="px-3 py-6 text-center text-xs text-tertiary">
|
|
No worklist items to preview
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|