Skip to content

Add Sortable 'Last Contacted At' Column to Leads Table (Closes #486, LO-111) - #647

Open
GeethaBurigalla wants to merge 1 commit into
Kuldeeep18:mainfrom
GeethaBurigalla:lo-111-sortable-last-contacted
Open

Add Sortable 'Last Contacted At' Column to Leads Table (Closes #486, LO-111)#647
GeethaBurigalla wants to merge 1 commit into
Kuldeeep18:mainfrom
GeethaBurigalla:lo-111-sortable-last-contacted

Conversation

@GeethaBurigalla

@GeethaBurigalla GeethaBurigalla commented Jul 11, 2026

Copy link
Copy Markdown

Summary

Adds a last_contacted_at column to the leads table, sortable via the API,
so users can see and sort by when a lead was most recently contacted.

Closes #486 (LO-111)

Backend

  • CampaignLead.last_sent_at: new field + migration, tracking the timestamp
    of the most recent message/call actually delivered to a lead on a given
    campaign enrollment.
  • Stamped on:
    • a successful email send (alongside last_sent_message_id)
    • a successful SMS send
    • a call attempt — both an automated Twilio call and the manual-task
      fallback when no Twilio credentials are configured, since both represent
      the lead actually being contacted. Not stamped on a genuine send failure.
  • LeadViewSet.get_queryset annotates last_contacted_at via
    Max('campaigns__last_sent_at') and supports
    ?ordering=last_contacted_at / ?ordering=-last_contacted_at, with
    never-contacted leads sorted last in both directions.
  • LeadSerializer exposes last_contacted_at as a SerializerMethodField
    rather than a plain DateTimeField, so it degrades to None for
    unannotated Lead instances (e.g. the object returned from create/update)
    instead of raising AttributeError.

Frontend (leads.html)

  • New "Last Contacted" column header with a 3-state sort toggle
    (desc → asc → unsorted) and an icon reflecting the current state.
  • New cell rendering the formatted timestamp or "Never".
  • Wires ordering into getFilterParams so sort composes with existing
    filters; resets on Clear Filters.

Testing

Ran the full leads and campaigns test suites locally — all 85 tests pass.
Also manually verified the annotation/ordering/serializer behavior end-to-end
(descending puts most-recent first, ascending reverses it, nulls always last,
serializer returns None safely for plain instances).

Aside

Noticed beautifulsoup4 is imported in campaigns/tasks.py but isn't listed
in requirements.txt — not part of this PR, just flagging in case it's worth
a quick separate fix.

Summary by CodeRabbit

  • New Features
    • Added a Last Contacted column to the leads table.
    • Added sorting by most recently or least recently contacted, with a reset option.
    • Leads now display the latest successful message or call time, or Never if no contact has occurred.
  • Improvements
    • Contact timestamps are recorded after successful SMS, email, and call activity.
    • Lead filtering and sorting now preserve accurate contact history.

…8#486, LO-111)

Backend:
- CampaignLead.last_sent_at: new field + migration, tracking the
  timestamp of the most recent message/call actually delivered to a
  lead on a given campaign enrollment.
- Stamped on successful email send (alongside last_sent_message_id),
  successful SMS send, and call attempts (both automated Twilio calls
  and the manual-task fallback when no Twilio creds are configured —
  both represent a real contact attempt). Not stamped on a genuine
  send failure.
- LeadViewSet.get_queryset annotates last_contacted_at via
  Max('campaigns__last_sent_at') and supports
  ?ordering=last_contacted_at / -last_contacted_at, with
  never-contacted leads sorted last in both directions.
- LeadSerializer exposes last_contacted_at as a SerializerMethodField
  rather than a plain DateTimeField, so it degrades to None for
  unannotated Lead instances (e.g. the object returned from
  create/update) instead of raising AttributeError.

Frontend (leads.html):
- New 'Last Contacted' column header with a 3-state sort toggle
  (desc -> asc -> unsorted) and an icon reflecting current state.
- New cell rendering the formatted timestamp or 'Never'.
- Wires 'ordering' into getFilterParams so sort composes with
  existing filters; resets on Clear Filters.

Verified: all 85 existing backend tests pass; manual sanity check
confirms annotation, both sort directions (nulls last), and
serializer null-safety.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Campaign sends now persist the latest contact timestamp. The leads API aggregates and sorts by that timestamp, and the leads table displays it with a three-state sort control.

Changes

Last Contacted Tracking

Layer / File(s) Summary
Record contact timestamps
backend/campaigns/migrations/..., backend/campaigns/models.py, backend/campaigns/tasks.py
Adds nullable CampaignLead.last_sent_at and updates it after successful SMS, manual calls, and email sends.
Expose aggregated contact dates
backend/leads/views.py, backend/leads/serializers.py
Annotates leads with the maximum related last_sent_at, supports ascending or descending ordering, and exposes last_contacted_at.
Display and sort contacts
frontend/leads.html
Adds the Last Contacted column, formats missing values as “Never,” sends ordering parameters, cycles sort states, and resets sorting when filters are cleared.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LeadsTable
  participant LeadViewSet
  participant CampaignLead
  LeadsTable->>LeadViewSet: Request ordering=last_contacted_at
  LeadViewSet->>CampaignLead: Aggregate Max(last_sent_at)
  CampaignLead-->>LeadViewSet: Latest contact timestamp
  LeadViewSet-->>LeadsTable: Ordered leads with last_contacted_at
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main user-facing change: a sortable Last Contacted At column in the leads table.
Linked Issues check ✅ Passed The PR adds last_contacted_at annotation, serializer support, ordering, and a sortable table column as requested.
Out of Scope Changes check ✅ Passed The changes stay focused on tracking, exposing, and sorting lead contact timestamps for the requested table column.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
backend/campaigns/models.py (1)

135-138: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider adding a database index on last_sent_at.

The Max('campaigns__last_sent_at') annotation in LeadViewSet.get_queryset runs on every leads list request. A composite index on (lead_id, last_sent_at) would let the database satisfy the MAX subquery via an index-only scan instead of scanning all CampaignLead rows per lead.

♻️ Optional: add db_index to last_sent_at
     last_sent_at = models.DateTimeField(
-        null=True, blank=True,
+        null=True, blank=True, db_index=True,
         help_text='Timestamp of the most recent message/call actually sent to the lead on this campaign.',
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/campaigns/models.py` around lines 135 - 138, Update the last_sent_at
field in the CampaignLead model to add a database index, supporting the
Max('campaigns__last_sent_at') annotation used by LeadViewSet.get_queryset. If
the project supports composite indexes, add the appropriate (lead_id,
last_sent_at) index through the model’s Meta configuration and generate the
corresponding migration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@backend/campaigns/models.py`:
- Around line 135-138: Update the last_sent_at field in the CampaignLead model
to add a database index, supporting the Max('campaigns__last_sent_at')
annotation used by LeadViewSet.get_queryset. If the project supports composite
indexes, add the appropriate (lead_id, last_sent_at) index through the model’s
Meta configuration and generate the corresponding migration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d118a5f-e939-42e1-86ae-f999c977ee88

📥 Commits

Reviewing files that changed from the base of the PR and between 4a33158 and c83a033.

📒 Files selected for processing (6)
  • backend/campaigns/migrations/0011_campaignlead_last_sent_at.py
  • backend/campaigns/models.py
  • backend/campaigns/tasks.py
  • backend/leads/serializers.py
  • backend/leads/views.py
  • frontend/leads.html

@GeethaBurigalla

Copy link
Copy Markdown
Author

Hi! I've submitted a PR (#647) for issue #486 (LO-111) — adds a sortable "Last Contacted At" column to the leads table, both backend and frontend .
Ran the full leads and campaigns test suites locally — all 85 pass. Happy to make any changes you'd like in review. Thanks for taking a look!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LO-111 [Intermediate]: Add Sortable 'Last Contacted At' Column to Leads Table

1 participant