diff --git a/src/components/call-desk/call-log.tsx b/src/components/call-desk/call-log.tsx new file mode 100644 index 0000000..2243c6d --- /dev/null +++ b/src/components/call-desk/call-log.tsx @@ -0,0 +1,77 @@ +import { Badge } from '@/components/base/badges/badges'; +import { Button } from '@/components/base/buttons/button'; +import { ArrowRight } from '@untitledui/icons'; +import { formatShortDate } from '@/lib/format'; +import type { Call, CallDisposition } from '@/types/entities'; + +interface CallLogProps { + calls: Call[]; +} + +const dispositionConfig: Record = { + APPOINTMENT_BOOKED: { label: 'Booked', color: 'success' }, + FOLLOW_UP_SCHEDULED: { label: 'Follow-up', color: 'brand' }, + INFO_PROVIDED: { label: 'Info', color: 'blue-light' }, + NO_ANSWER: { label: 'No Answer', color: 'warning' }, + WRONG_NUMBER: { label: 'Wrong #', color: 'gray' }, + CALLBACK_REQUESTED: { label: 'Not Interested', color: 'error' }, +}; + +const formatDuration = (seconds: number | null): string => { + if (seconds === null || seconds === 0) return '0 min'; + const minutes = Math.round(seconds / 60); + return `${minutes} min`; +}; + +export const CallLog = ({ calls }: CallLogProps) => { + return ( +
+
+
+ Today's Calls + {calls.length} +
+
+ + {calls.length > 0 ? ( +
+ {calls.map((call) => { + const config = call.disposition !== null + ? dispositionConfig[call.disposition] + : null; + + return ( +
+ + {call.startedAt !== null ? formatShortDate(call.startedAt) : '—'} + + + {call.leadName ?? call.callerNumber?.[0]?.number ?? 'Unknown'} + + {config !== null && ( + {config.label} + )} + + {formatDuration(call.durationSeconds)} + +
+ ); + })} +
+ ) : ( +
+

No calls handled today

+
+ )} + +
+ +
+
+ ); +}; diff --git a/src/components/call-desk/call-simulator.tsx b/src/components/call-desk/call-simulator.tsx new file mode 100644 index 0000000..4569a59 --- /dev/null +++ b/src/components/call-desk/call-simulator.tsx @@ -0,0 +1,26 @@ +import { PhoneCall01 } from '@untitledui/icons'; +import { cx } from '@/utils/cx'; + +interface CallSimulatorProps { + onSimulate: () => void; + isCallActive: boolean; +} + +export const CallSimulator = ({ onSimulate, isCallActive }: CallSimulatorProps) => { + return ( + + ); +}; diff --git a/src/components/call-desk/daily-stats.tsx b/src/components/call-desk/daily-stats.tsx new file mode 100644 index 0000000..7a57dbe --- /dev/null +++ b/src/components/call-desk/daily-stats.tsx @@ -0,0 +1,43 @@ +import type { Call } from '@/types/entities'; + +interface DailyStatsProps { + calls: Call[]; +} + +const formatAvgDuration = (calls: Call[]): string => { + if (calls.length === 0) return '0.0 min'; + const totalSeconds = calls.reduce((sum, c) => sum + (c.durationSeconds ?? 0), 0); + const avgMinutes = totalSeconds / calls.length / 60; + return `${avgMinutes.toFixed(1)} min`; +}; + +export const DailyStats = ({ calls }: DailyStatsProps) => { + const callsHandled = calls.length; + const appointmentsBooked = calls.filter((c) => c.disposition === 'APPOINTMENT_BOOKED').length; + const followUps = calls.filter((c) => c.disposition === 'FOLLOW_UP_SCHEDULED').length; + const avgDuration = formatAvgDuration(calls); + + const stats = [ + { label: 'Calls Handled', value: String(callsHandled) }, + { label: 'Appointments Booked', value: String(appointmentsBooked) }, + { label: 'Follow-ups', value: String(followUps) }, + { label: 'Avg Duration', value: avgDuration }, + ]; + + return ( +
+

Daily Stats

+ {stats.map((stat) => ( +
+
{stat.value}
+
+ {stat.label} +
+
+ ))} +
+ ); +}; diff --git a/src/components/call-desk/disposition-form.tsx b/src/components/call-desk/disposition-form.tsx new file mode 100644 index 0000000..b2c7b59 --- /dev/null +++ b/src/components/call-desk/disposition-form.tsx @@ -0,0 +1,111 @@ +import { useState } from 'react'; +import { TextArea } from '@/components/base/textarea/textarea'; +import type { CallDisposition } from '@/types/entities'; +import { cx } from '@/utils/cx'; + +interface DispositionFormProps { + onSubmit: (disposition: CallDisposition, notes: string) => void; +} + +const dispositionOptions: Array<{ + value: CallDisposition; + label: string; + activeClass: string; + defaultClass: string; +}> = [ + { + value: 'APPOINTMENT_BOOKED', + label: 'Appointment Booked', + activeClass: 'bg-success-solid text-white ring-transparent', + defaultClass: 'bg-success-primary text-success-primary border-success', + }, + { + value: 'FOLLOW_UP_SCHEDULED', + label: 'Follow-up Needed', + activeClass: 'bg-brand-solid text-white ring-transparent', + defaultClass: 'bg-brand-primary text-brand-secondary border-brand', + }, + { + value: 'INFO_PROVIDED', + label: 'Info Provided', + activeClass: 'bg-utility-blue-light-600 text-white ring-transparent', + defaultClass: 'bg-utility-blue-light-50 text-utility-blue-light-700 border-utility-blue-light-200', + }, + { + value: 'NO_ANSWER', + label: 'No Answer', + activeClass: 'bg-warning-solid text-white ring-transparent', + defaultClass: 'bg-warning-primary text-warning-primary border-warning', + }, + { + value: 'WRONG_NUMBER', + label: 'Wrong Number', + activeClass: 'bg-secondary-solid text-white ring-transparent', + defaultClass: 'bg-secondary text-secondary border-secondary', + }, + { + value: 'CALLBACK_REQUESTED', + label: 'Not Interested', + activeClass: 'bg-error-solid text-white ring-transparent', + defaultClass: 'bg-error-primary text-error-primary border-error', + }, +]; + +export const DispositionForm = ({ onSubmit }: DispositionFormProps) => { + const [selected, setSelected] = useState(null); + const [notes, setNotes] = useState(''); + + const handleSubmit = () => { + if (selected === null) return; + onSubmit(selected, notes); + }; + + return ( +
+ What happened? + +
+ {dispositionOptions.map((option) => { + const isSelected = selected === option.value; + return ( + + ); + })} +
+ +