Mobile Phase 8c: Grid-detail notes timeline + top-bar quick-log pencil

Grid detail (G6): replace the single row.notes blob with a NoteTimeline fed by
a new investor-level read, GET /api/communications?source_row_id=<grid row id>
(filter maps the grid row -> fundraising_investors.source_row_id ->
fundraising_contacts.contact_id -> communications, soft-delete-respecting;
cancelled-flag fetch + commsReload after a log). Note-logging now uses the
shared LogCommunicationSheet, retiring the bespoke noteForm select sheet and the
dead .fs-note-log style.

New MobileQuickLog: a shell mobile-top-bar pencil reachable from every tab —
two-step sheet (pick investor via search + recent-first pool -> inline log form)
writing through the one-row /api/fundraising/log-communication path.

source_row_id and contact_id are kept mutually exclusive in
handle_list_communications so a future caller passing both can't get the empty
intersection. Guarded by test_grid_comm_timeline.py (cross-contact aggregation,
investor isolation, soft-delete); 39/39 backend green.
This commit is contained in:
Keysat
2026-06-19 21:43:05 -05:00
parent e57b154a6d
commit 93ac0c240f
4 changed files with 373 additions and 32 deletions
+14
View File
@@ -3238,6 +3238,20 @@ class CRMHandler(BaseHTTPRequestHandler):
if params.get('contact_id'):
query += " AND cm.contact_id = ?"
args.append(params['contact_id'])
# source_row_id is an investor-scope filter; contact_id is a contact-scope filter. They are
# never combined by any caller (the /contacts/{id}/communications subpath sets contact_id;
# the mobile Grid detail sets source_row_id) — keep them mutually exclusive so a future
# caller passing both can't silently get the empty intersection.
elif params.get('source_row_id'):
# Investor-level timeline for a fundraising-grid row (the mobile Grid detail): map the
# grid JSON row id → its canonical contacts via the relational mirror, then return every
# communication across those contacts. cm.deleted_at is still filtered above (soft-delete).
query += """ AND cm.contact_id IN (
SELECT fc.contact_id FROM fundraising_contacts fc
JOIN fundraising_investors fi ON fc.investor_id = fi.id
WHERE fi.source_row_id = ? AND fc.contact_id IS NOT NULL
)"""
args.append(params['source_row_id'])
if params.get('type'):
query += " AND cm.type = ?"
args.append(params['type'])