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' },

View File

@@ -32,6 +32,7 @@ function getDb(): Database.Database {
initSchema(_db);
migrateSchema(_db);
seedDefaultPlaybooks();
seedCPMValues();
}
return _db;
}
@@ -48,6 +49,9 @@ function migrateSchema(db: Database.Database) {
if (!colNames.has('Campaign_Artifacts_URL')) {
db.exec("ALTER TABLE accounts ADD COLUMN Campaign_Artifacts_URL TEXT DEFAULT NULL");
}
if (!colNames.has('CPM')) {
db.exec("ALTER TABLE accounts ADD COLUMN CPM TEXT DEFAULT NULL");
}
}
function initSchema(db: Database.Database) {
@@ -67,6 +71,8 @@ function initSchema(db: Database.Database) {
Area_Sales_Leader TEXT,
DM TEXT,
AD TEXT,
IMS_BA TEXT,
CPM TEXT,
Logo_URL TEXT,
Next_Renewal_Date TEXT,
Next_Renewal_EAR REAL,
@@ -504,13 +510,13 @@ export function deleteTarget(accountName: string) {
export function upsertAccount(record: Record<string, unknown>) {
const db = getDb();
db.prepare(`INSERT INTO accounts (Account_Name, District_Name, Tier, Priority, AgentMinder_Status, Current_ARR_USD, MAP_In_Place_YN, Company_URL, Area_Sales_Leader, DM, AD)
VALUES (@Account_Name, @District_Name, @Tier, @Priority, @AgentMinder_Status, @Current_ARR_USD, @MAP_In_Place_YN, @Company_URL, @Area_Sales_Leader, @DM, @AD)
db.prepare(`INSERT INTO accounts (Account_Name, District_Name, Tier, Priority, AgentMinder_Status, Current_ARR_USD, MAP_In_Place_YN, Company_URL, Area_Sales_Leader, DM, AD, CPM)
VALUES (@Account_Name, @District_Name, @Tier, @Priority, @AgentMinder_Status, @Current_ARR_USD, @MAP_In_Place_YN, @Company_URL, @Area_Sales_Leader, @DM, @AD, @CPM)
ON CONFLICT(Account_Name) DO UPDATE SET
District_Name=excluded.District_Name, Tier=excluded.Tier, Priority=excluded.Priority,
AgentMinder_Status=excluded.AgentMinder_Status, Current_ARR_USD=excluded.Current_ARR_USD,
MAP_In_Place_YN=excluded.MAP_In_Place_YN, Company_URL=excluded.Company_URL,
Area_Sales_Leader=excluded.Area_Sales_Leader, DM=excluded.DM, AD=excluded.AD
Area_Sales_Leader=excluded.Area_Sales_Leader, DM=excluded.DM, AD=excluded.AD, CPM=excluded.CPM
`).run({
Account_Name: record.Account_Name || '',
District_Name: record.District_Name || '',
@@ -523,6 +529,7 @@ export function upsertAccount(record: Record<string, unknown>) {
Area_Sales_Leader: record.Area_Sales_Leader ?? null,
DM: record.DM ?? null,
AD: record.AD ?? null,
CPM: record.CPM ?? null,
});
}
@@ -802,3 +809,19 @@ export function upsertTarget(record: Record<string, unknown>) {
Risk_Notes: record.Risk_Notes || record.Notes || null,
});
}
function seedCPMValues() {
const db = getDb();
const cpmAccounts = [
'ADT', 'ADVENTHEALTH', 'ARBITRATION FORUMS', 'CARLYLE GROUP', 'CARNIVAL',
'ENTERGY', 'Geographic Solutions', 'GUIDEWELL BLUE CROSS BLUE SHIELD OF FLORIDA',
'JABIL CIRCUIT', 'KASEYA', 'LEE MEMORIAL HEALTH SYSTEM', 'LUMEN',
'NEXTERA ENERGY', 'NORWEGIAN CRUISE LINE', 'OCHSNER HEALTH SYSTEM',
'ORLANDO HEALTH', 'PAYSPAN', 'PUBLIX SUPERMARKETS', 'RAYMOND JAMES FINANCIAL',
'TECH DATA',
];
const stmt = db.prepare("UPDATE accounts SET CPM = 'David Stefan' WHERE Account_Name = ? AND (CPM IS NULL OR CPM = '')");
for (const name of cpmAccounts) {
stmt.run(name);
}
}

View File

@@ -85,6 +85,7 @@ function generateAccounts(): AccountRecord[] {
DM: null,
AD: null,
IMS_BA: null,
CPM: null,
Logo_URL: null,
Next_Renewal_Date: null,
Next_Renewal_EAR: null,

View File

@@ -41,6 +41,7 @@ export interface AccountRecord {
DM: string | null;
AD: string | null;
IMS_BA: string | null;
CPM: string | null;
Logo_URL: string | null;
Next_Renewal_Date: string | null;
Next_Renewal_EAR: number | null;