Files
helix-engage/src/components/admin/sla-metrics.tsx
saridsa2 6f40b82579 refactor: migrate all icons from Untitled UI to FontAwesome Pro Duotone
Replace all @untitledui/icons imports across 55 files with equivalent
@fortawesome/pro-duotone-svg-icons icons, using FontAwesomeIcon wrappers
(FC<{ className?: string }>) for prop-based usage and inline replacements
for direct JSX usage. Drops unsupported Untitled UI-specific props
(strokeWidth, numeric size). TypeScript compiles clean with no errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 11:07:19 +05:30

121 lines
4.6 KiB
TypeScript

import type { FC } from 'react';
import { useMemo } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircleCheck, faTriangleExclamation, faCircleExclamation } from '@fortawesome/pro-duotone-svg-icons';
import { cx } from '@/utils/cx';
import type { Lead } from '@/types/entities';
const CheckCircle: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faCircleCheck} className={className} />;
const AlertTriangle: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faTriangleExclamation} className={className} />;
const AlertCircle: FC<{ className?: string }> = ({ className }) => <FontAwesomeIcon icon={faCircleExclamation} className={className} />;
interface SlaMetricsProps {
leads: Lead[];
}
const SLA_TARGET_HOURS = 2;
export const SlaMetrics = ({ leads }: SlaMetricsProps) => {
const metrics = useMemo(() => {
const responseTimes: number[] = [];
let withinSla = 0;
let total = leads.length;
for (const lead of leads) {
if (lead.createdAt && lead.firstContactedAt) {
const created = new Date(lead.createdAt).getTime();
const contacted = new Date(lead.firstContactedAt).getTime();
const diffHours = (contacted - created) / (1000 * 60 * 60);
responseTimes.push(diffHours);
if (diffHours <= SLA_TARGET_HOURS) {
withinSla++;
}
}
// Leads without firstContactedAt are counted as outside SLA
}
const avgHours =
responseTimes.length > 0
? responseTimes.reduce((sum, h) => sum + h, 0) / responseTimes.length
: 0;
const slaPercent = total > 0 ? (withinSla / total) * 100 : 0;
return { avgHours, withinSla, total, slaPercent };
}, [leads]);
const getTargetStatus = (): { icon: FC<{ className?: string }>; label: string; colorClass: string } => {
const diff = metrics.avgHours - SLA_TARGET_HOURS;
if (diff <= 0) {
return {
icon: CheckCircle,
label: 'Below target',
colorClass: 'text-success-primary',
};
}
if (diff <= 0.5) {
return {
icon: AlertTriangle,
label: 'Near target',
colorClass: 'text-warning-primary',
};
}
return {
icon: AlertCircle,
label: 'Above target',
colorClass: 'text-error-primary',
};
};
const status = getTargetStatus();
const StatusIcon = status.icon;
return (
<div className="rounded-2xl border border-secondary bg-primary p-5">
<h3 className="text-sm font-bold text-primary">Response SLA</h3>
<div className="mt-4 flex items-end gap-3">
<span className="text-display-sm font-bold text-primary">
{metrics.avgHours.toFixed(1)}h
</span>
<div className="mb-1 flex items-center gap-1">
<span className="text-xs text-tertiary">Target: {SLA_TARGET_HOURS}h</span>
<StatusIcon className={cx('size-4', status.colorClass)} />
</div>
</div>
<span className={cx('mt-1 block text-xs font-medium', status.colorClass)}>
{status.label}
</span>
<div className="mt-4">
<div className="flex items-center justify-between text-xs text-tertiary">
<span>SLA Compliance</span>
<span className="font-medium text-primary">{Math.round(metrics.slaPercent)}%</span>
</div>
<div className="mt-1.5 h-2 w-full overflow-hidden rounded-full bg-tertiary">
<div
className={cx(
'h-full rounded-full transition-all',
metrics.slaPercent >= 80
? 'bg-success-500'
: metrics.slaPercent >= 60
? 'bg-warning-500'
: 'bg-error-500',
)}
style={{ width: `${metrics.slaPercent}%` }}
/>
</div>
<span className="mt-1.5 block text-xs text-tertiary">
{metrics.withinSla} of {metrics.total} leads within SLA ({Math.round(metrics.slaPercent)}%)
</span>
</div>
</div>
);
};