import React, { useState, useEffect, useMemo } from 'react';
import { Phone, CheckCircle2, Clock, AlertCircle, Plus, Filter, Search, X, ChevronDown, User, FileText, Mail, RotateCcw } from 'lucide-react';
// ============================================================
// CONFIGURATION — Edit these lists to update the dropdowns
// ============================================================
const ADJUSTERS = [
{ name: 'Leanna Flint', email: '
[email protected]' },
{ name: 'Monica Jackson', email: '
[email protected]' },
{ name: 'John Fischer', email: '
[email protected]' },
{ name: 'Amanda Armendariz', email: '
[email protected]' },
{ name: 'Becky Cedillo', email: '
[email protected]' },
{ name: 'Mario Yanez', email: '
[email protected]' },
{ name: 'Emaus Gutierrez', email: '
[email protected]' },
{ name: 'Alex Rodriguez', email: '
[email protected]' },
{ name: 'Laura Rendon', email: '
[email protected]' },
{ name: 'Samantha Foltz', email: '
[email protected]' },
{ name: 'Evelyn Lopez', email: '
[email protected]' },
{ name: 'Trae Hall', email: '
[email protected]' },
{ name: 'Mercedes Baker', email: '
[email protected]' },
{ name: 'Dan Merritt', email: '
[email protected]' },
{ name: 'Erika Epps', email: '
[email protected]' },
{ name: 'Dillon Emery', email: '
[email protected]' },
{ name: 'Edgar Cerda', email: '
[email protected]' },
{ name: 'Demarcus Smith', email: '
[email protected]' },
{ name: 'Sergio Torres', email: '
[email protected]' },
];
const TOPICS = [
'Status update on claim',
'Settlement discussion',
'Repair authorization',
'Rental car',
'Medical bills or treatment',
'Total loss',
'Subrogation',
'Document request',
'Complaint',
'Other',
];
const STORAGE_KEY = 'callbacks:all';
// ============================================================
// HELPERS
// ============================================================
function formatPhone(value) {
const digits = value.replace(/\D/g, '').slice(0, 10);
if (digits.length < 4) return digits;
if (digits.length < 7) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
}
function ageInMinutes(timestamp) {
return Math.floor((Date.now() - timestamp) / 60000);
}
function formatAge(minutes) {
if (minutes < 1) return 'just now';
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ${minutes % 60}m`;
const days = Math.floor(hours / 24);
return `${days}d ${hours % 24}h`;
}
function ageColor(minutes) {
if (minutes < 60) return 'fresh'; // under 1h
if (minutes < 240) return 'normal'; // under 4h
if (minutes < 1440) return 'warning'; // under 24h
return 'critical'; // 24h+
}
function formatTimestamp(ts) {
return new Date(ts).toLocaleString('en-US', {
month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit'
});
}
function uid() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}
// ============================================================
// MAIN COMPONENT
// ============================================================
export default function ClaimsCallbacksDashboard() {
const [callbacks, setCallbacks] = useState([]);
const [loading, setLoading] = useState(true);
const [view, setView] = useState('pending'); // 'pending' | 'completed'
const [adjusterFilter, setAdjusterFilter] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
const [showForm, setShowForm] = useState(false);
const [showEmailPreview, setShowEmailPreview] = useState(null);
const [tick, setTick] = useState(0); // forces re-render for age updates
// Load data on mount
useEffect(() => {
loadCallbacks();
}, []);
// Re-render every 30s so ages stay current
useEffect(() => {
const interval = setInterval(() => setTick(t => t + 1), 30000);
return () => clearInterval(interval);
}, []);
async function loadCallbacks() {
setLoading(true);
try {
const result = await window.storage.get(STORAGE_KEY, true);
if (result && result.value) {
setCallbacks(JSON.parse(result.value));
} else {
setCallbacks([]);
}
} catch (e) {
// Key doesn't exist yet — that's fine, start with empty array
setCallbacks([]);
}
setLoading(false);
}
async function saveCallbacks(updated) {
setCallbacks(updated);
try {
await window.storage.set(STORAGE_KEY, JSON.stringify(updated), true);
} catch (e) {
console.error('Save failed:', e);
}
}
async function addCallback(data) {
const newCallback = {
id: uid(),
...data,
status: 'pending',
createdAt: Date.now(),
completedAt: null,
completedBy: null,
};
const updated = [newCallback, ...callbacks];
await saveCallbacks(updated);
setShowForm(false);
setShowEmailPreview(newCallback);
}
async function markComplete(id, completedBy) {
const updated = callbacks.map(cb =>
cb.id === id
? { ...cb, status: 'completed', completedAt: Date.now(), completedBy }
: cb
);
await saveCallbacks(updated);
}
async function reopenCallback(id) {
const updated = callbacks.map(cb =>
cb.id === id
? { ...cb, status: 'pending', completedAt: null, completedBy: null }
: cb
);
await saveCallbacks(updated);
}
// Filtering
const filtered = useMemo(() => {
return callbacks
.filter(cb => cb.status === view)
.filter(cb => adjusterFilter === 'all' || cb.adjuster === adjusterFilter)
.filter(cb => {
if (!searchQuery) return true;
const q = searchQuery.toLowerCase();
return (
cb.callerName.toLowerCase().includes(q) ||
cb.claimNumber.toLowerCase().includes(q) ||
cb.phone.includes(q) ||
cb.topic.toLowerCase().includes(q)
);
});
}, [callbacks, view, adjusterFilter, searchQuery, tick]);
// Counts for tabs
const pendingCount = callbacks.filter(cb => cb.status === 'pending').length;
const completedCount = callbacks.filter(cb => cb.status === 'completed').length;
const overdueCount = callbacks.filter(
cb => cb.status === 'pending' && ageInMinutes(cb.createdAt) >= 240
).length;
return (
{/* Header */}
Claims Call Backs
Titan Claim Services
{/* Stat row */}
0 ? 'red' : 'neutral'}
/>
{/* Tabs + filters */}
setView('pending')}>
Pending ({pendingCount})
setView('completed')}>
Completed ({completedCount})
{/* Table */}
{loading ? (
Loading...
) : filtered.length === 0 ? (
) : (
)}
{/* Footer info */}
{callbacks.length} total records · Data shared across all users
Ages update every 30 seconds
{/* Form modal */}
{showForm && (
setShowForm(false)}
/>
)}
{/* Email preview modal */}
{showEmailPreview && (
setShowEmailPreview(null)}
/>
)}
);
}
// ============================================================
// SUB-COMPONENTS
// ============================================================
function StatCard({ label, value, icon: Icon, accent }) {
const accents = {
amber: 'bg-amber-50 text-amber-700 border-amber-100',
red: 'bg-red-50 text-red-700 border-red-100',
green: 'bg-emerald-50 text-emerald-700 border-emerald-100',
neutral: 'bg-stone-50 text-stone-600 border-stone-100',
};
return (
);
}
function TabButton({ active, onClick, children }) {
return (
);
}
function EmptyState({ view, hasFilters }) {
return (
{view === 'pending' ? (
) : (
)}
{hasFilters
? 'No matching call backs'
: view === 'pending'
? 'No pending call backs'
: 'No completed call backs yet'}
{hasFilters
? 'Try clearing filters or search'
: view === 'pending'
? 'All caught up. Click "New call back" to add one.'
: 'Completed call backs will appear here.'}
);
}
function CallbackTable({ callbacks, view, onComplete, onReopen, onViewEmail }) {
return (
|
Caller |
Claim # |
Phone |
Topic |
Adjuster |
{view === 'pending' ? 'Age' : 'Completed'} |
|
{callbacks.map(cb => (
))}
);
}
function CallbackRow({ callback: cb, view, onComplete, onReopen, onViewEmail }) {
const [showCompleteDialog, setShowCompleteDialog] = useState(false);
const minutes = ageInMinutes(cb.createdAt);
const ageBadge = ageColor(minutes);
const ageBadgeClass = {
fresh: 'bg-stone-100 text-stone-600',
normal: 'bg-stone-100 text-stone-600',
warning: 'bg-amber-50 text-amber-700 border border-amber-200',
critical: 'bg-red-50 text-red-700 border border-red-200',
}[ageBadge];
return (
<>
|
{view === 'pending' ? (
) : (
)}
|
{cb.callerName}
{cb.notes && (
{cb.notes}
)}
|
{cb.claimNumber} |
{cb.phone}
|
{cb.topic} |
{cb.adjuster} |
{view === 'pending' ? (
{formatAge(minutes)}
) : (
{formatTimestamp(cb.completedAt)}
by {cb.completedBy}
)}
|
{view === 'completed' && (
)}
|
{showCompleteDialog && (
{
onComplete(cb.id, name);
setShowCompleteDialog(false);
}}
onCancel={() => setShowCompleteDialog(false)}
/>
)}
>
);
}
function CompleteDialog({ callback: cb, onConfirm, onCancel }) {
const [completedBy, setCompletedBy] = useState(cb.adjuster);
return (
Mark call back complete
Confirm you called {cb.callerName} regarding claim {cb.claimNumber}.
);
}
function CallbackForm({ onSubmit, onCancel }) {
const [callerName, setCallerName] = useState('');
const [claimNumber, setClaimNumber] = useState('');
const [phone, setPhone] = useState('');
const [adjuster, setAdjuster] = useState('');
const [topic, setTopic] = useState('');
const [notes, setNotes] = useState('');
const [submittedBy, setSubmittedBy] = useState('');
const [errors, setErrors] = useState({});
function validate() {
const e = {};
if (!callerName.trim()) e.callerName = 'Required';
if (!claimNumber.trim()) e.claimNumber = 'Required';
if (!phone.trim()) e.phone = 'Required';
else if (phone.replace(/\D/g, '').length !== 10) e.phone = '10 digits required';
if (!adjuster) e.adjuster = 'Required';
if (!topic) e.topic = 'Required';
if (!submittedBy.trim()) e.submittedBy = 'Required';
return e;
}
function handleSubmit() {
const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
onSubmit({
callerName: callerName.trim(),
claimNumber: claimNumber.trim(),
phone: phone.trim(),
adjuster,
topic,
notes: notes.trim(),
submittedBy: submittedBy.trim(),
});
}
return (
New call back request
Fill out the details from the call. Submitting adds it to the dashboard and sends notifications.
);
}
function Field({ label, error, required, optional, children }) {
return (
{children}
{error && (
)}
);
}
function SelectWithChevron({ value, onChange, options }) {
return (
);
}
function EmailPreview({ callback: cb, onClose }) {
const adjusterEmail = ADJUSTERS.find(a => a.name === cb.adjuster)?.email || '';
const adjusterEmailBody = `Hi ${cb.adjuster.split(' ')[0]},
A new call back request has been logged for you.
Caller: ${cb.callerName}
Claim number: ${cb.claimNumber}
Phone: ${cb.phone}
Topic: ${cb.topic}
Logged by: ${cb.submittedBy}
Time received: ${formatTimestamp(cb.createdAt)}
${cb.notes ? `
Notes from intake:
${cb.notes}
` : ''}
Please call back as soon as possible. Mark complete in the dashboard once you've made contact.
— Titan Claim Services`;
const sharedInboxBody = `New call back request logged.
Caller: ${cb.callerName}
Claim number: ${cb.claimNumber}
Phone: ${cb.phone}
Topic: ${cb.topic}
Assigned to: ${cb.adjuster}
Logged by: ${cb.submittedBy}
Time received: ${formatTimestamp(cb.createdAt)}
${cb.notes ? `
Notes from intake:
${cb.notes}
` : ''}`;
return (
Notification emails
In the production version, these emails will be sent automatically when a call back is submitted.
Prototype note: No emails are actually being sent. This preview shows what they would look like.
`}
subject={`Call back — ${cb.callerName} — Claim ${cb.claimNumber}`}
body={adjusterEmailBody}
/>
);
}
function EmailCard({ label, to, subject, body }) {
return (
{label}
To:
{to}
Subject:
{subject}
);
}
function Modal({ children, onClose, wide }) {
useEffect(() => {
function handleEsc(e) {
if (e.key === 'Escape') onClose();
}
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, [onClose]);
return (
e.stopPropagation()}
>
{children}
);
}