mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 10:23:27 +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:
@@ -40,7 +40,8 @@ export const DatePicker = ({ value: valueProp, defaultValue, onChange, onApply,
|
||||
</AriaGroup>
|
||||
<AriaPopover
|
||||
offset={8}
|
||||
placement="bottom right"
|
||||
placement="bottom start"
|
||||
shouldFlip
|
||||
className={({ isEntering, isExiting }) =>
|
||||
cx(
|
||||
"origin-(--trigger-anchor-point) will-change-transform",
|
||||
|
||||
@@ -386,29 +386,29 @@ export const AppointmentForm = ({
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Select
|
||||
label="Department / Specialty"
|
||||
label="Department *"
|
||||
placeholder={doctors.length === 0 ? 'Loading...' : 'Select department'}
|
||||
items={departmentItems}
|
||||
selectedKey={department}
|
||||
onSelectionChange={(key) => setDepartment(key as string)}
|
||||
isRequired
|
||||
isDisabled={doctors.length === 0}
|
||||
>
|
||||
{(item) => <Select.Item id={item.id} label={item.label} />}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
label="Doctor"
|
||||
label="Doctor *"
|
||||
placeholder={!department ? 'Select department first' : 'Select doctor'}
|
||||
items={doctorSelectItems}
|
||||
selectedKey={doctor}
|
||||
onSelectionChange={(key) => setDoctor(key as string)}
|
||||
isRequired
|
||||
isDisabled={!department}
|
||||
>
|
||||
{(item) => <Select.Item id={item.id} label={item.label} />}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs font-medium text-secondary">Date <span className="text-error-primary">*</span></span>
|
||||
@@ -416,6 +416,7 @@ export const AppointmentForm = ({
|
||||
value={date ? parseDate(date) : null}
|
||||
onChange={(val) => setDate(val ? val.toString() : '')}
|
||||
granularity="day"
|
||||
isDisabled={!doctor}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ export const RecordingAnalysisSlideout = ({
|
||||
|
||||
{error && !loading && (
|
||||
<div className="flex flex-col items-center gap-3 py-12">
|
||||
<p className="text-sm text-error-primary">{error}</p>
|
||||
<p className="text-sm text-tertiary">Transcription is temporarily unavailable. Please try again.</p>
|
||||
<Button
|
||||
size="sm"
|
||||
color="secondary"
|
||||
|
||||
@@ -69,10 +69,10 @@ const getNavSections = (role: string): NavSection[] => {
|
||||
{ label: 'Live Call Monitor', href: '/live-monitor', icon: IconTowerBroadcast },
|
||||
]},
|
||||
{ label: 'Data & Reports', items: [
|
||||
{ label: 'Lead Master', href: '/leads', icon: IconUsers },
|
||||
{ label: 'Patient Master', href: '/patients', icon: IconHospitalUser },
|
||||
{ label: 'Appointment Master', href: '/appointments', icon: IconCalendarCheck },
|
||||
{ label: 'Call Log Master', href: '/call-history', icon: IconClockRewind },
|
||||
{ label: 'Leads', href: '/leads', icon: IconUsers },
|
||||
{ label: 'Patients', href: '/patients', icon: IconHospitalUser },
|
||||
{ label: 'Appointments', href: '/appointments', icon: IconCalendarCheck },
|
||||
{ label: 'Call Log', href: '/call-history', icon: IconClockRewind },
|
||||
{ label: 'Call Recordings', href: '/call-recordings', icon: IconFileAudio },
|
||||
{ label: 'Missed Calls', href: '/missed-calls', icon: IconPhoneMissed },
|
||||
]},
|
||||
|
||||
@@ -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());
|
||||
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,15 +133,64 @@ 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>
|
||||
</div>
|
||||
|
||||
@@ -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