Seth commited on
Commit
c50adfe
·
1 Parent(s): 480db82
backend/app/__pycache__/main.cpython-314.pyc CHANGED
Binary files a/backend/app/__pycache__/main.cpython-314.pyc and b/backend/app/__pycache__/main.cpython-314.pyc differ
 
backend/app/main.py CHANGED
@@ -16,7 +16,7 @@ import json
16
  import asyncio
17
  import math
18
  import re
19
- from datetime import datetime
20
 
21
  from .database import get_db, UploadedFile, Prompt, GeneratedSequence, SmartleadRun, Contact, CrmLead
22
  from .models import (
@@ -1119,6 +1119,155 @@ async def smartlead_webhook(request: Request, db: Session = Depends(get_db)):
1119
  return {"ok": True, "lead_id": row.id}
1120
 
1121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1122
  @app.get("/api/leads")
1123
  async def list_leads(
1124
  search: str = Query("", description="Search email, name, company"),
 
16
  import asyncio
17
  import math
18
  import re
19
+ from datetime import datetime, timedelta
20
 
21
  from .database import get_db, UploadedFile, Prompt, GeneratedSequence, SmartleadRun, Contact, CrmLead
22
  from .models import (
 
1119
  return {"ok": True, "lead_id": row.id}
1120
 
1121
 
1122
+ @app.post("/api/leads/seed-demo")
1123
+ async def seed_demo_leads(db: Session = Depends(get_db)):
1124
+ """
1125
+ Insert sample leads so the Leads UI can be previewed without a real Smartlead webhook.
1126
+ Deletes any previous demo rows (emails like demo.lead.*@emailout.local) then inserts fresh ones.
1127
+ """
1128
+ demo_email_filter = CrmLead.email.like("demo.lead.%@emailout.local")
1129
+ removed = db.query(CrmLead).filter(demo_email_filter).delete(synchronize_session=False)
1130
+
1131
+ campaign_id = "88001"
1132
+ campaign_name = "Logistics & Supply Chain Outreach"
1133
+ now = datetime.utcnow()
1134
+
1135
+ specs = [
1136
+ {
1137
+ "sl_id": "91001",
1138
+ "key": "jane",
1139
+ "first_name": "Jane",
1140
+ "last_name": "Morgan",
1141
+ "company_name": "ACME Logistics",
1142
+ "title": "VP of Operations",
1143
+ "crm_status": "new_lead",
1144
+ "subject": "Re: Quick question on your freight volumes",
1145
+ "body": (
1146
+ "Hi Sarah,\n\nThanks for reaching out. We're evaluating new 3PL partners in Q2. "
1147
+ "Could you send a one-pager on pricing for mid-market shippers?\n\nBest,\nJane"
1148
+ ),
1149
+ "days_ago": 0,
1150
+ },
1151
+ {
1152
+ "sl_id": "91002",
1153
+ "key": "marcus",
1154
+ "first_name": "Marcus",
1155
+ "last_name": "Chen",
1156
+ "company_name": "Pacific Rail Partners",
1157
+ "title": "Director of Procurement",
1158
+ "crm_status": "attempted_to_contact",
1159
+ "subject": "Re: Partnership",
1160
+ "body": (
1161
+ "Not interested right now — we renewed with our incumbent through 2026. "
1162
+ "Feel free to circle back next year."
1163
+ ),
1164
+ "days_ago": 1,
1165
+ },
1166
+ {
1167
+ "sl_id": "91003",
1168
+ "key": "elena",
1169
+ "first_name": "Elena",
1170
+ "last_name": "Vasquez",
1171
+ "company_name": "Harbor Freight Collective",
1172
+ "title": "Head of Supply Chain",
1173
+ "crm_status": "contacted",
1174
+ "subject": "Re: 15 min chat?",
1175
+ "body": (
1176
+ "Tuesday 3pm PT works for me. Here's my calendar link: https://example.com/cal/elena — "
1177
+ "please include an agenda."
1178
+ ),
1179
+ "days_ago": 2,
1180
+ },
1181
+ {
1182
+ "sl_id": "91004",
1183
+ "key": "david",
1184
+ "first_name": "David",
1185
+ "last_name": "Okonkwo",
1186
+ "company_name": "EZOFIS",
1187
+ "title": "CEO",
1188
+ "crm_status": "qualified",
1189
+ "subject": "Re: outbound sequences",
1190
+ "body": (
1191
+ "This looks promising. Loop in our COO (coo@ezofis.com) and let's do a 30-min scoping call this week."
1192
+ ),
1193
+ "days_ago": 3,
1194
+ },
1195
+ {
1196
+ "sl_id": "91005",
1197
+ "key": "priya",
1198
+ "first_name": "Priya",
1199
+ "last_name": "Nair",
1200
+ "company_name": "Summit Cold Storage",
1201
+ "title": "Operations Manager",
1202
+ "crm_status": "unqualified",
1203
+ "subject": "Re: cold storage",
1204
+ "body": (
1205
+ "We only work with vendors on our approved vendor list. Please don't follow up on this thread."
1206
+ ),
1207
+ "days_ago": 4,
1208
+ },
1209
+ {
1210
+ "sl_id": "91006",
1211
+ "key": "sam",
1212
+ "first_name": "Sam",
1213
+ "last_name": "Rivera",
1214
+ "company_name": "Inland Distribution Co.",
1215
+ "title": "Logistics Coordinator",
1216
+ "crm_status": "none",
1217
+ "subject": "Re: intro",
1218
+ "body": "Got it — thanks.",
1219
+ "days_ago": 5,
1220
+ },
1221
+ ]
1222
+
1223
+ for s in specs:
1224
+ email = f"demo.lead.{s['key']}@emailout.local"
1225
+ ts = now - timedelta(days=s["days_ago"])
1226
+ raw = {
1227
+ "event": "EMAIL_REPLIED",
1228
+ "demo_seed": True,
1229
+ "campaign_id": int(campaign_id),
1230
+ "campaign_name": campaign_name,
1231
+ "lead_id": int(s["sl_id"]),
1232
+ "lead": {
1233
+ "email": email,
1234
+ "first_name": s["first_name"],
1235
+ "last_name": s["last_name"],
1236
+ "company_name": s["company_name"],
1237
+ },
1238
+ "reply": {
1239
+ "subject": s["subject"],
1240
+ "body": s["body"],
1241
+ "received_at": ts.isoformat() + "Z",
1242
+ },
1243
+ }
1244
+ db.add(
1245
+ CrmLead(
1246
+ smartlead_lead_id=s["sl_id"],
1247
+ campaign_id=campaign_id,
1248
+ campaign_name=campaign_name,
1249
+ email=email,
1250
+ first_name=s["first_name"],
1251
+ last_name=s["last_name"],
1252
+ company_name=s["company_name"],
1253
+ title=s["title"],
1254
+ last_reply_subject=s["subject"],
1255
+ last_reply_body=s["body"],
1256
+ last_reply_at=ts,
1257
+ crm_status=s["crm_status"],
1258
+ contact_id=None,
1259
+ raw_webhook=raw,
1260
+ )
1261
+ )
1262
+
1263
+ db.commit()
1264
+ return {
1265
+ "ok": True,
1266
+ "removed_previous_demo_rows": removed,
1267
+ "inserted": len(specs),
1268
+ }
1269
+
1270
+
1271
  @app.get("/api/leads")
1272
  async def list_leads(
1273
  search: str = Query("", description="Search email, name, company"),
frontend/src/pages/Leads.jsx CHANGED
@@ -40,6 +40,7 @@ export default function Leads() {
40
  const [threadLoading, setThreadLoading] = useState(false);
41
  const [threadData, setThreadData] = useState(null);
42
  const [moveBusy, setMoveBusy] = useState(null);
 
43
 
44
  const webhookUrl = useMemo(() => {
45
  if (typeof window === 'undefined') return '';
@@ -69,6 +70,25 @@ export default function Leads() {
69
  }
70
  }, [search, statusFilter]);
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  useEffect(() => {
73
  const t = setTimeout(() => fetchLeads(), 250);
74
  return () => clearTimeout(t);
@@ -185,6 +205,19 @@ export default function Leads() {
185
  ))}
186
  </SelectContent>
187
  </Select>
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  <Button variant="outline" size="sm" onClick={() => fetchLeads()}>
189
  Refresh
190
  </Button>
@@ -216,10 +249,15 @@ export default function Leads() {
216
  <Loader2 className="h-8 w-8 animate-spin" />
217
  </div>
218
  ) : leads.length === 0 ? (
219
- <p className="text-center py-16 text-slate-500">
220
- No leads yet. When a prospect replies in Smartlead, they will show up
221
- here via webhook.
222
- </p>
 
 
 
 
 
223
  ) : (
224
  <table className="w-full text-sm">
225
  <thead>
 
40
  const [threadLoading, setThreadLoading] = useState(false);
41
  const [threadData, setThreadData] = useState(null);
42
  const [moveBusy, setMoveBusy] = useState(null);
43
+ const [seedBusy, setSeedBusy] = useState(false);
44
 
45
  const webhookUrl = useMemo(() => {
46
  if (typeof window === 'undefined') return '';
 
70
  }
71
  }, [search, statusFilter]);
72
 
73
+ const seedDemoLeads = async () => {
74
+ setSeedBusy(true);
75
+ try {
76
+ const res = await fetch('/api/leads/seed-demo', { method: 'POST' });
77
+ const data = await res.json().catch(() => ({}));
78
+ if (!res.ok) {
79
+ throw new Error(
80
+ typeof data.detail === 'string' ? data.detail : 'Could not load demo data'
81
+ );
82
+ }
83
+ await fetchLeads();
84
+ } catch (e) {
85
+ console.error(e);
86
+ alert(e.message || 'Could not load demo data');
87
+ } finally {
88
+ setSeedBusy(false);
89
+ }
90
+ };
91
+
92
  useEffect(() => {
93
  const t = setTimeout(() => fetchLeads(), 250);
94
  return () => clearTimeout(t);
 
205
  ))}
206
  </SelectContent>
207
  </Select>
208
+ <Button
209
+ variant="secondary"
210
+ size="sm"
211
+ onClick={() => seedDemoLeads()}
212
+ disabled={seedBusy}
213
+ title="Replace demo.lead.*@emailout.local rows with sample replies"
214
+ >
215
+ {seedBusy ? (
216
+ <Loader2 className="h-4 w-4 animate-spin" />
217
+ ) : (
218
+ 'Demo data'
219
+ )}
220
+ </Button>
221
  <Button variant="outline" size="sm" onClick={() => fetchLeads()}>
222
  Refresh
223
  </Button>
 
249
  <Loader2 className="h-8 w-8 animate-spin" />
250
  </div>
251
  ) : leads.length === 0 ? (
252
+ <div className="text-center py-16 text-slate-500 space-y-3">
253
+ <p>
254
+ No leads yet. When a prospect replies in Smartlead, they will show up
255
+ here via webhook.
256
+ </p>
257
+ <Button size="sm" variant="secondary" onClick={() => seedDemoLeads()} disabled={seedBusy}>
258
+ Load demo rows (preview UI)
259
+ </Button>
260
+ </div>
261
  ) : (
262
  <table className="w-full text-sm">
263
  <thead>