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

458 строки
21KB

  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. ' Inserts a business entered by hand (status "new") and returns its id. d holds the same keys as Update.
  200. Public Function Create(d)
  201. Dim rs
  202. Db_Exec "INSERT INTO businesses (name, contact_name, industry_id, phone, phone_digits, email, website, address1, city, state, zip, " & _
  203. "source, status, is_customer, first_order_date, notes, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,Now())", _
  204. 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")), _
  205. Db_PS(d("website")), Db_PS(d("address1")), Db_PS(d("city")), Db_PS(d("state")), Db_PS(d("zip")), _
  206. Db_PS("Added manually"), Db_PS("new"), Db_PI(d("is_customer")), Db_PD(d("first_order_date")), Db_PS(d("notes")))
  207. Set rs = Db_Query("SELECT @@IDENTITY", Array())
  208. Create = CLng(rs(0))
  209. rs.Close
  210. End Function
  211. Public Sub Update(id, d)
  212. Db_Exec "UPDATE businesses SET name = ?, contact_name = ?, industry_id = ?, phone = ?, phone_digits = ?, email = ?, " & _
  213. "website = ?, address1 = ?, city = ?, state = ?, zip = ?, notes = ?, is_customer = ?, first_order_date = ? WHERE id = ?", _
  214. 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")), _
  215. Db_PS(d("website")), Db_PS(d("address1")), Db_PS(d("city")), Db_PS(d("state")), Db_PS(d("zip")), Db_PS(d("notes")), _
  216. Db_PI(d("is_customer")), Db_PD(d("first_order_date")), Db_PI(id))
  217. End Sub
  218. ' Logs a touch and updates the business in one transaction:
  219. ' removing -> status removed, no next action
  220. ' follow-up date -> status follow_up, next_action_date = follow-up date
  221. ' otherwise -> status contacted, next_action_date cleared
  222. Public Sub LogInteraction(id, method, outcome, notes, contactedAt, followUp, removing, removeReason)
  223. Dim conn, b, status, nextAction, lastContacted, remReason, remAt, msg
  224. Set conn = DAL().Connection
  225. Set b = Find(id)
  226. lastContacted = contactedAt
  227. If Not IsNull(b("last_contacted_at")) Then
  228. If CDate(b("last_contacted_at")) > contactedAt Then lastContacted = CDate(b("last_contacted_at"))
  229. End If
  230. remReason = Null : remAt = Null : nextAction = Null
  231. If removing Then
  232. status = "removed"
  233. remReason = removeReason
  234. remAt = Now()
  235. ElseIf Not IsNull(followUp) Then
  236. status = "follow_up"
  237. nextAction = followUp
  238. Else
  239. status = "contacted"
  240. End If
  241. conn.BeginTrans
  242. On Error Resume Next
  243. Db_Exec "INSERT INTO interactions (business_id, method, outcome, notes, contacted_at, follow_up_date, created_at) VALUES (?,?,?,?,?,?,Now())", _
  244. Array(Db_PI(id), Db_PS(method), Db_PS(outcome), Db_PS(notes), Db_PD(contactedAt), Db_PD(followUp))
  245. If Err.Number = 0 Then
  246. Db_Exec "UPDATE businesses SET last_contacted_at = ?, status = ?, next_action_date = ?, removed_reason = ?, removed_at = ? WHERE id = ?", _
  247. Array(Db_PD(lastContacted), Db_PS(status), Db_PD(nextAction), Db_PS(remReason), Db_PD(remAt), Db_PI(id))
  248. End If
  249. If Err.Number <> 0 Then
  250. msg = Err.Description
  251. Err.Clear
  252. conn.RollbackTrans
  253. On Error GoTo 0
  254. Err.Raise 5, "BusinessRepository.LogInteraction", msg
  255. End If
  256. conn.CommitTrans
  257. On Error GoTo 0
  258. End Sub
  259. Public Sub Remove(id, reason)
  260. Db_Exec "UPDATE businesses SET status = 'removed', next_action_date = NULL, removed_reason = ?, removed_at = Now() WHERE id = ?", _
  261. Array(Db_PS(reason), Db_PI(id))
  262. End Sub
  263. Public Sub Restore(id)
  264. Db_Exec "UPDATE businesses SET status = IIf(last_contacted_at IS NULL, 'new', 'contacted'), removed_reason = NULL, removed_at = NULL WHERE id = ?", _
  265. Array(Db_PI(id))
  266. End Sub
  267. '---------------------------------------------------------------------------------------------------------------------
  268. ' Internals
  269. '---------------------------------------------------------------------------------------------------------------------
  270. ' Reads every remaining row of the recordset into a Dictionary (0-based index -> Dictionary of column -> value).
  271. Private Function ReadRows(rs)
  272. Dim list, d, i
  273. Set list = CreateObject("Scripting.Dictionary")
  274. Do While Not rs.EOF
  275. Set d = CreateObject("Scripting.Dictionary")
  276. For i = 0 To rs.Fields.Count - 1
  277. d(rs.Fields(i).Name) = rs.Fields(i).Value
  278. Next
  279. list.Add list.Count, d
  280. rs.MoveNext
  281. Loop
  282. Set ReadRows = list
  283. End Function
  284. ' Runs a query expected to return (id, name) and wraps the first row as Dictionary(id, name, matched), or Nothing.
  285. Private Function FirstMatch(sql, params, matched)
  286. Dim rs, d
  287. Set FirstMatch = Nothing
  288. Set rs = Db_Query(sql, params)
  289. If Not rs.EOF Then
  290. Set d = CreateObject("Scripting.Dictionary")
  291. d("id") = CLng(rs(0)) : d("name") = rs(1) & "" : d("matched") = matched
  292. Set FirstMatch = d
  293. End If
  294. rs.Close
  295. End Function
  296. ' Executes sql with the parameters collected by BuildWhere.
  297. Private Function Run(sql)
  298. Dim params(), i
  299. If m_params.Count = 0 Then
  300. Set Run = Db_Query(sql, Array())
  301. Exit Function
  302. End If
  303. ReDim params(m_params.Count - 1)
  304. For i = 0 To m_params.Count - 1
  305. params(i) = m_params(i)
  306. Next
  307. Set Run = Db_Query(sql, params)
  308. End Function
  309. Private Sub AddParam(typ, v)
  310. m_params.Add m_params.Count, Array(typ, v)
  311. End Sub
  312. Private Sub BuildWhere(f)
  313. Dim terms, t, d, q, n, frag, ind, ar, st
  314. m_where = "1=1"
  315. Set m_params = CreateObject("Scripting.Dictionary")
  316. q = Trim(Val_(f, "q"))
  317. If Len(q) > 0 Then
  318. terms = Split(q, " ")
  319. n = 0
  320. For Each t In terms
  321. If Len(t) > 0 And n < 5 Then
  322. n = n + 1
  323. t = Left(t, 50)
  324. d = ImportFile_PhoneDigitsLoose(t)
  325. frag = "(b.name LIKE ? OR b.contact_name LIKE ? OR b.email LIKE ? OR b.city LIKE ?"
  326. AddParam 202, "%" & Like_(t) & "%"
  327. AddParam 202, "%" & Like_(t) & "%"
  328. AddParam 202, "%" & Like_(t) & "%"
  329. AddParam 202, "%" & Like_(t) & "%"
  330. If Len(d) >= 3 Then
  331. frag = frag & " OR b.phone_digits LIKE ?"
  332. AddParam 202, "%" & d & "%"
  333. End If
  334. m_where = m_where & " AND " & frag & ")"
  335. End If
  336. Next
  337. End If
  338. ind = Val_(f, "industry")
  339. If ind = "0" Then
  340. m_where = m_where & " AND b.industry_id IS NULL"
  341. ElseIf Valid_Id(ind) Then
  342. m_where = m_where & " AND b.industry_id = ?"
  343. AddParam 3, CLng(ind)
  344. End If
  345. ar = Val_(f, "area")
  346. If ar = "0" Then
  347. m_where = m_where & " AND NOT EXISTS (SELECT ba.area_id FROM business_areas ba WHERE ba.business_id = b.id)"
  348. ElseIf Valid_Id(ar) Then
  349. ' Inlined as a validated CLng, not a ? marker: Access binds markers inside a subquery ahead of the outer
  350. ' ones, so a parameter here would swap values with the search terms before it.
  351. 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) & ")"
  352. End If
  353. st = Val_(f, "status")
  354. Select Case st
  355. Case "all"
  356. Case "new", "contacted", "follow_up", "removed"
  357. m_where = m_where & " AND b.status = ?"
  358. AddParam 202, st
  359. Case Else
  360. m_where = m_where & " AND b.status <> 'removed'"
  361. End Select
  362. Select Case Val_(f, "show")
  363. Case "prospects"
  364. m_where = m_where & " AND (b.is_customer = 0 OR b.is_customer IS NULL)"
  365. Case "customers"
  366. m_where = m_where & " AND b.is_customer = 1"
  367. End Select
  368. If Val_(f, "contacted") = "never" Then m_where = m_where & " AND b.last_contacted_at IS NULL"
  369. If Val_(f, "phone") = "1" Then m_where = m_where & " AND b.phone_digits IS NOT NULL"
  370. End Sub
  371. ' FROM clause for the list and export: the business, its industry, and fa.first_area (its alphabetically first
  372. ' area name) for sorting. Access won't take a subquery in ORDER BY, hence the derived table.
  373. Private Function ListFrom()
  374. ListFrom = "(businesses b LEFT JOIN industries i ON i.id = b.industry_id) " & _
  375. "LEFT JOIN (SELECT ba.business_id, MIN(a.name) AS first_area FROM business_areas ba " & _
  376. "INNER JOIN areas a ON a.id = ba.area_id GROUP BY ba.business_id) AS fa ON fa.business_id = b.id"
  377. End Function
  378. Private Function OrderByClause(f)
  379. Dim col, dirSql
  380. Select Case Val_(f, "sort")
  381. Case "city": col = "b.city"
  382. Case "industry": col = "i.name"
  383. Case "area": col = "fa.first_area"
  384. Case "status": col = "b.status"
  385. Case "last_contacted": col = "b.last_contacted_at"
  386. Case "next_action": col = "b.next_action_date"
  387. Case "added": col = "b.created_at"
  388. Case Else: col = "b.name"
  389. End Select
  390. If LCase(Val_(f, "dir")) = "desc" Then dirSql = "DESC" Else dirSql = "ASC"
  391. OrderByClause = col & " " & dirSql & ", b.id " & dirSql
  392. End Function
  393. Private Function Val_(f, key)
  394. If f.Exists(key) Then Val_ = CStr(f(key)) Else Val_ = ""
  395. End Function
  396. ' Escape LIKE wildcards so user input is matched literally.
  397. Private Function Like_(s)
  398. s = Replace(s, "[", "[[]")
  399. s = Replace(s, "%", "[%]")
  400. s = Replace(s, "_", "[_]")
  401. s = Replace(s, "*", "[*]")
  402. s = Replace(s, "?", "[?]")
  403. Like_ = s
  404. End Function
  405. End Class
  406. Dim BusinessRepository_Class__Singleton
  407. Function BusinessRepository()
  408. If IsEmpty(BusinessRepository_Class__Singleton) Then
  409. Set BusinessRepository_Class__Singleton = New BusinessRepository_Class
  410. End If
  411. Set BusinessRepository = BusinessRepository_Class__Singleton
  412. End Function
  413. %>

Powered by TurnKey Linux.