mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
feat: call-desk refresh — disposition modal, active-call UI, worklist + perf updates
- Call-desk: active-call-card supervisor presence badges, incoming-call-card polish, transfer-dialog, call-log - Disposition modal: auto-lock based on actions taken, not-interested split - Forms: appointment-form + enquiry-form improvements (placeholder handling, phone format) - Worklist-panel: pagination awareness, filter chips - Pages: all-leads/patients/patient-360/missed-calls/team-performance/call-history/appointments polish - SIP: sip-client reconnect, sip-provider + sip-manager state, agent-status-toggle spinner - Hooks: use-agent-state supervisor SSE events, use-worklist, use-performance-alerts - Types: entities.ts extended Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,10 +65,13 @@ const formatPhoneDisplay = (call: Call): string => {
|
||||
|
||||
const dispositionConfig: Record<CallDisposition, { label: string; color: 'success' | 'brand' | 'blue-light' | 'warning' | 'gray' | 'error' }> = {
|
||||
APPOINTMENT_BOOKED: { label: 'Appt Booked', color: 'success' },
|
||||
APPOINTMENT_RESCHEDULED: { label: 'Appt Rescheduled', color: 'warning' },
|
||||
APPOINTMENT_CANCELLED: { label: 'Appt Cancelled', color: 'error' },
|
||||
FOLLOW_UP_SCHEDULED: { label: 'Follow-up', color: 'brand' },
|
||||
INFO_PROVIDED: { label: 'Info Provided', color: 'blue-light' },
|
||||
NO_ANSWER: { label: 'No Answer', color: 'warning' },
|
||||
WRONG_NUMBER: { label: 'Wrong Number', color: 'gray' },
|
||||
NOT_INTERESTED: { label: 'Not Interested', color: 'error' },
|
||||
CALLBACK_REQUESTED: { label: 'Callback', color: 'brand' },
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FC } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { useSearchParams, useNavigate } from 'react-router';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faArrowLeft, faArrowDownToLine, faMagnifyingGlass } from '@fortawesome/pro-duotone-svg-icons';
|
||||
|
||||
@@ -38,6 +38,7 @@ const PAGE_SIZE = 15;
|
||||
|
||||
export const AllLeadsPage = () => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const initialSource = searchParams.get('source') as LeadSource | null;
|
||||
const [tab, setTab] = useState<TabKey>('new');
|
||||
@@ -231,11 +232,11 @@ export const AllLeadsPage = () => {
|
||||
<div className="flex shrink-0 items-center justify-between px-6 py-3 border-b border-secondary">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
href="/"
|
||||
onClick={() => navigate(-1)}
|
||||
color="secondary"
|
||||
size="sm"
|
||||
iconLeading={ArrowLeft}
|
||||
aria-label="Back to workspace"
|
||||
aria-label="Back"
|
||||
/>
|
||||
|
||||
<Tabs selectedKey={tab} onSelectionChange={handleTabChange}>
|
||||
|
||||
@@ -217,7 +217,7 @@ export const AppointmentsPage = () => {
|
||||
<span className="text-xs text-tertiary">{appt.department ?? '—'}</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span className="text-xs text-tertiary truncate block max-w-[130px]">{branch}</span>
|
||||
<span className="text-xs text-tertiary truncate block max-w-[180px]" title={branch}>{branch}</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge size="sm" color={statusColor} type="pill-color">
|
||||
|
||||
@@ -35,10 +35,13 @@ const filterItems = [
|
||||
|
||||
const dispositionConfig: Record<CallDisposition, { label: string; color: 'success' | 'brand' | 'blue-light' | 'warning' | 'gray' | 'error' }> = {
|
||||
APPOINTMENT_BOOKED: { label: 'Appt Booked', color: 'success' },
|
||||
APPOINTMENT_RESCHEDULED: { label: 'Appt Rescheduled', color: 'warning' },
|
||||
APPOINTMENT_CANCELLED: { label: 'Appt Cancelled', color: 'error' },
|
||||
FOLLOW_UP_SCHEDULED: { label: 'Follow-up', color: 'brand' },
|
||||
INFO_PROVIDED: { label: 'Info Provided', color: 'blue-light' },
|
||||
NO_ANSWER: { label: 'No Answer', color: 'warning' },
|
||||
WRONG_NUMBER: { label: 'Wrong Number', color: 'gray' },
|
||||
NOT_INTERESTED: { label: 'Not Interested', color: 'error' },
|
||||
CALLBACK_REQUESTED: { label: 'Callback', color: 'brand' },
|
||||
};
|
||||
|
||||
@@ -139,8 +142,9 @@ export const CallHistoryPage = () => {
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
// Direction / status filter
|
||||
if (filter === 'inbound') result = result.filter((c) => c.callDirection === 'INBOUND');
|
||||
// Direction / status filter. "Inbound" shows answered inbound only — missed
|
||||
// calls have their own dedicated filter so they don't double-appear.
|
||||
if (filter === 'inbound') result = result.filter((c) => c.callDirection === 'INBOUND' && c.callStatus !== 'MISSED');
|
||||
else if (filter === 'outbound') result = result.filter((c) => c.callDirection === 'OUTBOUND');
|
||||
else if (filter === 'missed') result = result.filter((c) => c.callStatus === 'MISSED');
|
||||
|
||||
|
||||
@@ -22,10 +22,10 @@ type MissedCallRecord = {
|
||||
callerNumber: { primaryPhoneNumber: string } | null;
|
||||
agentName: string | null;
|
||||
startedAt: string | null;
|
||||
callsourcenumber: string | null;
|
||||
callbackstatus: string | null;
|
||||
missedcallcount: number | null;
|
||||
callbackattemptedat: string | null;
|
||||
callSourceNumber: string | null;
|
||||
callbackStatus: string | null;
|
||||
missedCallCount: number | null;
|
||||
callbackAttemptedAt: string | null;
|
||||
sla: number | null;
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ const QUERY = `{ calls(first: 200, filter: {
|
||||
callStatus: { eq: MISSED }
|
||||
}, orderBy: [{ startedAt: DescNullsLast }]) { edges { node {
|
||||
id callerNumber { primaryPhoneNumber } agentName
|
||||
startedAt callsourcenumber callbackstatus missedcallcount callbackattemptedat sla
|
||||
startedAt callSourceNumber callbackStatus missedCallCount callbackAttemptedAt sla
|
||||
} } } }`;
|
||||
|
||||
const PAGE_SIZE = 15;
|
||||
@@ -92,7 +92,7 @@ export const MissedCallsPage = () => {
|
||||
const statusCounts = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const c of calls) {
|
||||
const s = c.callbackstatus ?? 'PENDING_CALLBACK';
|
||||
const s = c.callbackStatus ?? 'PENDING_CALLBACK';
|
||||
counts[s] = (counts[s] ?? 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
@@ -100,16 +100,16 @@ export const MissedCallsPage = () => {
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
let rows = calls;
|
||||
if (tab === 'PENDING_CALLBACK') rows = rows.filter(c => c.callbackstatus === 'PENDING_CALLBACK' || !c.callbackstatus);
|
||||
else if (tab === 'CALLBACK_ATTEMPTED') rows = rows.filter(c => c.callbackstatus === 'CALLBACK_ATTEMPTED');
|
||||
else if (tab === 'CALLBACK_COMPLETED') rows = rows.filter(c => c.callbackstatus === 'CALLBACK_COMPLETED' || c.callbackstatus === 'WRONG_NUMBER');
|
||||
if (tab === 'PENDING_CALLBACK') rows = rows.filter(c => c.callbackStatus === 'PENDING_CALLBACK' || !c.callbackStatus);
|
||||
else if (tab === 'CALLBACK_ATTEMPTED') rows = rows.filter(c => c.callbackStatus === 'CALLBACK_ATTEMPTED');
|
||||
else if (tab === 'CALLBACK_COMPLETED') rows = rows.filter(c => c.callbackStatus === 'CALLBACK_COMPLETED' || c.callbackStatus === 'WRONG_NUMBER');
|
||||
|
||||
if (search.trim()) {
|
||||
const q = search.toLowerCase();
|
||||
rows = rows.filter(c =>
|
||||
(c.callerNumber?.primaryPhoneNumber ?? '').includes(q) ||
|
||||
(c.agentName ?? '').toLowerCase().includes(q) ||
|
||||
(c.callsourcenumber ?? '').toLowerCase().includes(q),
|
||||
(c.callSourceNumber ?? '').toLowerCase().includes(q),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ export const MissedCallsPage = () => {
|
||||
const tb = b.startedAt ? new Date(b.startedAt).getTime() : 0;
|
||||
return (ta - tb) * dir;
|
||||
}
|
||||
case 'count': return ((a.missedcallcount ?? 1) - (b.missedcallcount ?? 1)) * dir;
|
||||
case 'count': return ((a.missedCallCount ?? 1) - (b.missedCallCount ?? 1)) * dir;
|
||||
case 'sla': return ((a.sla ?? 0) - (b.sla ?? 0)) * dir;
|
||||
case 'agent': return (a.agentName ?? '').localeCompare(b.agentName ?? '') * dir;
|
||||
default: return 0;
|
||||
@@ -190,7 +190,7 @@ export const MissedCallsPage = () => {
|
||||
<Table.Body items={pagedRows}>
|
||||
{(call) => {
|
||||
const phone = call.callerNumber?.primaryPhoneNumber ?? '';
|
||||
const status = call.callbackstatus ?? 'PENDING_CALLBACK';
|
||||
const status = call.callbackStatus ?? 'PENDING_CALLBACK';
|
||||
|
||||
return (
|
||||
<Table.Row id={call.id}>
|
||||
@@ -213,7 +213,7 @@ export const MissedCallsPage = () => {
|
||||
)}
|
||||
{visibleColumns.has('branch') && (
|
||||
<Table.Cell>
|
||||
<span className="text-xs text-tertiary">{call.callsourcenumber || '—'}</span>
|
||||
<span className="text-xs text-tertiary">{call.callSourceNumber || '—'}</span>
|
||||
</Table.Cell>
|
||||
)}
|
||||
{visibleColumns.has('agent') && (
|
||||
@@ -223,8 +223,8 @@ export const MissedCallsPage = () => {
|
||||
)}
|
||||
{visibleColumns.has('count') && (
|
||||
<Table.Cell>
|
||||
{call.missedcallcount && call.missedcallcount > 1 ? (
|
||||
<Badge size="sm" color="warning" type="pill-color">{call.missedcallcount}x</Badge>
|
||||
{call.missedCallCount && call.missedCallCount > 1 ? (
|
||||
<Badge size="sm" color="warning" type="pill-color">{call.missedCallCount}x</Badge>
|
||||
) : <span className="text-xs text-quaternary">1</span>}
|
||||
</Table.Cell>
|
||||
)}
|
||||
@@ -256,10 +256,10 @@ export const MissedCallsPage = () => {
|
||||
)}
|
||||
{visibleColumns.has('callback') && (
|
||||
<Table.Cell>
|
||||
{call.callbackattemptedat ? (
|
||||
{call.callbackAttemptedAt ? (
|
||||
<div>
|
||||
<span className="text-sm text-primary">{formatDateOrdinal(call.callbackattemptedat)}</span>
|
||||
<span className="block text-xs text-tertiary">{formatTimeOnly(call.callbackattemptedat)}</span>
|
||||
<span className="text-sm text-primary">{formatDateOrdinal(call.callbackAttemptedAt)}</span>
|
||||
<span className="block text-xs text-tertiary">{formatTimeOnly(call.callbackAttemptedAt)}</span>
|
||||
</div>
|
||||
) : <span className="text-xs text-quaternary">—</span>}
|
||||
</Table.Cell>
|
||||
|
||||
@@ -51,10 +51,13 @@ const DEFAULT_CONFIG: ActivityConfig = { icon: '📌', dotClass: 'bg-tertiary',
|
||||
|
||||
const DISPOSITION_COLORS: Record<CallDisposition, 'success' | 'brand' | 'blue' | 'error' | 'warning' | 'gray'> = {
|
||||
APPOINTMENT_BOOKED: 'success',
|
||||
APPOINTMENT_RESCHEDULED: 'warning',
|
||||
APPOINTMENT_CANCELLED: 'error',
|
||||
FOLLOW_UP_SCHEDULED: 'brand',
|
||||
INFO_PROVIDED: 'blue',
|
||||
WRONG_NUMBER: 'error',
|
||||
NO_ANSWER: 'warning',
|
||||
NOT_INTERESTED: 'error',
|
||||
CALLBACK_REQUESTED: 'gray',
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Badge } from '@/components/base/badges/badges';
|
||||
import { Input } from '@/components/base/input/input';
|
||||
import { Table, TableCard } from '@/components/application/table/table';
|
||||
import { PaginationPageDefault } from '@/components/application/pagination/pagination';
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
|
||||
import { ClickToCallButton } from '@/components/call-desk/click-to-call-button';
|
||||
import { PatientProfilePanel } from '@/components/shared/patient-profile-panel';
|
||||
import { useData } from '@/providers/data-provider';
|
||||
@@ -86,8 +86,6 @@ export const PatientsPage = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<TopBar title="Patients" subtitle={`${filteredPatients.length} patients`} />
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<div className="flex flex-1 flex-col overflow-y-auto p-7">
|
||||
<TableCard.Root size="sm">
|
||||
@@ -141,7 +139,7 @@ export const PatientsPage = () => {
|
||||
<Table.Head label="AGE" />
|
||||
<Table.Head label="ACTIONS" />
|
||||
</Table.Header>
|
||||
<Table.Body items={pagedPatients}>
|
||||
<Table.Body items={pagedPatients} dependencies={[selectedPatient?.id]}>
|
||||
{(patient) => {
|
||||
const displayName = getPatientDisplayName(patient);
|
||||
const age = computeAge(patient.dateOfBirth);
|
||||
|
||||
@@ -33,11 +33,11 @@ const parseTime = (timeStr: string): number => {
|
||||
|
||||
type AgentPerf = {
|
||||
name: string;
|
||||
ozonetelagentid: string;
|
||||
npsscore: number | null;
|
||||
maxidleminutes: number | null;
|
||||
minnpsthreshold: number | null;
|
||||
minconversionpercent: number | null;
|
||||
ozonetelAgentId: string;
|
||||
npsScore: number | null;
|
||||
maxIdleMinutes: number | null;
|
||||
minNpsThreshold: number | null;
|
||||
minConversionPercent: number | null;
|
||||
calls: number;
|
||||
inbound: number;
|
||||
missed: number;
|
||||
@@ -112,7 +112,7 @@ export const TeamPerformancePage = () => {
|
||||
if (teamAgents.length > 0) {
|
||||
// Real Ozonetel data available
|
||||
agentPerfs = teamAgents.map((agent: any) => {
|
||||
const agentCalls = calls.filter((c: any) => c.agentName === agent.name || c.agentName === agent.ozonetelagentid);
|
||||
const agentCalls = calls.filter((c: any) => c.agentName === agent.name || c.agentName === agent.ozonetelAgentId);
|
||||
const agentLeads = leads.filter((l: any) => l.assignedAgent === agent.name);
|
||||
const agentFollowUps = followUps.filter((f: any) => f.assignedAgent === agent.name);
|
||||
const agentAppts = agentCalls.filter((c: any) => c.callStatus === 'COMPLETED').length;
|
||||
@@ -127,12 +127,12 @@ export const TeamPerformancePage = () => {
|
||||
const breakSec = tb ? parseTime(tb.totalPauseTime ?? '0:0:0') : 0;
|
||||
|
||||
return {
|
||||
name: agent.name ?? agent.ozonetelagentid,
|
||||
ozonetelagentid: agent.ozonetelagentid,
|
||||
npsscore: agent.npsscore,
|
||||
maxidleminutes: agent.maxidleminutes,
|
||||
minnpsthreshold: agent.minnpsthreshold,
|
||||
minconversionpercent: agent.minconversionpercent,
|
||||
name: agent.name ?? agent.ozonetelAgentId,
|
||||
ozonetelAgentId: agent.ozonetelAgentId,
|
||||
npsScore: agent.npsScore,
|
||||
maxIdleMinutes: agent.maxIdleMinutes,
|
||||
minNpsThreshold: agent.minNpsThreshold,
|
||||
minConversionPercent: agent.minConversionPercent,
|
||||
calls: totalCalls,
|
||||
inbound,
|
||||
missed,
|
||||
@@ -159,11 +159,11 @@ export const TeamPerformancePage = () => {
|
||||
|
||||
return {
|
||||
name,
|
||||
ozonetelagentid: name,
|
||||
npsscore: null,
|
||||
maxidleminutes: null,
|
||||
minnpsthreshold: null,
|
||||
minconversionpercent: null,
|
||||
ozonetelAgentId: name,
|
||||
npsScore: null,
|
||||
maxIdleMinutes: null,
|
||||
minNpsThreshold: null,
|
||||
minConversionPercent: null,
|
||||
calls: totalCalls,
|
||||
inbound: agentCalls.filter((c: any) => c.direction === 'INBOUND').length,
|
||||
missed: agentCalls.filter((c: any) => c.callStatus === 'MISSED').length,
|
||||
@@ -223,9 +223,9 @@ export const TeamPerformancePage = () => {
|
||||
|
||||
// NPS
|
||||
const avgNps = useMemo(() => {
|
||||
const withNps = agents.filter(a => a.npsscore != null);
|
||||
const withNps = agents.filter(a => a.npsScore != null);
|
||||
if (withNps.length === 0) return 0;
|
||||
return Math.round(withNps.reduce((sum, a) => sum + (a.npsscore ?? 0), 0) / withNps.length);
|
||||
return Math.round(withNps.reduce((sum, a) => sum + (a.npsScore ?? 0), 0) / withNps.length);
|
||||
}, [agents]);
|
||||
|
||||
const npsOption = useMemo(() => ({
|
||||
@@ -246,13 +246,13 @@ export const TeamPerformancePage = () => {
|
||||
const alerts = useMemo(() => {
|
||||
const list: { agent: string; type: string; value: string; severity: 'error' | 'warning' }[] = [];
|
||||
for (const a of agents) {
|
||||
if (a.maxidleminutes && a.idleMinutes > a.maxidleminutes) {
|
||||
if (a.maxIdleMinutes && a.idleMinutes > a.maxIdleMinutes) {
|
||||
list.push({ agent: a.name, type: 'Excessive Idle Time', value: `${a.idleMinutes}m`, severity: 'error' });
|
||||
}
|
||||
if (a.minnpsthreshold && (a.npsscore ?? 100) < a.minnpsthreshold) {
|
||||
list.push({ agent: a.name, type: 'Low NPS', value: String(a.npsscore ?? 0), severity: 'warning' });
|
||||
if (a.minNpsThreshold && (a.npsScore ?? 100) < a.minNpsThreshold) {
|
||||
list.push({ agent: a.name, type: 'Low NPS', value: String(a.npsScore ?? 0), severity: 'warning' });
|
||||
}
|
||||
if (a.minconversionpercent && a.convPercent < a.minconversionpercent) {
|
||||
if (a.minConversionPercent && a.convPercent < a.minConversionPercent) {
|
||||
list.push({ agent: a.name, type: 'Low Conversion', value: `${a.convPercent}%`, severity: 'warning' });
|
||||
}
|
||||
}
|
||||
@@ -332,7 +332,7 @@ export const TeamPerformancePage = () => {
|
||||
</Table.Header>
|
||||
<Table.Body items={agents}>
|
||||
{(agent) => (
|
||||
<Table.Row id={agent.ozonetelagentid || agent.name}>
|
||||
<Table.Row id={agent.ozonetelAgentId || agent.name}>
|
||||
<Table.Cell><span className="text-sm font-medium text-primary">{agent.name}</span></Table.Cell>
|
||||
<Table.Cell><span className="text-sm text-primary">{agent.calls}</span></Table.Cell>
|
||||
<Table.Cell><span className="text-sm text-primary">{agent.inbound}</span></Table.Cell>
|
||||
@@ -345,12 +345,12 @@ export const TeamPerformancePage = () => {
|
||||
</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span className={cx('text-sm font-bold', (agent.npsscore ?? 0) >= 70 ? 'text-success-primary' : (agent.npsscore ?? 0) >= 50 ? 'text-warning-primary' : 'text-error-primary')}>
|
||||
{agent.npsscore ?? '—'}
|
||||
<span className={cx('text-sm font-bold', (agent.npsScore ?? 0) >= 70 ? 'text-success-primary' : (agent.npsScore ?? 0) >= 50 ? 'text-warning-primary' : 'text-error-primary')}>
|
||||
{agent.npsScore ?? '—'}
|
||||
</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span className={cx('text-sm', agent.maxidleminutes && agent.idleMinutes > agent.maxidleminutes ? 'text-error-primary font-bold' : 'text-primary')}>
|
||||
<span className={cx('text-sm', agent.maxIdleMinutes && agent.idleMinutes > agent.maxIdleMinutes ? 'text-error-primary font-bold' : 'text-primary')}>
|
||||
{agent.idleMinutes}m
|
||||
</span>
|
||||
</Table.Cell>
|
||||
@@ -389,7 +389,7 @@ export const TeamPerformancePage = () => {
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{agents.map(agent => {
|
||||
const total = agent.activeMinutes + agent.wrapMinutes + agent.idleMinutes + agent.breakMinutes || 1;
|
||||
const isHighIdle = agent.maxidleminutes && agent.idleMinutes > agent.maxidleminutes;
|
||||
const isHighIdle = agent.maxIdleMinutes && agent.idleMinutes > agent.maxIdleMinutes;
|
||||
return (
|
||||
<div key={agent.name} className={cx('rounded-lg border p-3', isHighIdle ? 'border-error bg-error-secondary' : 'border-secondary')}>
|
||||
<p className="text-xs font-semibold text-primary mb-2">{agent.name}</p>
|
||||
@@ -417,7 +417,7 @@ export const TeamPerformancePage = () => {
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 rounded-xl border border-secondary bg-primary p-4">
|
||||
<h3 className="text-sm font-semibold text-secondary mb-2">Overall NPS</h3>
|
||||
{agents.every(a => a.npsscore == null) ? (
|
||||
{agents.every(a => a.npsScore == null) ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<p className="text-xs text-tertiary">NPS data unavailable — configure NPS scores on agent profiles.</p>
|
||||
</div>
|
||||
@@ -425,13 +425,13 @@ export const TeamPerformancePage = () => {
|
||||
<>
|
||||
<ReactECharts option={npsOption} style={{ height: 150 }} />
|
||||
<div className="space-y-1 mt-2">
|
||||
{agents.filter(a => a.npsscore != null).map(a => (
|
||||
{agents.filter(a => a.npsScore != null).map(a => (
|
||||
<div key={a.name} className="flex items-center gap-2">
|
||||
<span className="text-xs text-secondary w-28 truncate">{a.name}</span>
|
||||
<div className="flex-1 h-2 rounded-full bg-tertiary overflow-hidden">
|
||||
<div className={cx('h-full rounded-full', (a.npsscore ?? 0) >= 70 ? 'bg-success-solid' : (a.npsscore ?? 0) >= 50 ? 'bg-warning-solid' : 'bg-error-solid')} style={{ width: `${a.npsscore ?? 0}%` }} />
|
||||
<div className={cx('h-full rounded-full', (a.npsScore ?? 0) >= 70 ? 'bg-success-solid' : (a.npsScore ?? 0) >= 50 ? 'bg-warning-solid' : 'bg-error-solid')} style={{ width: `${a.npsScore ?? 0}%` }} />
|
||||
</div>
|
||||
<span className="text-xs font-bold text-primary w-8 text-right">{a.npsscore}</span>
|
||||
<span className="text-xs font-bold text-primary w-8 text-right">{a.npsScore}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user