mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: SSE agent state, UCID fix, maint module, QA bug fixes
- Fix outbound disposition: store UCID from dial API response (root cause of silent disposition failure) - SSE agent state: real-time Ozonetel state drives status toggle (ready/break/calling/in-call/acw) - Maint module with OTP-protected endpoints (force-ready, unlock-agent, backfill, fix-timestamps) - Maint OTP modal with PinInput component, keyboard shortcuts (Ctrl+Shift+R/U/B/T) - Force-logout via SSE: admin unlock pushes force-logout to connected browsers - Silence JsSIP debug flood, add structured lifecycle logging ([SIP], [DIAL], [DISPOSE], [AGENT-STATE]) - Centralize date formatting with IST-aware formatters across 11 files - Fix call history: non-overlapping aggregates (completed/missed), correct timestamp display - Auto-dismiss CallWidget ended/failed state after 3 seconds - Remove floating "Helix Phone" idle badge from all pages - Fix dead code in agent-state endpoint (auto-assign was unreachable after return) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import { Table } from '@/components/application/table/table';
|
||||
import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
|
||||
import { formatPhone } from '@/lib/format';
|
||||
import { formatPhone, formatDateOnly, formatTimeOnly } from '@/lib/format';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
|
||||
type AppointmentRecord = {
|
||||
@@ -60,15 +60,9 @@ const QUERY = `{ appointments(first: 100, orderBy: [{ scheduledAt: DescNullsLast
|
||||
doctor { clinic { clinicName } }
|
||||
} } } }`;
|
||||
|
||||
const formatDate = (iso: string): string => {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
};
|
||||
const formatDate = (iso: string): string => formatDateOnly(iso);
|
||||
|
||||
const formatTime = (iso: string): string => {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleTimeString('en-IN', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
};
|
||||
const formatTime = (iso: string): string => formatTimeOnly(iso);
|
||||
|
||||
export const AppointmentsPage = () => {
|
||||
const [appointments, setAppointments] = useState<AppointmentRecord[]>([]);
|
||||
|
||||
@@ -108,6 +108,13 @@ export const CallHistoryPage = () => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [filter, setFilter] = useState<FilterKey>('all');
|
||||
|
||||
// Debug: log first call's raw timestamp to diagnose timezone issue
|
||||
if (calls.length > 0 && !(window as any).__callTimestampLogged) {
|
||||
const c = calls[0];
|
||||
console.log(`[DEBUG-TIME] Raw startedAt="${c.startedAt}" → parsed=${new Date(c.startedAt!)} → formatted="${c.startedAt ? new Intl.DateTimeFormat('en-IN', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }).format(new Date(c.startedAt)) : 'n/a'}" | direction=${c.callDirection} status=${c.callStatus}`);
|
||||
(window as any).__callTimestampLogged = true;
|
||||
}
|
||||
|
||||
// Build a map of lead names by ID for enrichment
|
||||
const leadNameMap = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
@@ -151,20 +158,19 @@ export const CallHistoryPage = () => {
|
||||
return result;
|
||||
}, [calls, filter, search, leadNameMap]);
|
||||
|
||||
const inboundCount = calls.filter((c) => c.callDirection === 'INBOUND').length;
|
||||
const outboundCount = calls.filter((c) => c.callDirection === 'OUTBOUND').length;
|
||||
const missedCount = calls.filter((c) => c.callStatus === 'MISSED').length;
|
||||
const completedCount = filteredCalls.filter((c) => c.callStatus !== 'MISSED').length;
|
||||
const missedCount = filteredCalls.filter((c) => c.callStatus === 'MISSED').length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<TopBar title="Call History" subtitle={`${calls.length} total calls`} />
|
||||
<TopBar title="Call History" subtitle={`${filteredCalls.length} total calls`} />
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-7">
|
||||
<TableCard.Root size="md">
|
||||
<TableCard.Header
|
||||
title="Call History"
|
||||
badge={String(filteredCalls.length)}
|
||||
description={`${inboundCount} inbound \u00B7 ${outboundCount} outbound \u00B7 ${missedCount} missed`}
|
||||
description={`${completedCount} completed \u00B7 ${missedCount} missed`}
|
||||
contentTrailing={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-44">
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Table } from '@/components/application/table/table';
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { formatPhone } from '@/lib/format';
|
||||
import { formatPhone, formatDateOnly } from '@/lib/format';
|
||||
|
||||
type RecordingRecord = {
|
||||
id: string;
|
||||
@@ -30,8 +30,7 @@ const QUERY = `{ calls(first: 200, orderBy: [{ startedAt: DescNullsLast }]) { ed
|
||||
recording { primaryLinkUrl primaryLinkLabel }
|
||||
} } } }`;
|
||||
|
||||
const formatDate = (iso: string): string =>
|
||||
new Date(iso).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
const formatDate = (iso: string): string => formatDateOnly(iso);
|
||||
|
||||
const formatDuration = (sec: number | null): string => {
|
||||
if (!sec) return '—';
|
||||
|
||||
@@ -12,7 +12,7 @@ import { HealthIndicator } from '@/components/campaigns/health-indicator';
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { useCampaigns } from '@/hooks/use-campaigns';
|
||||
import { useLeads } from '@/hooks/use-leads';
|
||||
import { formatCurrency } from '@/lib/format';
|
||||
import { formatCurrency, formatDateOnly } from '@/lib/format';
|
||||
|
||||
const detailTabs = [
|
||||
{ id: 'overview', label: 'Overview' },
|
||||
@@ -41,9 +41,7 @@ export const CampaignDetailPage = () => {
|
||||
|
||||
const formatDateShort = (dateStr: string | null) => {
|
||||
if (!dateStr) return '--';
|
||||
return new Intl.DateTimeFormat('en-IN', { month: 'short', day: 'numeric', year: 'numeric' }).format(
|
||||
new Date(dateStr),
|
||||
);
|
||||
return formatDateOnly(dateStr);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,11 +8,14 @@ import { Button } from '@/components/base/buttons/button';
|
||||
import { SocialButton } from '@/components/base/buttons/social-button';
|
||||
import { Checkbox } from '@/components/base/checkbox/checkbox';
|
||||
import { Input } from '@/components/base/input/input';
|
||||
import { MaintOtpModal } from '@/components/modals/maint-otp-modal';
|
||||
import { useMaintShortcuts } from '@/hooks/use-maint-shortcuts';
|
||||
|
||||
export const LoginPage = () => {
|
||||
const { loginWithUser } = useAuth();
|
||||
const { refresh } = useData();
|
||||
const navigate = useNavigate();
|
||||
const { isOpen, activeAction, close } = useMaintShortcuts();
|
||||
|
||||
const saved = localStorage.getItem('helix_remember');
|
||||
const savedCreds = saved ? JSON.parse(saved) : null;
|
||||
@@ -176,6 +179,8 @@ export const LoginPage = () => {
|
||||
|
||||
{/* Footer */}
|
||||
<a href="https://f0rty2.ai" target="_blank" rel="noopener noreferrer" className="mt-6 text-xs text-primary_on-brand opacity-60 hover:opacity-90 transition duration-100 ease-linear">Powered by F0rty2.ai</a>
|
||||
|
||||
<MaintOtpModal isOpen={isOpen} onOpenChange={(open) => !open && close()} action={activeAction} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { formatPhone } from '@/lib/format';
|
||||
import { formatPhone, formatDateTimeShort } from '@/lib/format';
|
||||
|
||||
type MissedCallRecord = {
|
||||
id: string;
|
||||
@@ -32,8 +32,7 @@ const QUERY = `{ calls(first: 200, filter: {
|
||||
startedAt callsourcenumber callbackstatus missedcallcount callbackattemptedat
|
||||
} } } }`;
|
||||
|
||||
const formatDate = (iso: string): string =>
|
||||
new Date(iso).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
const formatDate = (iso: string): string => formatDateTimeShort(iso);
|
||||
|
||||
const computeSla = (dateStr: string): { label: string; color: 'success' | 'warning' | 'error' } => {
|
||||
const minutes = Math.max(0, Math.round((Date.now() - new Date(dateStr).getTime()) / 60000));
|
||||
|
||||
Reference in New Issue
Block a user