|
|
|
|
@@ -4,13 +4,13 @@ import { useData } from '@/lib/data-context';
|
|
|
|
|
import { ChartCard } from '@/components/ui/ChartCard';
|
|
|
|
|
import { PageHeader } from '@/components/ui/PageHeader';
|
|
|
|
|
import { CHART_COLORS, CHART_PALETTE, DISTRICT_SHORT, formatPercent, formatCurrency, STATUS_COLORS, TIER_COLORS, STAGE_COLORS } from '@/lib/formatters';
|
|
|
|
|
import { useMemo, useState } from 'react';
|
|
|
|
|
import { useMemo, useState, useCallback } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend,
|
|
|
|
|
PieChart, Pie, Cell,
|
|
|
|
|
} from 'recharts';
|
|
|
|
|
import { parseISO, format, startOfWeek, differenceInDays, eachDayOfInterval } from 'date-fns';
|
|
|
|
|
import { ActivityRecord } from '@/types/data';
|
|
|
|
|
import { ActivityRecord, ACTIVITY_TYPES } from '@/types/data';
|
|
|
|
|
import { ExportButton } from '@/components/ui/ExportButton';
|
|
|
|
|
import { AccountNotes } from '@/components/account/AccountNotes';
|
|
|
|
|
import { ContactMap } from '@/components/account/ContactMap';
|
|
|
|
|
@@ -48,15 +48,19 @@ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?:
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type SubView = 'all' | 'follow-ups';
|
|
|
|
|
|
|
|
|
|
export default function ActivityDeepDive() {
|
|
|
|
|
const { filtered, config } = useData();
|
|
|
|
|
const { filtered, config, refresh } = useData();
|
|
|
|
|
const { activities, accounts, pipeline, targets } = filtered;
|
|
|
|
|
const [subView, setSubView] = useState<SubView>('all');
|
|
|
|
|
const [typeFilter, setTypeFilter] = useState<Set<string>>(new Set());
|
|
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
|
const [sortField, setSortField] = useState<'Activity_Date' | 'Account_Name' | 'Activity_Type'>('Activity_Date');
|
|
|
|
|
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
|
|
|
|
|
const [page, setPage] = useState(0);
|
|
|
|
|
const [selectedActivity, setSelectedActivity] = useState<ActivityRecord | null>(null);
|
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
const pageSize = 15;
|
|
|
|
|
|
|
|
|
|
const filteredActivities = useMemo(() => {
|
|
|
|
|
@@ -172,10 +176,102 @@ export default function ActivityDeepDive() {
|
|
|
|
|
|
|
|
|
|
const allTypes = [...new Set(activities.map(a => a.Activity_Type))].sort();
|
|
|
|
|
|
|
|
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
|
|
|
|
|
|
|
const followUpItems = useMemo(() => {
|
|
|
|
|
return (activities as (ActivityRecord & { id?: number })[])
|
|
|
|
|
.filter(a => {
|
|
|
|
|
if (a.Status === 'Completed' || a.Status === 'Did Not Occur') return false;
|
|
|
|
|
if (a.Status === 'Planned' && a.Activity_Date <= today) return true;
|
|
|
|
|
return false;
|
|
|
|
|
})
|
|
|
|
|
.sort((a, b) => a.Activity_Date.localeCompare(b.Activity_Date));
|
|
|
|
|
}, [activities, today]);
|
|
|
|
|
|
|
|
|
|
const plannedUpcoming = useMemo(() => {
|
|
|
|
|
return (activities as (ActivityRecord & { id?: number })[])
|
|
|
|
|
.filter(a => a.Status === 'Planned' && a.Activity_Date > today)
|
|
|
|
|
.sort((a, b) => a.Activity_Date.localeCompare(b.Activity_Date));
|
|
|
|
|
}, [activities, today]);
|
|
|
|
|
|
|
|
|
|
const resolveFollowUp = useCallback(async (id: number, status: 'Completed' | 'Did Not Occur', outcome?: string, notes?: string) => {
|
|
|
|
|
setSaving(true);
|
|
|
|
|
try {
|
|
|
|
|
await fetch('/api/admin', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ action: 'update_status', record: { id, Status: status, Outcome: outcome || null, Notes: notes || null } }),
|
|
|
|
|
});
|
|
|
|
|
await refresh();
|
|
|
|
|
} finally {
|
|
|
|
|
setSaving(false);
|
|
|
|
|
}
|
|
|
|
|
}, [refresh]);
|
|
|
|
|
|
|
|
|
|
const createNextStep = useCallback(async (accountName: string, districtName: string, form: Record<string, string>) => {
|
|
|
|
|
setSaving(true);
|
|
|
|
|
try {
|
|
|
|
|
await fetch('/api/admin', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
action: 'upsert',
|
|
|
|
|
table: 'activities',
|
|
|
|
|
record: {
|
|
|
|
|
Activity_Date: form.date,
|
|
|
|
|
Activity_Type: form.type || 'Call',
|
|
|
|
|
Account_Name: accountName,
|
|
|
|
|
District_Name: districtName,
|
|
|
|
|
Contact_Name: form.contact || null,
|
|
|
|
|
Notes: form.notes || null,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
await refresh();
|
|
|
|
|
} finally {
|
|
|
|
|
setSaving(false);
|
|
|
|
|
}
|
|
|
|
|
}, [refresh]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<PageHeader title="Activities" />
|
|
|
|
|
|
|
|
|
|
{/* Subview tabs */}
|
|
|
|
|
<div className="flex items-center gap-1 mb-4 border-b border-card-border">
|
|
|
|
|
{([
|
|
|
|
|
{ key: 'all' as SubView, label: 'All Activities' },
|
|
|
|
|
{ key: 'follow-ups' as SubView, label: 'Follow-Ups', count: followUpItems.length },
|
|
|
|
|
]).map(tab => (
|
|
|
|
|
<button
|
|
|
|
|
key={tab.key}
|
|
|
|
|
onClick={() => { setSubView(tab.key); setPage(0); }}
|
|
|
|
|
className={`px-4 py-2 text-xs font-medium border-b-2 transition -mb-px ${
|
|
|
|
|
subView === tab.key
|
|
|
|
|
? 'border-brand-azure text-brand-azure'
|
|
|
|
|
: 'border-transparent text-muted hover:text-foreground'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{tab.label}
|
|
|
|
|
{tab.count !== undefined && tab.count > 0 && (
|
|
|
|
|
<span className="ml-1.5 px-1.5 py-0.5 rounded-full text-[10px] font-bold bg-red-500 text-white">{tab.count}</span>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{subView === 'follow-ups' && (
|
|
|
|
|
<FollowUpsView
|
|
|
|
|
followUpItems={followUpItems}
|
|
|
|
|
plannedUpcoming={plannedUpcoming}
|
|
|
|
|
accounts={accounts}
|
|
|
|
|
resolveFollowUp={resolveFollowUp}
|
|
|
|
|
createNextStep={createNextStep}
|
|
|
|
|
saving={saving}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{subView === 'all' && <>
|
|
|
|
|
{/* Type filter chips */}
|
|
|
|
|
<div className="flex flex-wrap gap-2 mb-4">
|
|
|
|
|
{allTypes.map(type => (
|
|
|
|
|
@@ -403,7 +499,15 @@ export default function ActivityDeepDive() {
|
|
|
|
|
className="border-b border-card-border/50 hover:bg-brand-azure/5 transition cursor-pointer"
|
|
|
|
|
onClick={() => setSelectedActivity(a)}
|
|
|
|
|
>
|
|
|
|
|
<td className="py-2 px-2 text-muted">{format(parseISO(a.Activity_Date), 'MMM d')}</td>
|
|
|
|
|
<td className="py-2 px-2 text-muted">
|
|
|
|
|
{format(parseISO(a.Activity_Date), 'MMM d')}
|
|
|
|
|
{a.Status === 'Planned' && a.Activity_Date > today && (
|
|
|
|
|
<span className="ml-1 px-1.5 py-0.5 rounded text-[9px] font-bold bg-blue-100 text-blue-700">Planned</span>
|
|
|
|
|
)}
|
|
|
|
|
{a.Status === 'Planned' && a.Activity_Date <= today && (
|
|
|
|
|
<span className="ml-1 px-1.5 py-0.5 rounded text-[9px] font-bold bg-amber-100 text-amber-700">Follow-Up</span>
|
|
|
|
|
)}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="py-2 px-2 font-medium">{a.Account_Name}</td>
|
|
|
|
|
<td className="py-2 px-2">
|
|
|
|
|
<span className="px-2 py-0.5 rounded-full text-[10px] text-white font-medium" style={{ backgroundColor: TYPE_COLORS[a.Activity_Type] || CHART_COLORS.navy }}>
|
|
|
|
|
@@ -428,6 +532,7 @@ export default function ActivityDeepDive() {
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</ChartCard>
|
|
|
|
|
</>}
|
|
|
|
|
|
|
|
|
|
{/* Activity Detail Overlay */}
|
|
|
|
|
{selectedActivity && overlayData && (
|
|
|
|
|
@@ -664,3 +769,252 @@ export default function ActivityDeepDive() {
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function FollowUpsView({
|
|
|
|
|
followUpItems,
|
|
|
|
|
plannedUpcoming,
|
|
|
|
|
accounts,
|
|
|
|
|
resolveFollowUp,
|
|
|
|
|
createNextStep,
|
|
|
|
|
saving,
|
|
|
|
|
}: {
|
|
|
|
|
followUpItems: (ActivityRecord & { id?: number })[];
|
|
|
|
|
plannedUpcoming: (ActivityRecord & { id?: number })[];
|
|
|
|
|
accounts: { Account_Name: string; District_Name: string }[];
|
|
|
|
|
resolveFollowUp: (id: number, status: 'Completed' | 'Did Not Occur', outcome?: string, notes?: string) => Promise<void>;
|
|
|
|
|
createNextStep: (accountName: string, districtName: string, form: Record<string, string>) => Promise<void>;
|
|
|
|
|
saving: boolean;
|
|
|
|
|
}) {
|
|
|
|
|
const [expandedId, setExpandedId] = useState<number | null>(null);
|
|
|
|
|
const [outcomes, setOutcomes] = useState<Record<number, string>>({});
|
|
|
|
|
const [noteInputs, setNoteInputs] = useState<Record<number, string>>({});
|
|
|
|
|
const [nextStepForms, setNextStepForms] = useState<Record<number, { date: string; type: string; contact: string; notes: string }>>({});
|
|
|
|
|
const [showNextStep, setShowNextStep] = useState<Record<number, boolean>>({});
|
|
|
|
|
|
|
|
|
|
const handleResolve = async (id: number, status: 'Completed' | 'Did Not Occur') => {
|
|
|
|
|
await resolveFollowUp(id, status, outcomes[id], noteInputs[id]);
|
|
|
|
|
setExpandedId(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCreateNext = async (id: number, accountName: string, districtName: string) => {
|
|
|
|
|
const form = nextStepForms[id];
|
|
|
|
|
if (!form?.date) return;
|
|
|
|
|
await createNextStep(accountName, districtName, form);
|
|
|
|
|
setShowNextStep(prev => ({ ...prev, [id]: false }));
|
|
|
|
|
setNextStepForms(prev => { const n = { ...prev }; delete n[id]; return n; });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getDefaultNextDate = () => {
|
|
|
|
|
const d = new Date();
|
|
|
|
|
d.setDate(d.getDate() + 7);
|
|
|
|
|
return d.toISOString().split('T')[0];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<ChartCard title="Pending Follow-Ups" subtitle={`${followUpItems.length} activities need resolution`}>
|
|
|
|
|
{followUpItems.length === 0 ? (
|
|
|
|
|
<div className="text-center py-8 text-sm text-muted">
|
|
|
|
|
No pending follow-ups. You're all caught up!
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{followUpItems.map(item => {
|
|
|
|
|
const id = item.id!;
|
|
|
|
|
const isExpanded = expandedId === id;
|
|
|
|
|
const daysOverdue = Math.floor((new Date().getTime() - new Date(item.Activity_Date).getTime()) / 86400000);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={id} className={`rounded-lg border transition ${isExpanded ? 'border-brand-azure bg-brand-azure/5' : 'border-card-border hover:border-brand-azure/40'}`}>
|
|
|
|
|
<div
|
|
|
|
|
className="flex items-center gap-3 px-4 py-3 cursor-pointer"
|
|
|
|
|
onClick={() => setExpandedId(isExpanded ? null : id)}
|
|
|
|
|
>
|
|
|
|
|
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${daysOverdue > 7 ? 'bg-red-500' : daysOverdue > 3 ? 'bg-amber-500' : 'bg-blue-500'}`} />
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="text-sm font-semibold truncate">{item.Account_Name}</span>
|
|
|
|
|
<span className="px-2 py-0.5 rounded-full text-[10px] text-white font-medium" style={{ backgroundColor: TYPE_COLORS[item.Activity_Type] || CHART_COLORS.navy }}>
|
|
|
|
|
{item.Activity_Type}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-3 text-[11px] text-muted mt-0.5">
|
|
|
|
|
<span>Planned for {format(parseISO(item.Activity_Date), 'MMM d, yyyy')}</span>
|
|
|
|
|
{item.Contact_Name && <span>with {item.Contact_Name}</span>}
|
|
|
|
|
<span className={`font-medium ${daysOverdue > 7 ? 'text-red-600' : daysOverdue > 3 ? 'text-amber-600' : 'text-blue-600'}`}>
|
|
|
|
|
{daysOverdue === 0 ? 'Due today' : `${daysOverdue}d overdue`}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<svg className={`w-4 h-4 text-muted transition-transform ${isExpanded ? 'rotate-180' : ''}`} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="6 9 12 15 18 9" /></svg>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isExpanded && (
|
|
|
|
|
<div className="px-4 pb-4 border-t border-card-border/50 pt-3 space-y-3">
|
|
|
|
|
{item.Notes && (
|
|
|
|
|
<div className="text-xs text-muted bg-white rounded-lg p-2 border border-card-border">
|
|
|
|
|
<span className="font-medium text-foreground">Original notes: </span>{item.Notes}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="text-[10px] font-medium text-muted uppercase mb-1 block">Outcome</label>
|
|
|
|
|
<select
|
|
|
|
|
value={outcomes[id] || ''}
|
|
|
|
|
onChange={e => setOutcomes(prev => ({ ...prev, [id]: e.target.value }))}
|
|
|
|
|
className="w-full text-xs border border-card-border rounded-lg px-2.5 py-1.5 focus:outline-none focus:ring-2 focus:ring-brand-azure/30"
|
|
|
|
|
>
|
|
|
|
|
<option value="">Select outcome...</option>
|
|
|
|
|
<option value="Meeting Held">Meeting Held</option>
|
|
|
|
|
<option value="Meeting Rescheduled">Meeting Rescheduled</option>
|
|
|
|
|
<option value="Positive Response">Positive Response</option>
|
|
|
|
|
<option value="No Response">No Response</option>
|
|
|
|
|
<option value="Left Message">Left Message</option>
|
|
|
|
|
<option value="Proposal Sent">Proposal Sent</option>
|
|
|
|
|
<option value="Follow-Up Required">Follow-Up Required</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="text-[10px] font-medium text-muted uppercase mb-1 block">Notes</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={noteInputs[id] || ''}
|
|
|
|
|
onChange={e => setNoteInputs(prev => ({ ...prev, [id]: e.target.value }))}
|
|
|
|
|
placeholder="What happened?"
|
|
|
|
|
className="w-full text-xs border border-card-border rounded-lg px-2.5 py-1.5 focus:outline-none focus:ring-2 focus:ring-brand-azure/30"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleResolve(id, 'Completed')}
|
|
|
|
|
disabled={saving}
|
|
|
|
|
className="px-3 py-1.5 text-xs font-medium rounded-lg bg-green-600 text-white hover:bg-green-700 disabled:opacity-50 transition"
|
|
|
|
|
>
|
|
|
|
|
Mark Completed
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleResolve(id, 'Did Not Occur')}
|
|
|
|
|
disabled={saving}
|
|
|
|
|
className="px-3 py-1.5 text-xs font-medium rounded-lg bg-gray-500 text-white hover:bg-gray-600 disabled:opacity-50 transition"
|
|
|
|
|
>
|
|
|
|
|
Did Not Occur
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowNextStep(prev => ({ ...prev, [id]: !prev[id] }));
|
|
|
|
|
if (!nextStepForms[id]) {
|
|
|
|
|
setNextStepForms(prev => ({ ...prev, [id]: { date: getDefaultNextDate(), type: item.Activity_Type, contact: item.Contact_Name || '', notes: '' } }));
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className="px-3 py-1.5 text-xs font-medium rounded-lg border border-brand-azure text-brand-azure hover:bg-brand-azure/10 transition"
|
|
|
|
|
>
|
|
|
|
|
Create Next Step
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{showNextStep[id] && (
|
|
|
|
|
<div className="bg-white rounded-lg border border-brand-azure/30 p-3 space-y-2">
|
|
|
|
|
<div className="text-[10px] font-bold text-brand-navy uppercase tracking-wider">Next Step Activity</div>
|
|
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="text-[10px] text-muted block mb-0.5">Date</label>
|
|
|
|
|
<input
|
|
|
|
|
type="date"
|
|
|
|
|
value={nextStepForms[id]?.date || ''}
|
|
|
|
|
onChange={e => setNextStepForms(prev => ({ ...prev, [id]: { ...prev[id], date: e.target.value } }))}
|
|
|
|
|
className="w-full text-xs border border-card-border rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-brand-azure/30"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="text-[10px] text-muted block mb-0.5">Type</label>
|
|
|
|
|
<select
|
|
|
|
|
value={nextStepForms[id]?.type || ''}
|
|
|
|
|
onChange={e => setNextStepForms(prev => ({ ...prev, [id]: { ...prev[id], type: e.target.value } }))}
|
|
|
|
|
className="w-full text-xs border border-card-border rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-brand-azure/30"
|
|
|
|
|
>
|
|
|
|
|
{ACTIVITY_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="text-[10px] text-muted block mb-0.5">Contact</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={nextStepForms[id]?.contact || ''}
|
|
|
|
|
onChange={e => setNextStepForms(prev => ({ ...prev, [id]: { ...prev[id], contact: e.target.value } }))}
|
|
|
|
|
className="w-full text-xs border border-card-border rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-brand-azure/30"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="text-[10px] text-muted block mb-0.5">Notes</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={nextStepForms[id]?.notes || ''}
|
|
|
|
|
onChange={e => setNextStepForms(prev => ({ ...prev, [id]: { ...prev[id], notes: e.target.value } }))}
|
|
|
|
|
className="w-full text-xs border border-card-border rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-brand-azure/30"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleCreateNext(id, item.Account_Name, item.District_Name)}
|
|
|
|
|
disabled={saving || !nextStepForms[id]?.date}
|
|
|
|
|
className="px-3 py-1.5 text-xs font-medium rounded-lg bg-brand-azure text-white hover:bg-brand-azure/90 disabled:opacity-50 transition"
|
|
|
|
|
>
|
|
|
|
|
Create & Schedule
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</ChartCard>
|
|
|
|
|
|
|
|
|
|
{plannedUpcoming.length > 0 && (
|
|
|
|
|
<ChartCard title="Upcoming Planned" subtitle={`${plannedUpcoming.length} scheduled activities`}>
|
|
|
|
|
<div className="overflow-x-auto">
|
|
|
|
|
<table className="w-full text-xs">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr className="border-b border-card-border">
|
|
|
|
|
<th className="text-left py-2 px-2 text-muted font-medium">Date</th>
|
|
|
|
|
<th className="text-left py-2 px-2 text-muted font-medium">Account</th>
|
|
|
|
|
<th className="text-left py-2 px-2 text-muted font-medium">Type</th>
|
|
|
|
|
<th className="text-left py-2 px-2 text-muted font-medium hidden sm:table-cell">Contact</th>
|
|
|
|
|
<th className="text-left py-2 px-2 text-muted font-medium hidden md:table-cell">Notes</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{plannedUpcoming.map((a, i) => {
|
|
|
|
|
const daysUntil = Math.ceil((new Date(a.Activity_Date).getTime() - new Date().getTime()) / 86400000);
|
|
|
|
|
return (
|
|
|
|
|
<tr key={i} className="border-b border-card-border/50">
|
|
|
|
|
<td className="py-2 px-2">
|
|
|
|
|
<span className="text-muted">{format(parseISO(a.Activity_Date), 'MMM d')}</span>
|
|
|
|
|
<span className="ml-1 text-[10px] text-blue-600 font-medium">
|
|
|
|
|
{daysUntil === 0 ? 'Today' : daysUntil === 1 ? 'Tomorrow' : `in ${daysUntil}d`}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="py-2 px-2 font-medium">{a.Account_Name}</td>
|
|
|
|
|
<td className="py-2 px-2">
|
|
|
|
|
<span className="px-2 py-0.5 rounded-full text-[10px] text-white font-medium" style={{ backgroundColor: TYPE_COLORS[a.Activity_Type] || CHART_COLORS.navy }}>
|
|
|
|
|
{a.Activity_Type}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="py-2 px-2 text-muted hidden sm:table-cell">{a.Contact_Name || '—'}</td>
|
|
|
|
|
<td className="py-2 px-2 text-muted hidden md:table-cell max-w-[200px] truncate">{a.Notes || '—'}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</ChartCard>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|