Add CPM (Customer Portfolio Manager) attribute to accounts

- New CPM field on AccountRecord type, DB schema, and migration
- Seed "David Stefan" as CPM for 20 specified accounts
- CPM filter dropdown on Account Explorer (alongside AD and IMS BA)
- CPM shown on account cards, account detail view, and activity overlay
- CPM column added to admin data table
- upsertAccount includes CPM in insert/update

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 09:19:38 -04:00
parent 957376c770
commit 9d016a08cb
6 changed files with 62 additions and 9 deletions

View File

@@ -111,6 +111,7 @@ export default function AccountExplorer() {
const [renewalFilter, setRenewalFilter] = useState<string>('all');
const [priorityFilter, setPriorityFilter] = useState<string | null>(null);
const [competitorFilter, setCompetitorFilter] = useState<string | null>(null);
const [cpmFilter, setCpmFilter] = useState<string | null>(null);
const [selectedAccount, setSelectedAccount] = useState<AccountRecord | null>(null);
const [showCreateOpp, setShowCreateOpp] = useState(false);
const [showAddActivity, setShowAddActivity] = useState(false);
@@ -177,6 +178,7 @@ export default function AccountExplorer() {
const adValues = useMemo(() => Array.from(new Set(accounts.map(a => a.AD).filter(Boolean))).sort() as string[], [accounts]);
const imsbaValues = useMemo(() => Array.from(new Set(accounts.map(a => a.IMS_BA).filter(Boolean))).sort() as string[], [accounts]);
const cpmValues = useMemo(() => Array.from(new Set(accounts.map(a => a.CPM).filter(Boolean))).sort() as string[], [accounts]);
const filteredAccounts = useMemo(() => {
const now = new Date();
@@ -190,6 +192,7 @@ export default function AccountExplorer() {
if (districtFilter) result = result.filter(a => a.District_Name === districtFilter);
if (adFilter) result = result.filter(a => a.AD === adFilter);
if (imsbaFilter) result = result.filter(a => a.IMS_BA === imsbaFilter);
if (cpmFilter) result = result.filter(a => a.CPM === cpmFilter);
if (priorityFilter) result = result.filter(a => a.Priority === priorityFilter);
if (competitorFilter) result = result.filter(a => {
const comps = (a.Competitors || '').split(',').map(c => c.trim()).filter(Boolean);
@@ -213,7 +216,7 @@ export default function AccountExplorer() {
}
}
return result;
}, [accounts, search, tierFilter, statusFilter, districtFilter, adFilter, imsbaFilter, renewalFilter, priorityFilter, competitorFilter]);
}, [accounts, search, tierFilter, statusFilter, districtFilter, adFilter, imsbaFilter, cpmFilter, renewalFilter, priorityFilter, competitorFilter]);
const kpis = useMemo(() => {
const acctNames = new Set(filteredAccounts.map(a => a.Account_Name));
@@ -292,11 +295,12 @@ export default function AccountExplorer() {
setImsbaFilter(null);
setPriorityFilter(null);
setCompetitorFilter(null);
setCpmFilter(null);
setRenewalFilter('all');
setSearch('');
};
const hasFilters = tierFilter || statusFilter || districtFilter || adFilter || imsbaFilter || priorityFilter || competitorFilter || renewalFilter !== 'all' || search;
const hasFilters = tierFilter || statusFilter || districtFilter || adFilter || imsbaFilter || cpmFilter || priorityFilter || competitorFilter || renewalFilter !== 'all' || search;
if (selectedAccount) {
const suggestion = getSuggestedPriority(selectedAccount);
@@ -395,8 +399,8 @@ export default function AccountExplorer() {
</div>
)}
{(selectedAccount.AD || selectedAccount.IMS_BA || selectedAccount.Area_Sales_Leader || selectedAccount.DM) && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-3 pt-3 border-t border-card-border">
{(selectedAccount.AD || selectedAccount.IMS_BA || selectedAccount.CPM || selectedAccount.Area_Sales_Leader || selectedAccount.DM) && (
<div className="grid grid-cols-2 sm:grid-cols-5 gap-4 mt-3 pt-3 border-t border-card-border">
{selectedAccount.Area_Sales_Leader && (
<div>
<div className="text-[10px] text-muted uppercase">Area Sales Leader</div>
@@ -421,6 +425,12 @@ export default function AccountExplorer() {
<div className="text-sm font-semibold">{selectedAccount.IMS_BA}</div>
</div>
)}
{selectedAccount.CPM && (
<div>
<div className="text-[10px] text-muted uppercase">CPM</div>
<div className="text-sm font-semibold">{selectedAccount.CPM}</div>
</div>
)}
</div>
)}
</div>
@@ -815,6 +825,16 @@ export default function AccountExplorer() {
{imsbaValues.map(v => <option key={v} value={v}>{v}</option>)}
</select>
)}
{cpmValues.length > 0 && (
<select
value={cpmFilter || ''}
onChange={e => setCpmFilter(e.target.value || null)}
className="text-xs border border-card-border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-brand-azure/30 bg-white"
>
<option value="">All CPMs</option>
{cpmValues.map(v => <option key={v} value={v}>{v}</option>)}
</select>
)}
<select
value={renewalFilter}
onChange={e => setRenewalFilter(e.target.value)}
@@ -866,6 +886,7 @@ export default function AccountExplorer() {
<span className="px-1.5 py-0.5 rounded text-[9px] font-medium text-white" style={{ backgroundColor: STATUS_COLORS[account.AgentMinder_Status] || '#94A3B8' }}>{account.AgentMinder_Status}</span>
<span className="text-[10px] text-muted">{DISTRICT_SHORT[account.District_Name]}</span>
{account.AD && <span className="text-[10px] text-muted">| {account.AD}</span>}
{account.CPM && <span className="text-[10px] text-muted">| CPM: {account.CPM}</span>}
{(account.Google_Drive_URL || account.Campaign_Artifacts_URL) && (
<div className="flex items-center gap-1.5 ml-auto">
{account.Google_Drive_URL && (

View File

@@ -554,8 +554,8 @@ export default function ActivityDeepDive() {
<div className="font-semibold">{overlayData.account.MAP_In_Place_YN === 'Y' ? 'Yes' : 'No'}</div>
</div>
</div>
{(overlayData.account.AD || overlayData.account.DM || overlayData.account.Area_Sales_Leader) && (
<div className="grid grid-cols-3 gap-3 mt-3 pt-3 border-t border-card-border text-xs">
{(overlayData.account.AD || overlayData.account.DM || overlayData.account.Area_Sales_Leader || overlayData.account.CPM) && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-3 pt-3 border-t border-card-border text-xs">
{overlayData.account.Area_Sales_Leader && (
<div>
<div className="text-[10px] text-muted uppercase">ASL</div>
@@ -574,6 +574,12 @@ export default function ActivityDeepDive() {
<div className="font-semibold">{overlayData.account.AD}</div>
</div>
)}
{overlayData.account.CPM && (
<div>
<div className="text-[10px] text-muted uppercase">CPM</div>
<div className="font-semibold">{overlayData.account.CPM}</div>
</div>
)}
</div>
)}
</div>

View File

@@ -270,6 +270,7 @@ const TABLE_FIELDS: Record<string, FieldDef[]> = {
{ key: 'DM', label: 'DM' },
{ key: 'AD', label: 'AD' },
{ key: 'IMS_BA', label: 'IMS BA' },
{ key: 'CPM', label: 'CPM' },
{ key: 'Next_Renewal_Date', label: 'Next Renewal Date', type: 'date' },
{ key: 'Next_Renewal_EAR', label: 'Next Renewal EAR', type: 'number' },
{ key: 'Anchor_Contract_Date', label: 'Anchor Contract Date', type: 'date' },