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:
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user