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:
2026-04-02 16:55:16 +05:30
parent afd0829dc6
commit 8470dd03c7
7 changed files with 98 additions and 61 deletions

View File

@@ -40,7 +40,8 @@ export const DatePicker = ({ value: valueProp, defaultValue, onChange, onApply,
</AriaGroup> </AriaGroup>
<AriaPopover <AriaPopover
offset={8} offset={8}
placement="bottom right" placement="bottom start"
shouldFlip
className={({ isEntering, isExiting }) => className={({ isEntering, isExiting }) =>
cx( cx(
"origin-(--trigger-anchor-point) will-change-transform", "origin-(--trigger-anchor-point) will-change-transform",

View File

@@ -386,29 +386,29 @@ export const AppointmentForm = ({
</Select> </Select>
)} )}
<div className="grid grid-cols-2 gap-3">
<Select <Select
label="Department / Specialty" label="Department *"
placeholder={doctors.length === 0 ? 'Loading...' : 'Select department'} placeholder={doctors.length === 0 ? 'Loading...' : 'Select department'}
items={departmentItems} items={departmentItems}
selectedKey={department} selectedKey={department}
onSelectionChange={(key) => setDepartment(key as string)} onSelectionChange={(key) => setDepartment(key as string)}
isRequired
isDisabled={doctors.length === 0} isDisabled={doctors.length === 0}
> >
{(item) => <Select.Item id={item.id} label={item.label} />} {(item) => <Select.Item id={item.id} label={item.label} />}
</Select> </Select>
<Select <Select
label="Doctor" label="Doctor *"
placeholder={!department ? 'Select department first' : 'Select doctor'} placeholder={!department ? 'Select department first' : 'Select doctor'}
items={doctorSelectItems} items={doctorSelectItems}
selectedKey={doctor} selectedKey={doctor}
onSelectionChange={(key) => setDoctor(key as string)} onSelectionChange={(key) => setDoctor(key as string)}
isRequired
isDisabled={!department} isDisabled={!department}
> >
{(item) => <Select.Item id={item.id} label={item.label} />} {(item) => <Select.Item id={item.id} label={item.label} />}
</Select> </Select>
</div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="text-xs font-medium text-secondary">Date <span className="text-error-primary">*</span></span> <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} value={date ? parseDate(date) : null}
onChange={(val) => setDate(val ? val.toString() : '')} onChange={(val) => setDate(val ? val.toString() : '')}
granularity="day" granularity="day"
isDisabled={!doctor}
/> />
</div> </div>

View File

@@ -178,7 +178,7 @@ export const RecordingAnalysisSlideout = ({
{error && !loading && ( {error && !loading && (
<div className="flex flex-col items-center gap-3 py-12"> <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 <Button
size="sm" size="sm"
color="secondary" color="secondary"

View File

@@ -69,10 +69,10 @@ const getNavSections = (role: string): NavSection[] => {
{ label: 'Live Call Monitor', href: '/live-monitor', icon: IconTowerBroadcast }, { label: 'Live Call Monitor', href: '/live-monitor', icon: IconTowerBroadcast },
]}, ]},
{ label: 'Data & Reports', items: [ { label: 'Data & Reports', items: [
{ label: 'Lead Master', href: '/leads', icon: IconUsers }, { label: 'Leads', href: '/leads', icon: IconUsers },
{ label: 'Patient Master', href: '/patients', icon: IconHospitalUser }, { label: 'Patients', href: '/patients', icon: IconHospitalUser },
{ label: 'Appointment Master', href: '/appointments', icon: IconCalendarCheck }, { label: 'Appointments', href: '/appointments', icon: IconCalendarCheck },
{ label: 'Call Log Master', href: '/call-history', icon: IconClockRewind }, { label: 'Call Log', href: '/call-history', icon: IconClockRewind },
{ label: 'Call Recordings', href: '/call-recordings', icon: IconFileAudio }, { label: 'Call Recordings', href: '/call-recordings', icon: IconFileAudio },
{ label: 'Missed Calls', href: '/missed-calls', icon: IconPhoneMissed }, { label: 'Missed Calls', href: '/missed-calls', icon: IconPhoneMissed },
]}, ]},

View File

@@ -174,10 +174,6 @@ export const BrandingSettingsPage = () => {
setForm(prev => ({ ...prev, brand: { ...prev.brand, [key]: value } })); 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) => { const updateTypography = (key: keyof ThemeTokens['typography'], value: string) => {
setForm(prev => ({ ...prev, typography: { ...prev.typography, [key]: value } })); setForm(prev => ({ ...prev, typography: { ...prev.typography, [key]: value } }));
}; };

View File

@@ -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 { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
import { Button } from '@/components/base/buttons/button'; import { Button } from '@/components/base/buttons/button';
import { PriorityConfigPanel } from '@/components/rules/priority-config-panel'; import { PriorityConfigPanel } from '@/components/rules/priority-config-panel';
@@ -17,7 +17,7 @@ export const RulesSettingsPage = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [dirty, setDirty] = useState(false); const [dirty, setDirty] = useState(false);
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
@@ -60,28 +60,13 @@ export const RulesSettingsPage = () => {
const handleConfigChange = (newConfig: PriorityConfig) => { const handleConfigChange = (newConfig: PriorityConfig) => {
setConfig(newConfig); setConfig(newConfig);
setDirty(true); setDirty(true);
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
saveTimerRef.current = setTimeout(() => saveConfig(newConfig), 1000);
}; };
const applyTemplate = async () => { const applyTemplate = async () => {
try { try {
const res = await fetch(`${API_BASE}/api/rules/templates/hospital-starter/apply`, { await saveConfig(DEFAULT_PRIORITY_CONFIG);
method: 'POST', setConfig(DEFAULT_PRIORITY_CONFIG);
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); setDirty(false);
}
}
} catch { } catch {
// Silent fail // Silent fail
} }
@@ -106,8 +91,13 @@ export const RulesSettingsPage = () => {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{dirty && <span className="text-xs text-warning-primary">{saving ? 'Saving...' : 'Unsaved changes'}</span>} {dirty && <span className="text-xs text-warning-primary">{saving ? 'Saving...' : 'Unsaved changes'}</span>}
<Button size="sm" color="secondary" onClick={applyTemplate}> <Button size="sm" color="secondary" onClick={applyTemplate}>
Apply Starter Template Reset to Defaults
</Button> </Button>
{dirty && (
<Button size="sm" color="primary" isLoading={saving} onClick={() => saveConfig(config)}>
Apply Changes
</Button>
)}
</div> </div>
</div> </div>
@@ -143,15 +133,64 @@ export const RulesSettingsPage = () => {
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel id="automations" className="flex flex-1 min-h-0"> <Tabs.Panel id="automations" className="flex flex-1 min-h-0">
<div className="flex items-center justify-center flex-1 p-12"> <div className="flex-1 overflow-y-auto p-6 space-y-4">
<div className="text-center"> <div className="flex items-center justify-between mb-2">
<h3 className="text-md font-semibold text-primary mb-2">Automation Rules</h3> <div>
<p className="text-sm text-tertiary max-w-md"> <h3 className="text-sm font-semibold text-primary">Automation Rules</h3>
Configure rules that automatically assign leads, escalate SLA breaches, and manage lead lifecycle. <p className="text-xs text-tertiary">Rules that trigger actions when conditions are met assign leads, escalate breaches, update status.</p>
This feature is coming soon.
</p>
</div> </div>
</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.Panel>
</Tabs> </Tabs>
</div> </div>

View File

@@ -76,7 +76,7 @@ const KpiCard = ({ icon, value, label, color }: { icon: any; value: string | num
); );
export const TeamPerformancePage = () => { export const TeamPerformancePage = () => {
const [range, setRange] = useState<DateRange>('today'); const [range, setRange] = useState<DateRange>('week');
const [agents, setAgents] = useState<AgentPerf[]>([]); const [agents, setAgents] = useState<AgentPerf[]>([]);
const [allCalls, setAllCalls] = useState<any[]>([]); const [allCalls, setAllCalls] = useState<any[]>([]);
const [allAppointments, setAllAppointments] = useState<any[]>([]); const [allAppointments, setAllAppointments] = useState<any[]>([]);