mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: dashboard restructure, integrations, settings, UI fixes
Dashboard:
- Split into components (kpi-cards, agent-table, missed-queue)
- Add collapsible AI panel on right (same pattern as Call Desk)
- Add tabs: Agent Performance | Missed Queue | Campaigns
- Date range filter in header
Integrations page:
- Ozonetel (connected), WhatsApp, Facebook, Google, Instagram, Website, Email
- Status badges, config details, webhook URL with copy button
Settings page:
- Employee table from workspaceMembers GraphQL query
- Name, email, roles, status, reset password action
Fixes:
- Fix CALLS_QUERY: callerNumber needs { primaryPhoneNumber }, recordingUrl → recording { primaryLinkUrl }
- Remove duplicate AI Assistant header
- Remove Follow-ups from CC agent sidebar (already in worklist tabs)
- Remove global search from TopBar (decorative, unused)
- Slim down TopBar height
- Fix search/table gap in worklist
- Add brand border to active nav item
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import type { FC, HTMLAttributes } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
@@ -224,6 +224,16 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
|
||||
const callbackCount = allRows.filter((r) => r.type === 'callback').length;
|
||||
const followUpCount = allRows.filter((r) => r.type === 'follow-up').length;
|
||||
|
||||
const PAGE_SIZE = 15;
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
// Reset page when filters change
|
||||
const handleTabChange = useCallback((key: TabKey) => { setTab(key); setPage(1); }, []);
|
||||
const handleSearch = useCallback((value: string) => { setSearch(value); setPage(1); }, []);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / PAGE_SIZE));
|
||||
const pagedRows = filteredRows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
||||
|
||||
const tabItems = [
|
||||
{ id: 'all' as const, label: 'All Tasks', badge: allRows.length > 0 ? String(allRows.length) : undefined },
|
||||
{ id: 'missed' as const, label: 'Missed Calls', badge: missedCount > 0 ? String(missedCount) : undefined },
|
||||
@@ -251,43 +261,35 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCard.Root size="sm">
|
||||
<TableCard.Header
|
||||
title="Worklist"
|
||||
badge={String(allRows.length)}
|
||||
contentTrailing={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-48">
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
icon={SearchLg}
|
||||
size="sm"
|
||||
value={search}
|
||||
onChange={(value) => setSearch(value)}
|
||||
aria-label="Search worklist"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Filter tabs */}
|
||||
<div className="border-b border-secondary px-4">
|
||||
<Tabs selectedKey={tab} onSelectionChange={(key) => setTab(key as TabKey)}>
|
||||
<div className="flex flex-1 flex-col">
|
||||
{/* Filter tabs + search — single row */}
|
||||
<div className="flex items-end justify-between border-b border-secondary px-5 pt-3 pb-0.5">
|
||||
<Tabs selectedKey={tab} onSelectionChange={(key) => handleTabChange(key as TabKey)}>
|
||||
<TabList items={tabItems} type="underline" size="sm">
|
||||
{(item) => <Tab key={item.id} id={item.id} label={item.label} badge={item.badge} />}
|
||||
</TabList>
|
||||
</Tabs>
|
||||
<div className="w-44 shrink-0">
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
icon={SearchLg}
|
||||
size="sm"
|
||||
value={search}
|
||||
onChange={handleSearch}
|
||||
aria-label="Search worklist"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredRows.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<p className="text-sm text-quaternary">
|
||||
{search ? 'No matching items' : 'No items in this category'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<div className="px-2 pt-3">
|
||||
<Table size="sm">
|
||||
<Table.Header>
|
||||
<Table.Head label="PRIORITY" className="w-20" />
|
||||
<Table.Head label="PATIENT" />
|
||||
@@ -296,7 +298,7 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
|
||||
<Table.Head label="SLA" className="w-20" />
|
||||
<Table.Head label="ACTIONS" className="w-24" />
|
||||
</Table.Header>
|
||||
<Table.Body items={filteredRows}>
|
||||
<Table.Body items={pagedRows}>
|
||||
{(row) => {
|
||||
const priority = priorityConfig[row.priority] ?? priorityConfig.NORMAL;
|
||||
const sla = computeSla(row.createdAt);
|
||||
@@ -367,8 +369,44 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
|
||||
}}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between border-t border-secondary px-5 py-3">
|
||||
<span className="text-xs text-tertiary">
|
||||
Showing {(page - 1) * PAGE_SIZE + 1}–{Math.min(page * PAGE_SIZE, filteredRows.length)} of {filteredRows.length}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setPage(Math.max(1, page - 1))}
|
||||
disabled={page === 1}
|
||||
className="px-2 py-1 text-xs font-medium text-secondary rounded-md hover:bg-primary_hover disabled:text-disabled disabled:cursor-not-allowed transition duration-100 ease-linear"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => setPage(p)}
|
||||
className={cx(
|
||||
"size-8 text-xs font-medium rounded-lg transition duration-100 ease-linear",
|
||||
p === page ? "bg-active text-brand-secondary" : "text-tertiary hover:bg-primary_hover",
|
||||
)}
|
||||
>
|
||||
{p}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={() => setPage(Math.min(totalPages, page + 1))}
|
||||
disabled={page === totalPages}
|
||||
className="px-2 py-1 text-xs font-medium text-secondary rounded-md hover:bg-primary_hover disabled:text-disabled disabled:cursor-not-allowed transition duration-100 ease-linear"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableCard.Root>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user