Retire contacts.contact_type; derive Contacts status from the grid (v0.1.0:106)
The Investors/Prospects distinction is now derived live from the canonical grid (contact_grid_signals -> committed/pipeline_stage), not the mechanically set contact_type column: - Desktop Contacts: drop the Investors/Prospects tabs + TYPE badge; show a derived Status (existing-LP badge + pipeline stage chip). - Dashboard: repoint Total LPs / Prospects onto fundraising_investors entities (committed>0 vs $0, graveyard + blank-row placeholder excluded); fix a total_contacts soft-delete leak. - Stop reading/writing contact_type across the create/update/import/sync paths. The column is left inert in place; a physical drop is deferred to a later signed-off table-rebuild migration (SQLite no-drop-column; contacts is FK-referenced) -- same retire-then-drop path lp_profiles took.
This commit is contained in:
+34
-23
@@ -161,6 +161,11 @@ def init_db():
|
||||
mobile TEXT,
|
||||
title TEXT,
|
||||
organization_id TEXT REFERENCES organizations(id) ON DELETE SET NULL,
|
||||
-- RETIRED (v0.1.0:106), inert: the Investors/Prospects distinction is now derived live
|
||||
-- from the grid (contact_grid_signals → committed/pipeline_stage), not this column. No
|
||||
-- code reads or writes it; the DEFAULT keeps NOT NULL satisfied for inserts that omit it.
|
||||
-- Physical drop deferred to a signed-off table-rebuild migration (SQLite has no DROP COLUMN
|
||||
-- here; contacts is FK-referenced) — same retire-then-drop path lp_profiles took (v78→v104).
|
||||
contact_type TEXT NOT NULL DEFAULT 'prospect',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
source TEXT,
|
||||
@@ -840,7 +845,7 @@ def _upsert_contact_from_fundraising(conn, investor_name, contact, actor_user_id
|
||||
conn.execute("""
|
||||
UPDATE contacts
|
||||
SET first_name = ?, last_name = ?, email = ?, title = ?,
|
||||
organization_id = ?, source = ?, contact_type = 'investor', city = ?, state = ?, country = ?, location_query = ?, linkedin_url = ?, phone = ?, mobile = ?, updated_at = ?
|
||||
organization_id = ?, source = ?, city = ?, state = ?, country = ?, location_query = ?, linkedin_url = ?, phone = ?, mobile = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""", (next_first, next_last, next_email, next_title, next_org, next_source, next_city, next_state, next_country, next_location_query, next_linkedin, next_phone, next_mobile, now(), existing['id']))
|
||||
return existing['id']
|
||||
@@ -848,8 +853,8 @@ def _upsert_contact_from_fundraising(conn, investor_name, contact, actor_user_id
|
||||
contact_id = generate_id()
|
||||
conn.execute("""
|
||||
INSERT INTO contacts (
|
||||
id, first_name, last_name, email, title, organization_id, source, contact_type, status, city, state, country, location_query, linkedin_url, phone, mobile, created_by, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, 'investor', 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
id, first_name, last_name, email, title, organization_id, source, status, city, state, country, location_query, linkedin_url, phone, mobile, created_by, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
contact_id,
|
||||
first_name or 'Unknown',
|
||||
@@ -2724,9 +2729,6 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
"""
|
||||
args = []
|
||||
|
||||
if params.get('type'):
|
||||
query += " AND c.contact_type = ?"
|
||||
args.append(params['type'])
|
||||
if params.get('status'):
|
||||
query += " AND c.status = ?"
|
||||
args.append(params['status'])
|
||||
@@ -2743,7 +2745,7 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
|
||||
sort = params.get('sort', 'updated_at')
|
||||
order = 'DESC' if params.get('order', 'desc').lower() == 'desc' else 'ASC'
|
||||
allowed_sorts = ['first_name', 'last_name', 'email', 'created_at', 'updated_at', 'contact_type', 'source']
|
||||
allowed_sorts = ['first_name', 'last_name', 'email', 'created_at', 'updated_at', 'source']
|
||||
if sort in allowed_sorts:
|
||||
query += f" ORDER BY c.{sort} {order}"
|
||||
else:
|
||||
@@ -2832,14 +2834,14 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
tags = json.dumps(body.get('tags', []))
|
||||
conn.execute("""
|
||||
INSERT INTO contacts (id, first_name, last_name, email, phone, mobile, title,
|
||||
organization_id, contact_type, status, source, tags, notes, linkedin_url,
|
||||
organization_id, status, source, tags, notes, linkedin_url,
|
||||
city, state, country, location_query, preferred_contact, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
contact_id, body['first_name'], body['last_name'],
|
||||
body.get('email'), body.get('phone'), body.get('mobile'),
|
||||
body.get('title'), organization_id,
|
||||
body.get('contact_type', 'prospect'), body.get('status', 'active'),
|
||||
body.get('status', 'active'),
|
||||
body.get('source'), tags, body.get('notes'),
|
||||
body.get('linkedin_url'), body.get('city'), body.get('state'),
|
||||
body.get('country'), body.get('location_query'),
|
||||
@@ -2871,7 +2873,7 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
previous_contact = row_to_dict(existing)
|
||||
|
||||
updatable = ['first_name', 'last_name', 'email', 'phone', 'mobile', 'title',
|
||||
'organization_id', 'contact_type', 'status', 'source', 'notes',
|
||||
'organization_id', 'status', 'source', 'notes',
|
||||
'linkedin_url', 'city', 'state', 'country', 'location_query', 'preferred_contact']
|
||||
sets = []
|
||||
args = []
|
||||
@@ -4196,10 +4198,21 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
def handle_dashboard_report(self, user):
|
||||
conn = get_db()
|
||||
|
||||
# Key metrics
|
||||
total_lps = conn.execute("SELECT COUNT(*) as c FROM contacts WHERE contact_type = 'investor'").fetchone()['c']
|
||||
total_prospects = conn.execute("SELECT COUNT(*) as c FROM contacts WHERE contact_type = 'prospect'").fetchone()['c']
|
||||
total_contacts = conn.execute("SELECT COUNT(*) as c FROM contacts").fetchone()['c']
|
||||
# Key metrics. "Total LPs" / "Prospects" are derived from the canonical grid
|
||||
# (investor entities), not the retired contacts.contact_type: an LP is a grid
|
||||
# investor with committed capital (total_invested > 0); a prospect is a live
|
||||
# grid row with nothing committed yet. Graveyarded rows are excluded (same basis
|
||||
# as Total Committed below); blank grid rows (synced as the 'Untitled Investor'
|
||||
# placeholder, see sync_fundraising_relational) are excluded from the prospect
|
||||
# count so empties don't inflate it.
|
||||
total_lps = conn.execute(
|
||||
"SELECT COUNT(*) as c FROM fundraising_investors WHERE total_invested > 0 AND graveyard = 0"
|
||||
).fetchone()['c']
|
||||
total_prospects = conn.execute(
|
||||
"SELECT COUNT(*) as c FROM fundraising_investors "
|
||||
"WHERE COALESCE(total_invested, 0) = 0 AND graveyard = 0 AND investor_name != 'Untitled Investor'"
|
||||
).fetchone()['c']
|
||||
total_contacts = conn.execute("SELECT COUNT(*) as c FROM contacts WHERE deleted_at IS NULL").fetchone()['c']
|
||||
|
||||
# Committed capital comes from the canonical fundraising grid (per-investor
|
||||
# rollup of per-fund commitments). Graveyarded (written-off) investors are
|
||||
@@ -4495,7 +4508,6 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
conn.execute("""
|
||||
UPDATE contacts SET first_name=?, last_name=?, phone=?, title=?,
|
||||
organization_id=COALESCE(?, organization_id),
|
||||
contact_type=COALESCE(?, contact_type),
|
||||
linkedin_url=COALESCE(?, linkedin_url),
|
||||
city=COALESCE(?, city),
|
||||
state=COALESCE(?, state),
|
||||
@@ -4505,7 +4517,6 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
WHERE id=?
|
||||
""", (first_name, last_name, data.get('phone'),
|
||||
data.get('title'), org_id,
|
||||
data.get('contact_type'),
|
||||
linkedin_url if linkedin_url else None,
|
||||
city if city else None,
|
||||
state if state else None,
|
||||
@@ -4530,12 +4541,12 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
contact_id = generate_id()
|
||||
conn.execute("""
|
||||
INSERT INTO contacts (id, first_name, last_name, email, phone,
|
||||
title, organization_id, contact_type, status, source,
|
||||
title, organization_id, status, source,
|
||||
linkedin_url, city, state, country, location_query, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'active', 'import', ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'active', 'import', ?, ?, ?, ?, ?, ?)
|
||||
""", (contact_id, first_name, last_name, email,
|
||||
data.get('phone'), data.get('title'), org_id,
|
||||
data.get('contact_type', 'prospect'), linkedin_url,
|
||||
linkedin_url,
|
||||
city, state, country, location_query, user['user_id']))
|
||||
if email:
|
||||
batch_email_matches[email_key] = {
|
||||
@@ -4558,12 +4569,12 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
contact_id = generate_id()
|
||||
conn.execute("""
|
||||
INSERT INTO contacts (id, first_name, last_name, email, phone,
|
||||
title, organization_id, contact_type, status, source,
|
||||
title, organization_id, status, source,
|
||||
linkedin_url, city, state, country, location_query, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'active', 'import', ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'active', 'import', ?, ?, ?, ?, ?, ?)
|
||||
""", (contact_id, first_name, last_name, email,
|
||||
data.get('phone'), data.get('title'), org_id,
|
||||
data.get('contact_type', 'prospect'), linkedin_url,
|
||||
linkedin_url,
|
||||
city, state, country, location_query, user['user_id']))
|
||||
if email:
|
||||
batch_email_matches[email_key] = {
|
||||
|
||||
Reference in New Issue
Block a user