Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

425 wiersze
19KB

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

Powered by TurnKey Linux.