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

445 строки
20KB

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

Powered by TurnKey Linux.