Build AgentMinder Campaign Command Center - Phase 1 complete
Full-featured sales campaign dashboard replacing Looker Studio with responsive Next.js app. Includes Executive Overview, Account Explorer with priority algorithm, Activities with detail overlay, Pipeline/Opps with deal panels, Implementation tracking, and Data Admin with CSV import and CRUD operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
484
src/lib/db.ts
Normal file
484
src/lib/db.ts
Normal file
@@ -0,0 +1,484 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
import {
|
||||
PipelineRecord,
|
||||
AccountRecord,
|
||||
ActivityRecord,
|
||||
TargetRecord,
|
||||
ImplementationRecord,
|
||||
MetricTarget,
|
||||
DashboardData,
|
||||
} from '@/types/data';
|
||||
|
||||
const DB_PATH = path.join(process.cwd(), 'data', 'campaign.db');
|
||||
|
||||
let _db: Database.Database | null = null;
|
||||
|
||||
function getDb(): Database.Database {
|
||||
if (!_db) {
|
||||
const fs = require('fs');
|
||||
const dir = path.dirname(DB_PATH);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
_db = new Database(DB_PATH);
|
||||
_db.pragma('journal_mode = WAL');
|
||||
initSchema(_db);
|
||||
}
|
||||
return _db;
|
||||
}
|
||||
|
||||
function initSchema(db: Database.Database) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
Account_Name TEXT PRIMARY KEY,
|
||||
District_Name TEXT NOT NULL DEFAULT '',
|
||||
Tier TEXT NOT NULL DEFAULT '',
|
||||
Priority TEXT NOT NULL DEFAULT '',
|
||||
AgentMinder_Status TEXT NOT NULL DEFAULT 'Not Touched',
|
||||
Current_ARR_USD REAL,
|
||||
Touch_Count INTEGER DEFAULT 0,
|
||||
Date_First_Touched TEXT,
|
||||
Date_Last_Touched TEXT,
|
||||
MAP_In_Place_YN TEXT,
|
||||
Company_URL TEXT,
|
||||
Area_Sales_Leader TEXT,
|
||||
DM TEXT,
|
||||
AD TEXT,
|
||||
Logo_URL TEXT,
|
||||
Next_Renewal_Date TEXT,
|
||||
Next_Renewal_EAR REAL,
|
||||
Anchor_Contract_Date TEXT,
|
||||
Anchor_Contract_EAR REAL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pipeline (
|
||||
Opportunity_ID TEXT PRIMARY KEY,
|
||||
Account_Name TEXT NOT NULL,
|
||||
District_Name TEXT NOT NULL DEFAULT '',
|
||||
Stage TEXT NOT NULL DEFAULT '01-Qualified',
|
||||
Forecast_Category TEXT NOT NULL DEFAULT 'Pipeline',
|
||||
Amount_USD REAL NOT NULL DEFAULT 0,
|
||||
Closed_Amount_USD REAL,
|
||||
Probability_Pct INTEGER NOT NULL DEFAULT 0,
|
||||
Created_Date TEXT NOT NULL,
|
||||
Expected_Close_Date TEXT NOT NULL DEFAULT '',
|
||||
Closed_Date TEXT,
|
||||
Champion_Name TEXT,
|
||||
Economic_Buyer TEXT,
|
||||
Next_Step TEXT,
|
||||
Next_Step_Date TEXT,
|
||||
Primary_Objection TEXT,
|
||||
Competitor TEXT,
|
||||
Source_Play TEXT,
|
||||
Product TEXT NOT NULL DEFAULT 'AgentMinder',
|
||||
Deal_Type TEXT,
|
||||
Status TEXT DEFAULT 'Open',
|
||||
Stage_Entered_Date TEXT,
|
||||
Tier TEXT,
|
||||
MAP_In_Place_YN TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activities (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
Activity_ID TEXT,
|
||||
Activity_Date TEXT NOT NULL,
|
||||
Activity_Type TEXT NOT NULL,
|
||||
Account_Name TEXT NOT NULL,
|
||||
District_Name TEXT NOT NULL DEFAULT '',
|
||||
Contact_Name TEXT,
|
||||
Notes TEXT,
|
||||
Play TEXT,
|
||||
Channel TEXT,
|
||||
Persona TEXT,
|
||||
Outcome TEXT,
|
||||
Logged_By TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS implementations (
|
||||
Implementation_ID TEXT PRIMARY KEY,
|
||||
Opportunity_ID TEXT,
|
||||
Account_Name TEXT NOT NULL,
|
||||
District_Name TEXT,
|
||||
Tier TEXT,
|
||||
Closed_Date TEXT,
|
||||
Closed_Amount_USD REAL,
|
||||
Kickoff_Date TEXT,
|
||||
Go_Live_Target TEXT,
|
||||
Go_Live_Actual TEXT,
|
||||
Deployment_Status TEXT NOT NULL DEFAULT 'Not Started',
|
||||
Health_RYG TEXT,
|
||||
Milestones_Complete INTEGER DEFAULT 0,
|
||||
Milestones_Total INTEGER DEFAULT 5,
|
||||
Onboarding_Owner TEXT,
|
||||
Risk_Notes TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS metric_targets (
|
||||
Target_ID TEXT PRIMARY KEY,
|
||||
Period_Type TEXT NOT NULL,
|
||||
Period_Label TEXT NOT NULL DEFAULT '',
|
||||
District_Name TEXT NOT NULL DEFAULT '',
|
||||
Metric TEXT NOT NULL,
|
||||
Target_Value REAL NOT NULL DEFAULT 0,
|
||||
Notes TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pipeline_account ON pipeline(Account_Name);
|
||||
CREATE INDEX IF NOT EXISTS idx_pipeline_district ON pipeline(District_Name);
|
||||
CREATE INDEX IF NOT EXISTS idx_activities_account ON activities(Account_Name);
|
||||
CREATE INDEX IF NOT EXISTS idx_activities_date ON activities(Activity_Date);
|
||||
CREATE INDEX IF NOT EXISTS idx_impl_account ON implementations(Account_Name);
|
||||
`);
|
||||
}
|
||||
|
||||
export function getDashboardData(): DashboardData {
|
||||
const db = getDb();
|
||||
return {
|
||||
pipeline: db.prepare('SELECT * FROM pipeline').all() as PipelineRecord[],
|
||||
accounts: db.prepare('SELECT * FROM accounts').all() as AccountRecord[],
|
||||
activities: db.prepare('SELECT id, Activity_Date, Activity_Type, Account_Name, District_Name, Contact_Name, Notes, Play, Channel, Persona, Outcome, Logged_By FROM activities ORDER BY Activity_Date DESC').all() as (ActivityRecord & { id: number })[],
|
||||
targets: buildTargetsFromImplementations(db),
|
||||
implementations: db.prepare('SELECT * FROM implementations WHERE Account_Name IS NOT NULL').all() as ImplementationRecord[],
|
||||
metricTargets: db.prepare('SELECT * FROM metric_targets').all() as MetricTarget[],
|
||||
lastRefreshed: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildTargetsFromImplementations(db: Database.Database): TargetRecord[] {
|
||||
const impls = db.prepare('SELECT * FROM implementations WHERE Account_Name IS NOT NULL').all() as ImplementationRecord[];
|
||||
return impls.map(impl => ({
|
||||
Account_Name: impl.Account_Name,
|
||||
Implementation_Stage: impl.Deployment_Status || 'Not Started',
|
||||
Go_Live_Date: impl.Go_Live_Target || impl.Go_Live_Actual || null,
|
||||
Health_Status: impl.Health_RYG || null,
|
||||
Notes: impl.Risk_Notes || null,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getDbStats() {
|
||||
const db = getDb();
|
||||
return {
|
||||
accounts: (db.prepare('SELECT COUNT(*) as c FROM accounts').get() as { c: number }).c,
|
||||
pipeline: (db.prepare('SELECT COUNT(*) as c FROM pipeline').get() as { c: number }).c,
|
||||
activities: (db.prepare('SELECT COUNT(*) as c FROM activities').get() as { c: number }).c,
|
||||
targets: (db.prepare('SELECT COUNT(*) as c FROM implementations').get() as { c: number }).c,
|
||||
metricTargets: (db.prepare('SELECT COUNT(*) as c FROM metric_targets').get() as { c: number }).c,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateLastTouched(accountName: string, date: string) {
|
||||
const db = getDb();
|
||||
const acct = db.prepare('SELECT Date_First_Touched, Touch_Count FROM accounts WHERE Account_Name = ?').get(accountName) as { Date_First_Touched: string | null; Touch_Count: number | null } | undefined;
|
||||
if (!acct) return;
|
||||
|
||||
const updates: Record<string, unknown> = {
|
||||
Account_Name: accountName,
|
||||
Date_Last_Touched: date,
|
||||
Touch_Count: (acct.Touch_Count || 0) + 1,
|
||||
};
|
||||
|
||||
if (!acct.Date_First_Touched) {
|
||||
updates.Date_First_Touched = date;
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
UPDATE accounts SET
|
||||
Date_Last_Touched = @Date_Last_Touched,
|
||||
Touch_Count = @Touch_Count
|
||||
${!acct.Date_First_Touched ? ', Date_First_Touched = @Date_First_Touched' : ''}
|
||||
WHERE Account_Name = @Account_Name
|
||||
`).run(updates);
|
||||
}
|
||||
|
||||
export function recalcAccountTouchData() {
|
||||
const db = getDb();
|
||||
const activities = db.prepare(`
|
||||
SELECT Account_Name, MIN(Activity_Date) as first_touch, MAX(Activity_Date) as last_touch, COUNT(*) as touch_count
|
||||
FROM activities
|
||||
GROUP BY Account_Name
|
||||
`).all() as { Account_Name: string; first_touch: string; last_touch: string; touch_count: number }[];
|
||||
|
||||
const update = db.prepare(`
|
||||
UPDATE accounts SET
|
||||
Date_First_Touched = @first_touch,
|
||||
Date_Last_Touched = @last_touch,
|
||||
Touch_Count = @touch_count
|
||||
WHERE Account_Name = @Account_Name
|
||||
`);
|
||||
|
||||
const tx = db.transaction(() => {
|
||||
for (const a of activities) {
|
||||
update.run(a);
|
||||
}
|
||||
});
|
||||
tx();
|
||||
}
|
||||
|
||||
export function recalcAccountStatus() {
|
||||
const db = getDb();
|
||||
db.exec(`
|
||||
UPDATE accounts SET AgentMinder_Status = CASE
|
||||
WHEN Account_Name IN (SELECT DISTINCT Account_Name FROM pipeline WHERE Status = 'Open') THEN '20% - Research'
|
||||
WHEN Account_Name IN (SELECT DISTINCT Account_Name FROM activities) THEN '10% - Prospect'
|
||||
ELSE 'Not Touched'
|
||||
END
|
||||
WHERE AgentMinder_Status = 'Not Touched'
|
||||
OR AgentMinder_Status IS NULL
|
||||
`);
|
||||
}
|
||||
|
||||
export function addActivity(record: {
|
||||
Activity_Date: string;
|
||||
Activity_Type: string;
|
||||
Account_Name: string;
|
||||
District_Name: string;
|
||||
Contact_Name?: string | null;
|
||||
Notes?: string | null;
|
||||
Play?: string | null;
|
||||
Channel?: string | null;
|
||||
Persona?: string | null;
|
||||
Outcome?: string | null;
|
||||
Logged_By?: string | null;
|
||||
}) {
|
||||
const db = getDb();
|
||||
db.prepare(`
|
||||
INSERT INTO activities (Activity_Date, Activity_Type, Account_Name, District_Name, Contact_Name, Notes, Play, Channel, Persona, Outcome, Logged_By)
|
||||
VALUES (@Activity_Date, @Activity_Type, @Account_Name, @District_Name, @Contact_Name, @Notes, @Play, @Channel, @Persona, @Outcome, @Logged_By)
|
||||
`).run({
|
||||
Activity_Date: record.Activity_Date,
|
||||
Activity_Type: record.Activity_Type,
|
||||
Account_Name: record.Account_Name,
|
||||
District_Name: record.District_Name,
|
||||
Contact_Name: record.Contact_Name || null,
|
||||
Notes: record.Notes || null,
|
||||
Play: record.Play || null,
|
||||
Channel: record.Channel || null,
|
||||
Persona: record.Persona || null,
|
||||
Outcome: record.Outcome || null,
|
||||
Logged_By: record.Logged_By || null,
|
||||
});
|
||||
|
||||
updateLastTouched(record.Account_Name, record.Activity_Date);
|
||||
}
|
||||
|
||||
export function importData(data: {
|
||||
accounts?: Record<string, string | null>[];
|
||||
pipeline?: Record<string, string | null>[];
|
||||
activities?: Record<string, string | null>[];
|
||||
implementations?: Record<string, string | null>[];
|
||||
metricTargets?: Record<string, string | null>[];
|
||||
}): Record<string, number> {
|
||||
const db = getDb();
|
||||
const counts: Record<string, number> = {};
|
||||
|
||||
const tx = db.transaction(() => {
|
||||
if (data.accounts?.length) {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO accounts (Account_Name, District_Name, Tier, Priority, AgentMinder_Status, Current_ARR_USD, Touch_Count, Date_First_Touched, Date_Last_Touched, MAP_In_Place_YN, Company_URL, Area_Sales_Leader, DM, AD, Logo_URL, Next_Renewal_Date, Next_Renewal_EAR, Anchor_Contract_Date, Anchor_Contract_EAR)
|
||||
VALUES (@Account_Name, @District_Name, @Tier, @Priority, @AgentMinder_Status, @Current_ARR_USD, @Touch_Count, @Date_First_Touched, @Date_Last_Touched, @MAP_In_Place_YN, @Company_URL, @Area_Sales_Leader, @DM, @AD, @Logo_URL, @Next_Renewal_Date, @Next_Renewal_EAR, @Anchor_Contract_Date, @Anchor_Contract_EAR)
|
||||
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,
|
||||
Touch_Count=excluded.Touch_Count, Date_First_Touched=excluded.Date_First_Touched,
|
||||
Date_Last_Touched=excluded.Date_Last_Touched, 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, Logo_URL=excluded.Logo_URL,
|
||||
Next_Renewal_Date=excluded.Next_Renewal_Date, Next_Renewal_EAR=excluded.Next_Renewal_EAR,
|
||||
Anchor_Contract_Date=excluded.Anchor_Contract_Date, Anchor_Contract_EAR=excluded.Anchor_Contract_EAR
|
||||
`);
|
||||
let c = 0;
|
||||
for (const r of data.accounts) {
|
||||
if (!r.Account_Name) continue;
|
||||
stmt.run(r);
|
||||
c++;
|
||||
}
|
||||
counts.accounts = c;
|
||||
}
|
||||
|
||||
if (data.pipeline?.length) {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO pipeline (Opportunity_ID, Account_Name, District_Name, Stage, Forecast_Category, Amount_USD, Closed_Amount_USD, Probability_Pct, Created_Date, Expected_Close_Date, Closed_Date, Champion_Name, Economic_Buyer, Next_Step, Next_Step_Date, Primary_Objection, Competitor, Source_Play, Product, Deal_Type, Status, Stage_Entered_Date, Tier, MAP_In_Place_YN)
|
||||
VALUES (@Opportunity_ID, @Account_Name, @District_Name, @Stage, @Forecast_Category, @Amount_USD, @Closed_Amount_USD, @Probability_Pct, @Created_Date, @Expected_Close_Date, @Closed_Date, @Champion_Name, @Economic_Buyer, @Next_Step, @Next_Step_Date, @Primary_Objection, @Competitor, @Source_Play, @Product, @Deal_Type, @Status, @Stage_Entered_Date, @Tier, @MAP_In_Place_YN)
|
||||
ON CONFLICT(Opportunity_ID) DO UPDATE SET
|
||||
Account_Name=excluded.Account_Name, District_Name=excluded.District_Name, Stage=excluded.Stage,
|
||||
Forecast_Category=excluded.Forecast_Category, Amount_USD=excluded.Amount_USD,
|
||||
Closed_Amount_USD=excluded.Closed_Amount_USD, Probability_Pct=excluded.Probability_Pct,
|
||||
Created_Date=excluded.Created_Date, Expected_Close_Date=excluded.Expected_Close_Date,
|
||||
Closed_Date=excluded.Closed_Date, Champion_Name=excluded.Champion_Name,
|
||||
Economic_Buyer=excluded.Economic_Buyer, Next_Step=excluded.Next_Step,
|
||||
Next_Step_Date=excluded.Next_Step_Date, Primary_Objection=excluded.Primary_Objection,
|
||||
Competitor=excluded.Competitor, Source_Play=excluded.Source_Play, Product=excluded.Product,
|
||||
Deal_Type=excluded.Deal_Type, Status=excluded.Status, Stage_Entered_Date=excluded.Stage_Entered_Date,
|
||||
Tier=excluded.Tier, MAP_In_Place_YN=excluded.MAP_In_Place_YN
|
||||
`);
|
||||
let c = 0;
|
||||
for (const r of data.pipeline) {
|
||||
if (!r.Opportunity_ID) continue;
|
||||
stmt.run(r);
|
||||
c++;
|
||||
}
|
||||
counts.pipeline = c;
|
||||
}
|
||||
|
||||
if (data.activities?.length) {
|
||||
db.prepare('DELETE FROM activities').run();
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO activities (Activity_ID, Activity_Date, Activity_Type, Account_Name, District_Name, Contact_Name, Notes, Play, Channel, Persona, Outcome, Logged_By)
|
||||
VALUES (@Activity_ID, @Activity_Date, @Activity_Type, @Account_Name, @District_Name, @Contact_Name, @Notes, @Play, @Channel, @Persona, @Outcome, @Logged_By)
|
||||
`);
|
||||
let c = 0;
|
||||
for (const r of data.activities) {
|
||||
if (!r.Account_Name || !r.Activity_Date) continue;
|
||||
stmt.run(r);
|
||||
c++;
|
||||
}
|
||||
counts.activities = c;
|
||||
}
|
||||
|
||||
if (data.implementations?.length) {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO implementations (Implementation_ID, Opportunity_ID, Account_Name, District_Name, Tier, Closed_Date, Closed_Amount_USD, Kickoff_Date, Go_Live_Target, Go_Live_Actual, Deployment_Status, Health_RYG, Milestones_Complete, Milestones_Total, Onboarding_Owner, Risk_Notes)
|
||||
VALUES (@Implementation_ID, @Opportunity_ID, @Account_Name, @District_Name, @Tier, @Closed_Date, @Closed_Amount_USD, @Kickoff_Date, @Go_Live_Target, @Go_Live_Actual, @Deployment_Status, @Health_RYG, @Milestones_Complete, @Milestones_Total, @Onboarding_Owner, @Risk_Notes)
|
||||
ON CONFLICT(Implementation_ID) DO UPDATE SET
|
||||
Opportunity_ID=excluded.Opportunity_ID, Account_Name=excluded.Account_Name,
|
||||
District_Name=excluded.District_Name, Tier=excluded.Tier,
|
||||
Closed_Date=excluded.Closed_Date, Closed_Amount_USD=excluded.Closed_Amount_USD,
|
||||
Kickoff_Date=excluded.Kickoff_Date, Go_Live_Target=excluded.Go_Live_Target,
|
||||
Go_Live_Actual=excluded.Go_Live_Actual, Deployment_Status=excluded.Deployment_Status,
|
||||
Health_RYG=excluded.Health_RYG, Milestones_Complete=excluded.Milestones_Complete,
|
||||
Milestones_Total=excluded.Milestones_Total, Onboarding_Owner=excluded.Onboarding_Owner,
|
||||
Risk_Notes=excluded.Risk_Notes
|
||||
`);
|
||||
let c = 0;
|
||||
for (const r of data.implementations) {
|
||||
if (!r.Implementation_ID || !r.Account_Name) continue;
|
||||
stmt.run(r);
|
||||
c++;
|
||||
}
|
||||
counts.implementations = c;
|
||||
}
|
||||
|
||||
if (data.metricTargets?.length) {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO metric_targets (Target_ID, Period_Type, Period_Label, District_Name, Metric, Target_Value, Notes)
|
||||
VALUES (@Target_ID, @Period_Type, @Period_Label, @District_Name, @Metric, @Target_Value, @Notes)
|
||||
ON CONFLICT(Target_ID) DO UPDATE SET
|
||||
Period_Type=excluded.Period_Type, Period_Label=excluded.Period_Label,
|
||||
District_Name=excluded.District_Name, Metric=excluded.Metric,
|
||||
Target_Value=excluded.Target_Value, Notes=excluded.Notes
|
||||
`);
|
||||
let c = 0;
|
||||
for (const r of data.metricTargets) {
|
||||
if (!r.Target_ID) continue;
|
||||
stmt.run(r);
|
||||
c++;
|
||||
}
|
||||
counts.metricTargets = c;
|
||||
}
|
||||
});
|
||||
|
||||
tx();
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function deleteAccount(name: string) {
|
||||
const db = getDb();
|
||||
db.prepare('DELETE FROM accounts WHERE Account_Name = ?').run(name);
|
||||
}
|
||||
|
||||
export function deletePipeline(id: string) {
|
||||
const db = getDb();
|
||||
db.prepare('DELETE FROM pipeline WHERE Opportunity_ID = ?').run(id);
|
||||
}
|
||||
|
||||
export function deleteActivity(id: number) {
|
||||
const db = getDb();
|
||||
db.prepare('DELETE FROM activities WHERE id = ?').run(id);
|
||||
}
|
||||
|
||||
export function deleteTarget(accountName: string) {
|
||||
const db = getDb();
|
||||
db.prepare('DELETE FROM implementations WHERE Account_Name = ?').run(accountName);
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
`).run({
|
||||
Account_Name: record.Account_Name || '',
|
||||
District_Name: record.District_Name || '',
|
||||
Tier: record.Tier || '',
|
||||
Priority: record.Priority || '',
|
||||
AgentMinder_Status: record.AgentMinder_Status || 'Not Touched',
|
||||
Current_ARR_USD: record.Current_ARR_USD ?? null,
|
||||
MAP_In_Place_YN: record.MAP_In_Place_YN ?? null,
|
||||
Company_URL: record.Company_URL ?? null,
|
||||
Area_Sales_Leader: record.Area_Sales_Leader ?? null,
|
||||
DM: record.DM ?? null,
|
||||
AD: record.AD ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
export function upsertPipeline(record: Record<string, unknown>) {
|
||||
const db = getDb();
|
||||
db.prepare(`INSERT INTO pipeline (Opportunity_ID, Account_Name, District_Name, Stage, Forecast_Category, Amount_USD, Closed_Amount_USD, Probability_Pct, Created_Date, Expected_Close_Date, Closed_Date, Champion_Name, Economic_Buyer, Next_Step, Next_Step_Date, Primary_Objection, Competitor, Source_Play, Product)
|
||||
VALUES (@Opportunity_ID, @Account_Name, @District_Name, @Stage, @Forecast_Category, @Amount_USD, @Closed_Amount_USD, @Probability_Pct, @Created_Date, @Expected_Close_Date, @Closed_Date, @Champion_Name, @Economic_Buyer, @Next_Step, @Next_Step_Date, @Primary_Objection, @Competitor, @Source_Play, @Product)
|
||||
ON CONFLICT(Opportunity_ID) DO UPDATE SET
|
||||
Account_Name=excluded.Account_Name, District_Name=excluded.District_Name, Stage=excluded.Stage,
|
||||
Forecast_Category=excluded.Forecast_Category, Amount_USD=excluded.Amount_USD,
|
||||
Closed_Amount_USD=excluded.Closed_Amount_USD, Probability_Pct=excluded.Probability_Pct,
|
||||
Expected_Close_Date=excluded.Expected_Close_Date, Closed_Date=excluded.Closed_Date,
|
||||
Champion_Name=excluded.Champion_Name, Economic_Buyer=excluded.Economic_Buyer,
|
||||
Next_Step=excluded.Next_Step, Next_Step_Date=excluded.Next_Step_Date,
|
||||
Primary_Objection=excluded.Primary_Objection, Competitor=excluded.Competitor,
|
||||
Source_Play=excluded.Source_Play
|
||||
`).run({
|
||||
Opportunity_ID: record.Opportunity_ID || '',
|
||||
Account_Name: record.Account_Name || '',
|
||||
District_Name: record.District_Name || '',
|
||||
Stage: record.Stage || '01-Qualified',
|
||||
Forecast_Category: record.Forecast_Category || 'Pipeline',
|
||||
Amount_USD: record.Amount_USD ?? 0,
|
||||
Closed_Amount_USD: record.Closed_Amount_USD ?? null,
|
||||
Probability_Pct: record.Probability_Pct ?? 0,
|
||||
Created_Date: record.Created_Date || new Date().toISOString().split('T')[0],
|
||||
Expected_Close_Date: record.Expected_Close_Date || '',
|
||||
Closed_Date: record.Closed_Date ?? null,
|
||||
Champion_Name: record.Champion_Name ?? null,
|
||||
Economic_Buyer: record.Economic_Buyer ?? null,
|
||||
Next_Step: record.Next_Step ?? null,
|
||||
Next_Step_Date: record.Next_Step_Date ?? null,
|
||||
Primary_Objection: record.Primary_Objection ?? null,
|
||||
Competitor: record.Competitor ?? null,
|
||||
Source_Play: record.Source_Play ?? null,
|
||||
Product: record.Product || 'AgentMinder',
|
||||
});
|
||||
}
|
||||
|
||||
export function upsertTarget(record: Record<string, unknown>) {
|
||||
const db = getDb();
|
||||
const id = (record.Implementation_ID as string) || `IMPL-${Date.now()}`;
|
||||
db.prepare(`INSERT INTO implementations (Implementation_ID, Account_Name, District_Name, Deployment_Status, Health_RYG, Go_Live_Target, Go_Live_Actual, Risk_Notes)
|
||||
VALUES (@Implementation_ID, @Account_Name, @District_Name, @Deployment_Status, @Health_RYG, @Go_Live_Target, @Go_Live_Actual, @Risk_Notes)
|
||||
ON CONFLICT(Implementation_ID) DO UPDATE SET
|
||||
Account_Name=excluded.Account_Name, District_Name=excluded.District_Name,
|
||||
Deployment_Status=excluded.Deployment_Status, Health_RYG=excluded.Health_RYG,
|
||||
Go_Live_Target=excluded.Go_Live_Target, Go_Live_Actual=excluded.Go_Live_Actual,
|
||||
Risk_Notes=excluded.Risk_Notes
|
||||
`).run({
|
||||
Implementation_ID: id,
|
||||
Account_Name: record.Account_Name || '',
|
||||
District_Name: record.District_Name ?? null,
|
||||
Deployment_Status: record.Deployment_Status || record.Implementation_Stage || 'Not Started',
|
||||
Health_RYG: record.Health_RYG || record.Health_Status || null,
|
||||
Go_Live_Target: record.Go_Live_Target || record.Go_Live_Date || null,
|
||||
Go_Live_Actual: record.Go_Live_Actual ?? null,
|
||||
Risk_Notes: record.Risk_Notes || record.Notes || null,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user