Files
helix-engage/src/components/rules/worklist-preview.tsx
saridsa2 b90740e009 feat: rules engine — priority config UI + worklist scoring
- 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>
2026-04-01 17:20:59 +05:30

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">&middot;</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>
);
};