Shurem commited on
Commit
6d7b014
·
1 Parent(s): ac8697d

added fresh screens

Browse files
alembic/versions/0015_starred_at.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """leads.starred_at
2
+
3
+ "Star" priority mark (user, 2026-09-23): "agar humyn us lead ko priority deni ho toh sales team star mark
4
+ kr dy... yeh sirf mark hai koi action nhi" -- a rep flags a lead as worth prioritizing. Tag only, same as
5
+ call_picked_by (revision 0013): never changes pipeline_stage or queue position, not logged as a
6
+ call_events row. Pipeline shows/filters by it; a starred card renders with a blue accent.
7
+
8
+ Nullable, no default, no backfill: every existing lead is unaffected (not starred).
9
+
10
+ Revision ID: 0015_starred_at
11
+ Revises: 0014_hidden_and_callback
12
+ Create Date: 2026-09-23
13
+ """
14
+
15
+ from typing import Sequence, Union
16
+
17
+ import sqlalchemy as sa
18
+ from alembic import op
19
+
20
+ revision: str = "0015_starred_at"
21
+ down_revision: Union[str, None] = "0014_hidden_and_callback"
22
+ branch_labels: Union[str, Sequence[str], None] = None
23
+ depends_on: Union[str, Sequence[str], None] = None
24
+
25
+
26
+ def upgrade() -> None:
27
+ op.add_column("leads", sa.Column("starred_at", sa.DateTime(), nullable=True))
28
+ op.create_index("idx_leads_starred", "leads", ["starred_at"])
29
+
30
+
31
+ def downgrade() -> None:
32
+ op.drop_index("idx_leads_starred", table_name="leads")
33
+ op.drop_column("leads", "starred_at")
app/api/leads.py CHANGED
@@ -190,6 +190,13 @@ def needs_email(
190
  return LeadOut.from_model(crud.mark_needs_email(db, lead, user))
191
 
192
 
 
 
 
 
 
 
 
193
  @router.post("/leads/{lead_id}/meeting-outcome", response_model=LeadOut)
194
  def meeting_outcome(
195
  lead_id: int,
 
190
  return LeadOut.from_model(crud.mark_needs_email(db, lead, user))
191
 
192
 
193
+ @router.post("/leads/{lead_id}/star", response_model=LeadOut)
194
+ def star(lead_id: int, db: Session = Depends(get_db), user: User | None = Depends(get_current_user)):
195
+ """"Star" priority mark -- toggle, see crud.toggle_star(). User request, 2026-09-23."""
196
+ lead = _require(db, lead_id)
197
+ return LeadOut.from_model(crud.toggle_star(db, lead, user))
198
+
199
+
200
  @router.post("/leads/{lead_id}/meeting-outcome", response_model=LeadOut)
201
  def meeting_outcome(
202
  lead_id: int,
app/crud/leads.py CHANGED
@@ -509,6 +509,20 @@ def set_call_picked_by(db: Session, lead: Lead, value: str, user=None) -> Lead:
509
  return lead
510
 
511
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  def mark_needs_email(db: Session, lead: Lead, user=None) -> Lead:
513
  """
514
  "Email Send" button -- structure-plan.md Phase 3/4. A forward-looking flag ("this lead now needs an
 
509
  return lead
510
 
511
 
512
+ def toggle_star(db: Session, lead: Lead, user=None) -> Lead:
513
+ """
514
+ "Star" priority mark (user, 2026-09-23) -- same tag-only rule as set_call_picked_by above: no stage
515
+ change, no queue movement, not logged as a call_events row. Just flips starred_at on/off.
516
+ """
517
+ lead.starred_at = None if lead.starred_at else _now()
518
+ lead.updated_at = _now()
519
+ if user is not None:
520
+ lead.last_touched_by_user_id = user.id
521
+ db.commit()
522
+ db.refresh(lead)
523
+ return lead
524
+
525
+
526
  def mark_needs_email(db: Session, lead: Lead, user=None) -> Lead:
527
  """
528
  "Email Send" button -- structure-plan.md Phase 3/4. A forward-looking flag ("this lead now needs an
app/models.py CHANGED
@@ -73,6 +73,7 @@ class Lead(Base):
73
  Index("idx_leads_sent_to_desk", "sent_to_desk_at"),
74
  Index("idx_leads_hidden", "hidden_at"),
75
  Index("idx_leads_callback", "callback_at"),
 
76
  )
77
 
78
  id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
@@ -198,6 +199,10 @@ class Lead(Base):
198
  # Callback Scheduled's date/time (separate from meeting_at -- a callback is not a meeting and must
199
  # never appear on the Meetings calendar). Pipeline shows/filters by this.
200
  callback_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
 
 
 
 
201
 
202
  contacts: Mapped[list["LeadContact"]] = relationship(
203
  back_populates="lead", cascade="all, delete-orphan", passive_deletes=True
 
73
  Index("idx_leads_sent_to_desk", "sent_to_desk_at"),
74
  Index("idx_leads_hidden", "hidden_at"),
75
  Index("idx_leads_callback", "callback_at"),
76
+ Index("idx_leads_starred", "starred_at"),
77
  )
78
 
79
  id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
 
199
  # Callback Scheduled's date/time (separate from meeting_at -- a callback is not a meeting and must
200
  # never appear on the Meetings calendar). Pipeline shows/filters by this.
201
  callback_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
202
+ # "Star" priority mark (revision 0015, user 2026-09-23) -- tag only, same as call_picked_by: never
203
+ # changes pipeline_stage or queue position, not logged as a call_events row. A rep sets this to flag
204
+ # "this lead is worth prioritizing"; Pipeline shows/filters by it.
205
+ starred_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
206
 
207
  contacts: Mapped[list["LeadContact"]] = relationship(
208
  back_populates="lead", cascade="all, delete-orphan", passive_deletes=True
app/schemas/lead.py CHANGED
@@ -173,6 +173,8 @@ class LeadOut(BaseModel):
173
  hidden_at: str = ""
174
  # "" = no callback scheduled. Pipeline shows/filters leads by this.
175
  callback_at: str = ""
 
 
176
 
177
  @classmethod
178
  def from_model(cls, m: Any) -> "LeadOut":
@@ -234,6 +236,7 @@ class LeadOut(BaseModel):
234
  call_picked_by=_s(getattr(m, "call_picked_by", "")),
235
  hidden_at=_dt_utc(getattr(m, "hidden_at", None)),
236
  callback_at=_dt_utc(getattr(m, "callback_at", None)),
 
237
  call_tz=call_timezone(
238
  _s(m.contact_phone),
239
  _s(getattr(m, "phone_country", "")) or _s(getattr(m, "country", "")),
 
173
  hidden_at: str = ""
174
  # "" = no callback scheduled. Pipeline shows/filters leads by this.
175
  callback_at: str = ""
176
+ # "" = not starred. Priority mark, tag only -- see models.Lead.starred_at. User request, 2026-09-23.
177
+ starred_at: str = ""
178
 
179
  @classmethod
180
  def from_model(cls, m: Any) -> "LeadOut":
 
236
  call_picked_by=_s(getattr(m, "call_picked_by", "")),
237
  hidden_at=_dt_utc(getattr(m, "hidden_at", None)),
238
  callback_at=_dt_utc(getattr(m, "callback_at", None)),
239
+ starred_at=_dt_utc(getattr(m, "starred_at", None)),
240
  call_tz=call_timezone(
241
  _s(m.contact_phone),
242
  _s(getattr(m, "phone_country", "")) or _s(getattr(m, "country", "")),
scripts/check_star.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ check_star.py -- offline: the "Star" priority mark (user, 2026-09-23).
3
+
4
+ uv run python scripts/check_star.py
5
+
6
+ No database, no network -- throwaway in-memory SQLite. Covers: toggle_star() flips starred_at on/off,
7
+ never touches pipeline_stage, sent_to_desk_at, or hidden_at (tag only, same rule as call_picked_by), and
8
+ is not logged as a call_events row.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
17
+
18
+ from sqlalchemy import create_engine, select # noqa: E402
19
+ from sqlalchemy.orm import sessionmaker # noqa: E402
20
+ from sqlalchemy.pool import StaticPool # noqa: E402
21
+
22
+ import app.models # noqa: E402,F401
23
+ from app.crud import leads as crud_leads # noqa: E402
24
+ from app.database import Base # noqa: E402
25
+ from app.models import CallEvent # noqa: E402
26
+
27
+ FAILED = 0
28
+
29
+
30
+ def check(label: str, ok: bool, detail: str = "") -> None:
31
+ global FAILED
32
+ FAILED += 0 if ok else 1
33
+ print(f" [{'ok ' if ok else 'FAIL'}] {label}" + ("" if ok else f" {detail}"))
34
+
35
+
36
+ def main() -> int:
37
+ engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
38
+ Base.metadata.create_all(engine)
39
+ db = sessionmaker(bind=engine)()
40
+
41
+ lead = crud_leads.add_lead_dict(db, {"company_name": "Star Co"})
42
+ before_stage, before_desk, before_hidden = lead.pipeline_stage, lead.sent_to_desk_at, lead.hidden_at
43
+ events_before = db.scalars(select(CallEvent).where(CallEvent.lead_id == lead.id)).all()
44
+
45
+ print("\nStar on")
46
+ lead = crud_leads.toggle_star(db, lead)
47
+ check("starred_at set", lead.starred_at is not None)
48
+ check("pipeline_stage untouched", lead.pipeline_stage == before_stage)
49
+ check("sent_to_desk_at untouched", lead.sent_to_desk_at == before_desk)
50
+ check("hidden_at untouched", lead.hidden_at == before_hidden)
51
+ events_after = db.scalars(select(CallEvent).where(CallEvent.lead_id == lead.id)).all()
52
+ check("no call_events row logged", len(events_after) == len(events_before))
53
+
54
+ print("\nStar off (toggle again)")
55
+ lead = crud_leads.toggle_star(db, lead)
56
+ check("starred_at cleared", lead.starred_at is None)
57
+
58
+ print(f"\n{'FAILED: ' + str(FAILED) if FAILED else 'All checks passed.'}")
59
+ return 1 if FAILED else 0
60
+
61
+
62
+ if __name__ == "__main__":
63
+ sys.exit(main())