Add planned activities and follow-ups workflow

Activities with future dates are automatically tagged as Planned. Once
the date passes they become Pending Follow-Ups requiring resolution.

- Added Status column to activities (Planned/Completed/Did Not Occur)
- Built Follow-Ups subview with expandable cards, outcome capture,
  and Create Next Step workflow for scheduling follow-on activities
- Upcoming Planned table shows scheduled future activities
- Executive Overview gains Meetings Scheduled and Follow-Ups Pending KPIs
- Planned/Follow-Up badges appear in the activity detail table
- Admin page includes Status field for activities

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 10:22:20 -04:00
parent 4e2e847828
commit 1a1cf7e652
7 changed files with 409 additions and 12 deletions

View File

@@ -52,6 +52,12 @@ function migrateSchema(db: Database.Database) {
if (!colNames.has('CPM')) {
db.exec("ALTER TABLE accounts ADD COLUMN CPM TEXT DEFAULT NULL");
}
const actCols = db.prepare("PRAGMA table_info(activities)").all() as { name: string }[];
const actColNames = new Set(actCols.map(c => c.name));
if (!actColNames.has('Status')) {
db.exec("ALTER TABLE activities ADD COLUMN Status TEXT DEFAULT NULL");
}
}
function initSchema(db: Database.Database) {
@@ -120,7 +126,8 @@ function initSchema(db: Database.Database) {
Channel TEXT,
Persona TEXT,
Outcome TEXT,
Logged_By TEXT
Logged_By TEXT,
Status TEXT
);
CREATE TABLE IF NOT EXISTS implementations (
@@ -243,7 +250,7 @@ export function getDashboardData(): DashboardData {
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 })[],
activities: db.prepare('SELECT id, Activity_Date, Activity_Type, Account_Name, District_Name, Contact_Name, Notes, Play, Channel, Persona, Outcome, Logged_By, Status 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[],
@@ -346,11 +353,15 @@ export function addActivity(record: {
Persona?: string | null;
Outcome?: string | null;
Logged_By?: string | null;
Status?: string | null;
}) {
const db = getDb();
const today = new Date().toISOString().split('T')[0];
const status = record.Status || (record.Activity_Date > today ? 'Planned' : null);
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)
INSERT INTO activities (Activity_Date, Activity_Type, Account_Name, District_Name, Contact_Name, Notes, Play, Channel, Persona, Outcome, Logged_By, Status)
VALUES (@Activity_Date, @Activity_Type, @Account_Name, @District_Name, @Contact_Name, @Notes, @Play, @Channel, @Persona, @Outcome, @Logged_By, @Status)
`).run({
Activity_Date: record.Activity_Date,
Activity_Type: record.Activity_Type,
@@ -363,9 +374,27 @@ export function addActivity(record: {
Persona: record.Persona || null,
Outcome: record.Outcome || null,
Logged_By: record.Logged_By || null,
Status: status,
});
updateLastTouched(record.Account_Name, record.Activity_Date);
if (!status || status !== 'Planned') {
updateLastTouched(record.Account_Name, record.Activity_Date);
}
}
export function updateActivityStatus(id: number, updates: { Status: string; Outcome?: string | null; Notes?: string | null }) {
const db = getDb();
const params: Record<string, unknown> = { id, Status: updates.Status };
const sets = ['Status = @Status'];
if (updates.Outcome !== undefined) { sets.push('Outcome = @Outcome'); params.Outcome = updates.Outcome ?? null; }
if (updates.Notes !== undefined) { sets.push('Notes = @Notes'); params.Notes = updates.Notes ?? null; }
db.prepare(`UPDATE activities SET ${sets.join(', ')} WHERE id = @id`).run(params);
if (updates.Status === 'Completed') {
const act = db.prepare('SELECT Account_Name, Activity_Date FROM activities WHERE id = ?').get(id) as { Account_Name: string; Activity_Date: string } | undefined;
if (act) updateLastTouched(act.Account_Name, act.Activity_Date);
}
}
export function importData(data: {