mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
fix: UI polish — nav labels, date picker, rules engine, error messages
- Sidebar: removed "Master" from nav labels (Leads, Patients, Appointments, Call Log) - Appointment form: Dept + Doctor in 2-col row, Date below, disabled cascade - DatePicker: placement="bottom start" + shouldFlip fixes popover positioning - Team Performance: default to "Week", grid KPI cards, chart legend spacing - Rules Engine: manual save (removed auto-debounce), Reset to Defaults uses DEFAULT_PRIORITY_CONFIG (no template endpoint), removed dead saveTimerRef - Automation rules: 6 showcase cards with trigger/condition/action, replaced agent-specific rule with generic round-robin - Recording analysis: friendly error message with retry instead of raw Deepgram error - Sidebar active/hover: brand color reference for theming Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -174,10 +174,6 @@ export const BrandingSettingsPage = () => {
|
||||
setForm(prev => ({ ...prev, brand: { ...prev.brand, [key]: value } }));
|
||||
};
|
||||
|
||||
const updateColor = (stop: string, value: string) => {
|
||||
setForm(prev => ({ ...prev, colors: { ...prev.colors, brand: { ...prev.colors.brand, [stop]: value } } }));
|
||||
};
|
||||
|
||||
const updateTypography = (key: keyof ThemeTokens['typography'], value: string) => {
|
||||
setForm(prev => ({ ...prev, typography: { ...prev.typography, [key]: value } }));
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { PriorityConfigPanel } from '@/components/rules/priority-config-panel';
|
||||
@@ -17,7 +17,7 @@ export const RulesSettingsPage = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
@@ -60,28 +60,13 @@ export const RulesSettingsPage = () => {
|
||||
const handleConfigChange = (newConfig: PriorityConfig) => {
|
||||
setConfig(newConfig);
|
||||
setDirty(true);
|
||||
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
||||
saveTimerRef.current = setTimeout(() => saveConfig(newConfig), 1000);
|
||||
};
|
||||
|
||||
const applyTemplate = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/rules/templates/hospital-starter/apply`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
const configRes = await fetch(`${API_BASE}/api/rules/priority-config`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (configRes.ok) {
|
||||
setConfig(await configRes.json());
|
||||
setDirty(false);
|
||||
}
|
||||
}
|
||||
await saveConfig(DEFAULT_PRIORITY_CONFIG);
|
||||
setConfig(DEFAULT_PRIORITY_CONFIG);
|
||||
setDirty(false);
|
||||
} catch {
|
||||
// Silent fail
|
||||
}
|
||||
@@ -106,8 +91,13 @@ export const RulesSettingsPage = () => {
|
||||
<div className="flex items-center gap-3">
|
||||
{dirty && <span className="text-xs text-warning-primary">{saving ? 'Saving...' : 'Unsaved changes'}</span>}
|
||||
<Button size="sm" color="secondary" onClick={applyTemplate}>
|
||||
Apply Starter Template
|
||||
Reset to Defaults
|
||||
</Button>
|
||||
{dirty && (
|
||||
<Button size="sm" color="primary" isLoading={saving} onClick={() => saveConfig(config)}>
|
||||
Apply Changes
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -143,14 +133,63 @@ export const RulesSettingsPage = () => {
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="automations" className="flex flex-1 min-h-0">
|
||||
<div className="flex items-center justify-center flex-1 p-12">
|
||||
<div className="text-center">
|
||||
<h3 className="text-md font-semibold text-primary mb-2">Automation Rules</h3>
|
||||
<p className="text-sm text-tertiary max-w-md">
|
||||
Configure rules that automatically assign leads, escalate SLA breaches, and manage lead lifecycle.
|
||||
This feature is coming soon.
|
||||
</p>
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-primary">Automation Rules</h3>
|
||||
<p className="text-xs text-tertiary">Rules that trigger actions when conditions are met — assign leads, escalate breaches, update status.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{[
|
||||
{ name: 'SLA Breach → Supervisor Alert', description: 'Alert supervisor when a missed call callback exceeds 12-hour SLA', trigger: 'Every 5 minutes', condition: 'SLA > 100% AND status = PENDING_CALLBACK', action: 'Notify supervisor via bell + toast', category: 'escalation', enabled: true },
|
||||
{ name: 'Cold Lead after 3 Attempts', description: 'Mark lead as COLD when 3 contact attempts fail', trigger: 'On call ended', condition: 'Contact attempts ≥ 3 AND disposition ≠ APPOINTMENT_BOOKED', action: 'Update lead status → COLD', category: 'lifecycle', enabled: true },
|
||||
{ name: 'Round-robin Lead Assignment', description: 'Distribute new campaign leads evenly across available agents', trigger: 'On lead created', condition: 'assignedAgent is empty AND agent status = READY', action: 'Assign to least-loaded ready agent', category: 'assignment', enabled: false },
|
||||
{ name: 'Follow-up Reminder at 80% SLA', description: 'Push notification when a follow-up approaches its SLA deadline', trigger: 'Every 5 minutes', condition: 'SLA elapsed ≥ 80% AND status = PENDING', action: 'Notify assigned agent via bell', category: 'escalation', enabled: true },
|
||||
{ name: 'Spam Lead Auto-close', description: 'Automatically close leads with spam score above 80', trigger: 'On lead updated', condition: 'Spam score > 80', action: 'Update lead status → SPAM_CLOSED', category: 'lifecycle', enabled: false },
|
||||
{ name: 'VIP Patient Escalation', description: 'Escalate to supervisor when a returning patient calls and waits over 5 minutes', trigger: 'Every 1 minute', condition: 'Patient type = RETURNING AND wait time > 5 min', action: 'Notify supervisor + assign to next available agent', category: 'escalation', enabled: false },
|
||||
].map((rule, i) => {
|
||||
const categoryColors: Record<string, string> = {
|
||||
escalation: 'bg-error-secondary text-error-primary',
|
||||
lifecycle: 'bg-warning-secondary text-warning-primary',
|
||||
assignment: 'bg-brand-secondary text-brand-secondary',
|
||||
};
|
||||
return (
|
||||
<div key={i} className="rounded-xl border border-secondary bg-primary p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-sm font-semibold text-primary">{rule.name}</span>
|
||||
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${categoryColors[rule.category] ?? 'bg-secondary text-tertiary'}`}>
|
||||
{rule.category}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-tertiary mb-3">{rule.description}</p>
|
||||
<div className="grid grid-cols-3 gap-3 text-xs">
|
||||
<div>
|
||||
<span className="font-semibold text-quaternary uppercase tracking-wider text-[10px]">Trigger</span>
|
||||
<p className="text-secondary mt-0.5">{rule.trigger}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-quaternary uppercase tracking-wider text-[10px]">Condition</span>
|
||||
<p className="text-secondary mt-0.5">{rule.condition}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-quaternary uppercase tracking-wider text-[10px]">Action</span>
|
||||
<p className="text-secondary mt-0.5">{rule.action}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 ml-4 flex flex-col items-center gap-1">
|
||||
<div className={`size-3 rounded-full ${rule.enabled ? 'bg-success-solid' : 'bg-quaternary'}`} />
|
||||
<span className="text-[10px] text-tertiary">{rule.enabled ? 'On' : 'Off'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<p className="text-xs text-tertiary text-center pt-2">Rule editing and creation will be available in a future update.</p>
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
@@ -76,7 +76,7 @@ const KpiCard = ({ icon, value, label, color }: { icon: any; value: string | num
|
||||
);
|
||||
|
||||
export const TeamPerformancePage = () => {
|
||||
const [range, setRange] = useState<DateRange>('today');
|
||||
const [range, setRange] = useState<DateRange>('week');
|
||||
const [agents, setAgents] = useState<AgentPerf[]>([]);
|
||||
const [allCalls, setAllCalls] = useState<any[]>([]);
|
||||
const [allAppointments, setAllAppointments] = useState<any[]>([]);
|
||||
|
||||
Reference in New Issue
Block a user