Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

470 строки
22KB

  1. <%
  2. '=======================================================================================================================
  3. ' BUSINESS REPOSITORY
  4. ' Data access for businesses and their logged contacts (interactions). Everything is bound as ADODB parameters
  5. ' through the Db_* helpers (app/lib/lib.Db.asp).
  6. '
  7. ' List filters (Scripting.Dictionary, all optional):
  8. ' q free text: every whitespace-separated term must match name, contact, email, city or phone
  9. ' industry "" = any, "0" = no industry, otherwise an industries.id
  10. ' area "" = any, "0" = no area, otherwise an areas.id (businesses linked to that area)
  11. ' status "active" (default, hides removed), "all", or new | contacted | follow_up | removed
  12. ' show "" = everyone, "prospects", "customers"
  13. ' contacted "never" = only businesses with no contact yet
  14. ' phone "1" = only businesses with a phone number
  15. ' outcome an outcome key (or "none") = businesses with a contact logged with that outcome in the last 30 days
  16. ' sort name | city | industry | area | status | last_contacted | next_action | added (default name)
  17. ' area sorts by each business's alphabetically first area; businesses with none sort first
  18. ' dir asc | desc
  19. '=======================================================================================================================
  20. Class BusinessRepository_Class
  21. Private m_where
  22. Private m_params ' Dictionary index -> Array(adoType, value), in the order of the ? markers in m_where
  23. '---------------------------------------------------------------------------------------------------------------------
  24. ' Lists
  25. '---------------------------------------------------------------------------------------------------------------------
  26. ' One page of the filtered list. Returns Dictionary("rows" -> Dictionary of row Dictionaries, "total" -> count).
  27. Public Function Search(f, page, perPage)
  28. Dim result, rows, total, rs, i, skip, sql
  29. BuildWhere f
  30. Set rs = Run("SELECT COUNT(*) AS n FROM businesses b WHERE " & m_where)
  31. total = CLng(rs("n"))
  32. rs.Close
  33. If page < 1 Then page = 1
  34. skip = (page - 1) * perPage
  35. ' Access has no OFFSET: read TOP (skip + perPage) rows and step past the first `skip` of them.
  36. sql = "SELECT TOP " & CLng(skip + perPage) & " b.id, b.name, b.contact_name, b.phone, b.email, b.website, " & _
  37. "b.city, b.state, b.status, b.is_customer, b.last_contacted_at, b.next_action_date, i.name AS industry_name " & _
  38. "FROM " & ListFrom() & " WHERE " & m_where & " ORDER BY " & OrderByClause(f)
  39. Set rs = Run(sql)
  40. For i = 1 To skip
  41. If rs.EOF Then Exit For
  42. rs.MoveNext
  43. Next
  44. Set rows = ReadRows(rs)
  45. rs.Close
  46. ' Each row gets "areas": its area names, comma-separated.
  47. Dim ids, names, k
  48. ids = ""
  49. For i = 0 To rows.Count - 1
  50. If Len(ids) > 0 Then ids = ids & ","
  51. ids = ids & CLng(rows(i)("id"))
  52. Next
  53. Set names = CreateObject("Scripting.Dictionary")
  54. If Len(ids) > 0 Then Set names = AreaRepository().NamesByBusiness(ids)
  55. For i = 0 To rows.Count - 1
  56. k = CStr(rows(i)("id"))
  57. If names.Exists(k) Then rows(i)("areas") = names(k) Else rows(i)("areas") = ""
  58. Next
  59. Set result = CreateObject("Scripting.Dictionary")
  60. Set result("rows") = rows
  61. result("total") = total
  62. Set Search = result
  63. End Function
  64. ' Every business matching the filters (no paging), as a forward-only recordset for streaming to CSV.
  65. Public Function ExportRows(f)
  66. BuildWhere f
  67. Set ExportRows = Run("SELECT b.id, b.name, b.contact_name, i.name AS industry_name, b.phone, b.email, b.website, " & _
  68. "b.address1, b.city, b.state, b.zip, b.status, b.is_customer, b.last_contacted_at, b.next_action_date " & _
  69. "FROM " & ListFrom() & " WHERE " & m_where & " ORDER BY " & OrderByClause(f))
  70. End Function
  71. ' Businesses with a next-contact date on or before throughDate (removed ones excluded), oldest first,
  72. ' each with the details of its most recent logged contact.
  73. Public Function DueThrough(throughDate)
  74. Dim list, rs, i, ids, notesById, k, sql
  75. sql = "SELECT TOP 500 b.id, b.name, b.contact_name, b.phone, b.email, b.city, b.state, b.is_customer, " & _
  76. "b.next_action_date, b.last_contacted_at, i.name AS industry_name, " & _
  77. "(SELECT TOP 1 x.method FROM interactions x WHERE x.business_id = b.id ORDER BY x.contacted_at DESC, x.id DESC) AS last_method, " & _
  78. "(SELECT TOP 1 x.outcome FROM interactions x WHERE x.business_id = b.id ORDER BY x.contacted_at DESC, x.id DESC) AS last_outcome, " & _
  79. "(SELECT TOP 1 x.id FROM interactions x WHERE x.business_id = b.id ORDER BY x.contacted_at DESC, x.id DESC) AS last_interaction_id " & _
  80. "FROM businesses b LEFT JOIN industries i ON i.id = b.industry_id " & _
  81. "WHERE b.next_action_date IS NOT NULL AND b.next_action_date <= ? AND b.status <> 'removed' " & _
  82. "ORDER BY b.next_action_date ASC, b.name ASC"
  83. Set rs = Db_Query(sql, Array(Db_PD(throughDate)))
  84. Set list = ReadRows(rs)
  85. rs.Close
  86. ' Notes are a memo field, which Access won't return from a TOP 1 subquery, so fetch them by interaction id.
  87. ids = ""
  88. For i = 0 To list.Count - 1
  89. If Not IsNull(list(i)("last_interaction_id")) Then
  90. If Len(ids) > 0 Then ids = ids & ","
  91. ids = ids & CLng(list(i)("last_interaction_id"))
  92. End If
  93. Next
  94. Set notesById = CreateObject("Scripting.Dictionary")
  95. If Len(ids) > 0 Then
  96. Set rs = Db_Query("SELECT id, notes FROM interactions WHERE id IN (" & ids & ")", Array())
  97. Do While Not rs.EOF
  98. notesById(CStr(rs(0))) = rs(1) & ""
  99. rs.MoveNext
  100. Loop
  101. rs.Close
  102. End If
  103. For i = 0 To list.Count - 1
  104. list(i)("last_notes") = ""
  105. If Not IsNull(list(i)("last_interaction_id")) Then
  106. k = CStr(list(i)("last_interaction_id"))
  107. If notesById.Exists(k) Then list(i)("last_notes") = notesById(k)
  108. End If
  109. Next
  110. Set DueThrough = list
  111. End Function
  112. ' id -> name, for dropdowns
  113. Public Function Industries()
  114. Dim list, rs
  115. Set list = CreateObject("Scripting.Dictionary")
  116. Set rs = Db_Query("SELECT id, name FROM industries ORDER BY name", Array())
  117. Do While Not rs.EOF
  118. list.Add CStr(rs(0)), rs(1) & ""
  119. rs.MoveNext
  120. Loop
  121. rs.Close
  122. Set Industries = list
  123. End Function
  124. '---------------------------------------------------------------------------------------------------------------------
  125. ' One business and its history
  126. '---------------------------------------------------------------------------------------------------------------------
  127. ' All columns plus industry_name, or Nothing.
  128. Public Function Find(id)
  129. Dim rs, rows
  130. Set rs = Db_Query("SELECT b.*, i.name AS industry_name FROM businesses b LEFT JOIN industries i ON i.id = b.industry_id WHERE b.id = ?", Array(Db_PI(id)))
  131. Set rows = ReadRows(rs)
  132. rs.Close
  133. If rows.Count = 0 Then Set Find = Nothing Else Set Find = rows(0)
  134. End Function
  135. ' Logged contacts for a business, newest first.
  136. Public Function Interactions(businessId)
  137. Dim rs
  138. Set rs = Db_Query("SELECT id, method, outcome, notes, contacted_at, follow_up_date FROM interactions WHERE business_id = ? ORDER BY contacted_at DESC, id DESC", Array(Db_PI(businessId)))
  139. Set Interactions = ReadRows(rs)
  140. rs.Close
  141. End Function
  142. ' Another business (not excludeId) with the same phone digits or the same email, or Nothing.
  143. ' Returns Dictionary(id, name, matched) where matched is "phone" or "email".
  144. Public Function FindDuplicate(excludeId, phoneDigits, email)
  145. Dim d
  146. Set FindDuplicate = Nothing
  147. If Len(phoneDigits) > 0 Then
  148. Set d = FirstMatch("SELECT TOP 1 id, name FROM businesses WHERE id <> ? AND phone_digits = ?", Array(Db_PI(excludeId), Db_PS(phoneDigits)), "phone")
  149. If Not d Is Nothing Then
  150. Set FindDuplicate = d
  151. Exit Function
  152. End If
  153. End If
  154. If Len(email) > 0 Then
  155. Set FindDuplicate = FirstMatch("SELECT TOP 1 id, name FROM businesses WHERE id <> ? AND email = ?", Array(Db_PI(excludeId), Db_PS(email)), "email")
  156. End If
  157. End Function
  158. ' Businesses whose email equals one of the keys of `emails` (a Dictionary of lowercased addresses).
  159. ' Returns a Dictionary of row Dictionaries (id, name, contact_name, email, city, state, status, next_action_date).
  160. Public Function FindByEmails(emails)
  161. Dim list, rs, k, d, i
  162. Set list = CreateObject("Scripting.Dictionary")
  163. For Each k In emails.Keys
  164. Set rs = Db_Query("SELECT id, name, contact_name, email, city, state, status, next_action_date FROM businesses WHERE email = ? ORDER BY name, id", Array(Db_PS(k)))
  165. Do While Not rs.EOF
  166. Set d = CreateObject("Scripting.Dictionary")
  167. For i = 0 To rs.Fields.Count - 1
  168. d(rs.Fields(i).Name) = rs.Fields(i).Value
  169. Next
  170. list.Add list.Count, d
  171. rs.MoveNext
  172. Loop
  173. rs.Close
  174. Next
  175. Set FindByEmails = list
  176. End Function
  177. ' Returns the industry id for a name, creating the industry when it does not exist. Null for a blank name.
  178. Public Function FindOrCreateIndustry(name)
  179. Dim rs
  180. name = Left(Trim(name), 100)
  181. If Len(name) = 0 Then
  182. FindOrCreateIndustry = Null
  183. Exit Function
  184. End If
  185. Set rs = Db_Query("SELECT id FROM industries WHERE name = ?", Array(Db_PS(name)))
  186. If Not rs.EOF Then
  187. FindOrCreateIndustry = CLng(rs(0))
  188. rs.Close
  189. Exit Function
  190. End If
  191. rs.Close
  192. Db_Exec "INSERT INTO industries (name) VALUES (?)", Array(Db_PS(name))
  193. Set rs = Db_Query("SELECT @@IDENTITY", Array())
  194. FindOrCreateIndustry = CLng(rs(0))
  195. rs.Close
  196. End Function
  197. '---------------------------------------------------------------------------------------------------------------------
  198. ' Changes
  199. '---------------------------------------------------------------------------------------------------------------------
  200. ' Inserts a business entered by hand (status "new") and returns its id. d holds the same keys as Update.
  201. Public Function Create(d)
  202. Dim rs
  203. Db_Exec "INSERT INTO businesses (name, contact_name, industry_id, phone, phone_digits, email, website, address1, city, state, zip, " & _
  204. "source, status, is_customer, first_order_date, notes, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,Now())", _
  205. Array(Db_PS(d("name")), Db_PS(d("contact_name")), Db_PI(d("industry_id")), Db_PS(d("phone")), Db_PS(d("phone_digits")), Db_PS(d("email")), _
  206. Db_PS(d("website")), Db_PS(d("address1")), Db_PS(d("city")), Db_PS(d("state")), Db_PS(d("zip")), _
  207. Db_PS("Added manually"), Db_PS("new"), Db_PI(d("is_customer")), Db_PD(d("first_order_date")), Db_PS(d("notes")))
  208. Set rs = Db_Query("SELECT @@IDENTITY", Array())
  209. Create = CLng(rs(0))
  210. rs.Close
  211. End Function
  212. Public Sub Update(id, d)
  213. Db_Exec "UPDATE businesses SET name = ?, contact_name = ?, industry_id = ?, phone = ?, phone_digits = ?, email = ?, " & _
  214. "website = ?, address1 = ?, city = ?, state = ?, zip = ?, notes = ?, is_customer = ?, first_order_date = ? WHERE id = ?", _
  215. Array(Db_PS(d("name")), Db_PS(d("contact_name")), Db_PI(d("industry_id")), Db_PS(d("phone")), Db_PS(d("phone_digits")), Db_PS(d("email")), _
  216. Db_PS(d("website")), Db_PS(d("address1")), Db_PS(d("city")), Db_PS(d("state")), Db_PS(d("zip")), Db_PS(d("notes")), _
  217. Db_PI(d("is_customer")), Db_PD(d("first_order_date")), Db_PI(id))
  218. End Sub
  219. ' Logs a touch and updates the business in one transaction:
  220. ' removing -> status removed, no next action
  221. ' follow-up date -> status follow_up, next_action_date = follow-up date
  222. ' otherwise -> status contacted, next_action_date cleared
  223. Public Sub LogInteraction(id, method, outcome, notes, contactedAt, followUp, removing, removeReason)
  224. Dim conn, b, status, nextAction, lastContacted, remReason, remAt, msg
  225. Set conn = DAL().Connection
  226. Set b = Find(id)
  227. lastContacted = contactedAt
  228. If Not IsNull(b("last_contacted_at")) Then
  229. If CDate(b("last_contacted_at")) > contactedAt Then lastContacted = CDate(b("last_contacted_at"))
  230. End If
  231. remReason = Null : remAt = Null : nextAction = Null
  232. If removing Then
  233. status = "removed"
  234. remReason = removeReason
  235. remAt = Now()
  236. ElseIf Not IsNull(followUp) Then
  237. status = "follow_up"
  238. nextAction = followUp
  239. Else
  240. status = "contacted"
  241. End If
  242. conn.BeginTrans
  243. On Error Resume Next
  244. Db_Exec "INSERT INTO interactions (business_id, method, outcome, notes, contacted_at, follow_up_date, created_at) VALUES (?,?,?,?,?,?,Now())", _
  245. Array(Db_PI(id), Db_PS(method), Db_PS(outcome), Db_PS(notes), Db_PD(contactedAt), Db_PD(followUp))
  246. If Err.Number = 0 Then
  247. Db_Exec "UPDATE businesses SET last_contacted_at = ?, status = ?, next_action_date = ?, removed_reason = ?, removed_at = ? WHERE id = ?", _
  248. Array(Db_PD(lastContacted), Db_PS(status), Db_PD(nextAction), Db_PS(remReason), Db_PD(remAt), Db_PI(id))
  249. End If
  250. If Err.Number <> 0 Then
  251. msg = Err.Description
  252. Err.Clear
  253. conn.RollbackTrans
  254. On Error GoTo 0
  255. Err.Raise 5, "BusinessRepository.LogInteraction", msg
  256. End If
  257. conn.CommitTrans
  258. On Error GoTo 0
  259. End Sub
  260. Public Sub Remove(id, reason)
  261. Db_Exec "UPDATE businesses SET status = 'removed', next_action_date = NULL, removed_reason = ?, removed_at = Now() WHERE id = ?", _
  262. Array(Db_PS(reason), Db_PI(id))
  263. End Sub
  264. Public Sub Restore(id)
  265. Db_Exec "UPDATE businesses SET status = IIf(last_contacted_at IS NULL, 'new', 'contacted'), removed_reason = NULL, removed_at = NULL WHERE id = ?", _
  266. Array(Db_PI(id))
  267. End Sub
  268. '---------------------------------------------------------------------------------------------------------------------
  269. ' Internals
  270. '---------------------------------------------------------------------------------------------------------------------
  271. ' Reads every remaining row of the recordset into a Dictionary (0-based index -> Dictionary of column -> value).
  272. Private Function ReadRows(rs)
  273. Dim list, d, i
  274. Set list = CreateObject("Scripting.Dictionary")
  275. Do While Not rs.EOF
  276. Set d = CreateObject("Scripting.Dictionary")
  277. For i = 0 To rs.Fields.Count - 1
  278. d(rs.Fields(i).Name) = rs.Fields(i).Value
  279. Next
  280. list.Add list.Count, d
  281. rs.MoveNext
  282. Loop
  283. Set ReadRows = list
  284. End Function
  285. ' Runs a query expected to return (id, name) and wraps the first row as Dictionary(id, name, matched), or Nothing.
  286. Private Function FirstMatch(sql, params, matched)
  287. Dim rs, d
  288. Set FirstMatch = Nothing
  289. Set rs = Db_Query(sql, params)
  290. If Not rs.EOF Then
  291. Set d = CreateObject("Scripting.Dictionary")
  292. d("id") = CLng(rs(0)) : d("name") = rs(1) & "" : d("matched") = matched
  293. Set FirstMatch = d
  294. End If
  295. rs.Close
  296. End Function
  297. ' Executes sql with the parameters collected by BuildWhere.
  298. Private Function Run(sql)
  299. Dim params(), i
  300. If m_params.Count = 0 Then
  301. Set Run = Db_Query(sql, Array())
  302. Exit Function
  303. End If
  304. ReDim params(m_params.Count - 1)
  305. For i = 0 To m_params.Count - 1
  306. params(i) = m_params(i)
  307. Next
  308. Set Run = Db_Query(sql, params)
  309. End Function
  310. Private Sub AddParam(typ, v)
  311. m_params.Add m_params.Count, Array(typ, v)
  312. End Sub
  313. Private Sub BuildWhere(f)
  314. Dim terms, t, d, q, n, frag, ind, ar, st, oc
  315. m_where = "1=1"
  316. Set m_params = CreateObject("Scripting.Dictionary")
  317. q = Trim(Val_(f, "q"))
  318. If Len(q) > 0 Then
  319. terms = Split(q, " ")
  320. n = 0
  321. For Each t In terms
  322. If Len(t) > 0 And n < 5 Then
  323. n = n + 1
  324. t = Left(t, 50)
  325. d = ImportFile_PhoneDigitsLoose(t)
  326. frag = "(b.name LIKE ? OR b.contact_name LIKE ? OR b.email LIKE ? OR b.city LIKE ?"
  327. AddParam 202, "%" & Like_(t) & "%"
  328. AddParam 202, "%" & Like_(t) & "%"
  329. AddParam 202, "%" & Like_(t) & "%"
  330. AddParam 202, "%" & Like_(t) & "%"
  331. If Len(d) >= 3 Then
  332. frag = frag & " OR b.phone_digits LIKE ?"
  333. AddParam 202, "%" & d & "%"
  334. End If
  335. m_where = m_where & " AND " & frag & ")"
  336. End If
  337. Next
  338. End If
  339. ind = Val_(f, "industry")
  340. If ind = "0" Then
  341. m_where = m_where & " AND b.industry_id IS NULL"
  342. ElseIf Valid_Id(ind) Then
  343. m_where = m_where & " AND b.industry_id = ?"
  344. AddParam 3, CLng(ind)
  345. End If
  346. ar = Val_(f, "area")
  347. If ar = "0" Then
  348. m_where = m_where & " AND NOT EXISTS (SELECT ba.area_id FROM business_areas ba WHERE ba.business_id = b.id)"
  349. ElseIf Valid_Id(ar) Then
  350. ' Inlined as a validated CLng, not a ? marker: Access binds markers inside a subquery ahead of the outer
  351. ' ones, so a parameter here would swap values with the search terms before it.
  352. m_where = m_where & " AND EXISTS (SELECT ba.area_id FROM business_areas ba WHERE ba.business_id = b.id AND ba.area_id = " & CLng(ar) & ")"
  353. End If
  354. st = Val_(f, "status")
  355. Select Case st
  356. Case "all"
  357. Case "new", "contacted", "follow_up", "removed"
  358. m_where = m_where & " AND b.status = ?"
  359. AddParam 202, st
  360. Case Else
  361. m_where = m_where & " AND b.status <> 'removed'"
  362. End Select
  363. Select Case Val_(f, "show")
  364. Case "prospects"
  365. m_where = m_where & " AND (b.is_customer = 0 OR b.is_customer IS NULL)"
  366. Case "customers"
  367. m_where = m_where & " AND b.is_customer = 1"
  368. End Select
  369. If Val_(f, "contacted") = "never" Then m_where = m_where & " AND b.last_contacted_at IS NULL"
  370. If Val_(f, "phone") = "1" Then m_where = m_where & " AND b.phone_digits IS NOT NULL"
  371. ' Businesses with a contact logged in the last 30 days with this outcome (same window as the dashboard).
  372. ' "none" = contacts logged without an outcome. Inlined from a whitelist, not a ? marker (see area above).
  373. oc = Val_(f, "outcome")
  374. If oc = "none" Then
  375. m_where = m_where & " AND EXISTS (SELECT x.id FROM interactions x WHERE x.business_id = b.id " & _
  376. "AND x.contacted_at >= DateAdd('d', -29, Date()) AND (x.outcome IS NULL OR x.outcome = ''))"
  377. ElseIf Businesses_IsKey(Businesses_Outcomes(), oc) Then
  378. m_where = m_where & " AND EXISTS (SELECT x.id FROM interactions x WHERE x.business_id = b.id " & _
  379. "AND x.contacted_at >= DateAdd('d', -29, Date()) AND x.outcome = '" & oc & "')"
  380. End If
  381. End Sub
  382. ' FROM clause for the list and export: the business, its industry, and fa.first_area (its alphabetically first
  383. ' area name) for sorting. Access won't take a subquery in ORDER BY, hence the derived table.
  384. Private Function ListFrom()
  385. ListFrom = "(businesses b LEFT JOIN industries i ON i.id = b.industry_id) " & _
  386. "LEFT JOIN (SELECT ba.business_id, MIN(a.name) AS first_area FROM business_areas ba " & _
  387. "INNER JOIN areas a ON a.id = ba.area_id GROUP BY ba.business_id) AS fa ON fa.business_id = b.id"
  388. End Function
  389. Private Function OrderByClause(f)
  390. Dim col, dirSql
  391. Select Case Val_(f, "sort")
  392. Case "city": col = "b.city"
  393. Case "industry": col = "i.name"
  394. Case "area": col = "fa.first_area"
  395. Case "status": col = "b.status"
  396. Case "last_contacted": col = "b.last_contacted_at"
  397. Case "next_action": col = "b.next_action_date"
  398. Case "added": col = "b.created_at"
  399. Case Else: col = "b.name"
  400. End Select
  401. If LCase(Val_(f, "dir")) = "desc" Then dirSql = "DESC" Else dirSql = "ASC"
  402. OrderByClause = col & " " & dirSql & ", b.id " & dirSql
  403. End Function
  404. Private Function Val_(f, key)
  405. If f.Exists(key) Then Val_ = CStr(f(key)) Else Val_ = ""
  406. End Function
  407. ' Escape LIKE wildcards so user input is matched literally.
  408. Private Function Like_(s)
  409. s = Replace(s, "[", "[[]")
  410. s = Replace(s, "%", "[%]")
  411. s = Replace(s, "_", "[_]")
  412. s = Replace(s, "*", "[*]")
  413. s = Replace(s, "?", "[?]")
  414. Like_ = s
  415. End Function
  416. End Class
  417. Dim BusinessRepository_Class__Singleton
  418. Function BusinessRepository()
  419. If IsEmpty(BusinessRepository_Class__Singleton) Then
  420. Set BusinessRepository_Class__Singleton = New BusinessRepository_Class
  421. End If
  422. Set BusinessRepository = BusinessRepository_Class__Singleton
  423. End Function
  424. %>

Powered by TurnKey Linux.