diff --git a/app/controllers/AreasController.asp b/app/controllers/AreasController.asp new file mode 100644 index 0000000..396d68a --- /dev/null +++ b/app/controllers/AreasController.asp @@ -0,0 +1,128 @@ +<% +Class AreasController_Class + Private m_useLayout + Private m_title + + Private Sub Class_Initialize() + m_useLayout = True + m_title = "Areas" + End Sub + + Public Property Get useLayout + useLayout = m_useLayout + End Property + + Public Property Let useLayout(v) + m_useLayout = v + End Property + + Public Property Get Title + Title = m_title + End Property + + Public Property Let Title(v) + m_title = v + End Property + + '------------------------------------------------------------------------------------------------------------------- + ' GET /areas + '------------------------------------------------------------------------------------------------------------------- + Public Sub index() + Dim list, unassigned, csrf + Set list = AreaRepository().All() + unassigned = AreaRepository().UnassignedCount() + csrf = Csrf_Token("areas") + %> + + <% + End Sub + + '------------------------------------------------------------------------------------------------------------------- + ' POST /areas/create + '------------------------------------------------------------------------------------------------------------------- + Public Sub create() + Dim name + GuardCsrf + name = Left(Trim(F("name")), 100) + If Len(name) = 0 Then Fail "Enter an area name." + If AreaRepository().NameTaken(name, 0) Then Fail """" & name & """ already exists." + AreaRepository().Create name + Flash().Success = "Added """ & name & """." + Response.Redirect "/areas" + End Sub + + '------------------------------------------------------------------------------------------------------------------- + ' POST /areas/:id/rename + '------------------------------------------------------------------------------------------------------------------- + Public Sub rename(id) + Dim area, name + Set area = Load(id) + name = Left(Trim(F("name")), 100) + If Len(name) = 0 Then Fail "The name can't be blank." + If AreaRepository().NameTaken(name, area("id")) Then + Fail """" & name & """ already exists. Use Merge to combine them." + End If + AreaRepository().Rename area("id"), name + Flash().Success = "Renamed to """ & name & """." + Response.Redirect "/areas" + End Sub + + '------------------------------------------------------------------------------------------------------------------- + ' POST /areas/:id/merge : move every business from this area into another one, then delete this one + '------------------------------------------------------------------------------------------------------------------- + Public Sub merge(id) + Dim area, target, moved + Set area = Load(id) + If Not Valid_Id(F("target_id")) Then Fail "Choose the area to merge into." + If CLng(F("target_id")) = area("id") Then Fail "Choose a different area to merge into." + Set target = AreaRepository().Find(CLng(F("target_id"))) + If target Is Nothing Then Fail "That area no longer exists." + moved = AreaRepository().Merge(area("id"), target("id")) + Flash().Success = "Merged """ & area("name") & """ into """ & target("name") & """ (" & moved & " businesses moved)." + Response.Redirect "/areas" + End Sub + + '------------------------------------------------------------------------------------------------------------------- + ' POST /areas/:id/delete : only when no business is in it + '------------------------------------------------------------------------------------------------------------------- + Public Sub delete(id) + Dim area + Set area = Load(id) + If area("n") > 0 Then Fail "Can't delete """ & area("name") & """ while " & area("n") & " businesses are in it. Merge it instead." + AreaRepository().Delete area("id") + Flash().Success = "Deleted """ & area("name") & """." + Response.Redirect "/areas" + End Sub + + '------------------------------------------------------------------------------------------------------------------- + Private Function Load(id) + GuardCsrf + If Not Valid_Id(id) Then Response.Redirect "/404" + Dim area : Set area = AreaRepository().Find(CLng(id)) + If area Is Nothing Then Response.Redirect "/404" + Set Load = area + End Function + + Private Sub GuardCsrf() + If Not Csrf_Valid("areas", F("csrf")) Then Fail "Invalid form state. Please try again." + End Sub + + Private Sub Fail(msg) + Flash().AddError msg + Response.Redirect "/areas" + End Sub + + Private Function F(name) + F = CStr(Request.Form(name)) + End Function + +End Class + +Dim AreasController_Class__Singleton +Function AreasController() + If IsEmpty(AreasController_Class__Singleton) Then + Set AreasController_Class__Singleton = New AreasController_Class + End If + Set AreasController = AreasController_Class__Singleton +End Function +%> diff --git a/app/controllers/BusinessesController.asp b/app/controllers/BusinessesController.asp index f253853..339ed23 100644 --- a/app/controllers/BusinessesController.asp +++ b/app/controllers/BusinessesController.asp @@ -29,10 +29,10 @@ Class BusinessesController_Class '------------------------------------------------------------------------------------------------------------------- Public Sub index() Const PER_PAGE = 50 - Dim f, k, page, pageCount, result, rows, total, industries, sort, dir, firstRow, lastRow + Dim f, k, page, pageCount, result, rows, total, industries, areas, sort, dir, firstRow, lastRow Set f = CreateObject("Scripting.Dictionary") - For Each k In Array("q", "industry", "status", "show", "contacted", "phone", "sort", "dir") + For Each k In Array("q", "industry", "area", "status", "show", "contacted", "phone", "sort", "dir") f(k) = Trim(CStr(Request.QueryString(k))) Next f("q") = Left(f("q"), 100) @@ -48,6 +48,7 @@ Class BusinessesController_Class total = result("total") Set rows = result("rows") Set industries = BusinessRepository().Industries() + Set areas = AreaRepository().All() pageCount = Int((total + PER_PAGE - 1) / PER_PAGE) If pageCount < 1 Then pageCount = 1 @@ -80,9 +81,21 @@ Class BusinessesController_Class ' Renders the detail page. dup (a matching other business) and fv (unsaved form values) are Nothing normally. Private Sub RenderShow(id, b, dup, fv) - Dim interactions, industries, csrf, today + Dim interactions, industries, allAreas, bizAreas, areaSel, ai, csrf, today Set interactions = BusinessRepository().Interactions(CLng(id)) Set industries = BusinessRepository().Industries() + Set allAreas = AreaRepository().All() + Set bizAreas = AreaRepository().ForBusiness(CLng(id)) + + ' Ticked area ids (CStr keys): the saved ones, or the unsaved selection when re-showing the form. + If fv Is Nothing Then + Set areaSel = CreateObject("Scripting.Dictionary") + For ai = 0 To bizAreas.Count - 1 + areaSel(CStr(bizAreas(ai)("id"))) = True + Next + Else + Set areaSel = fv("_area_ids") + End If csrf = Csrf_Token("business") today = Date_Iso(Date) m_title = b("name") @@ -95,7 +108,7 @@ Class BusinessesController_Class ' POST /businesses/:id/update : edit details '------------------------------------------------------------------------------------------------------------------- Public Sub update(id) - Dim d, email, phone, indName, indSel + Dim d, email, phone, indName, indSel, areaIds, areaNew, ai, areaId, areaName GuardPost id Set d = CreateObject("Scripting.Dictionary") @@ -129,6 +142,13 @@ Class BusinessesController_Class indSel = F("industry_id") If Valid_Id(indSel) Then d("industry_id") = CLng(indSel) Else d("industry_id") = Null + ' Areas: the ticked boxes, plus any comma-separated new names (created once the save is going ahead). + Set areaIds = CreateObject("Scripting.Dictionary") + For ai = 1 To Request.Form("area_ids").Count + If Valid_Id(Request.Form("area_ids")(ai)) Then areaIds(CStr(CLng(Request.Form("area_ids")(ai)))) = True + Next + areaNew = Left(Trim(F("area_new")), 500) + ' Warn when a changed phone/email is already used by another business (unless "Save anyway" was pressed). If F("allow_duplicate") <> "1" Then Dim existing, chkPhone, chkEmail, dupMatch, fv @@ -144,6 +164,8 @@ Class BusinessesController_Class fv(k) = d(k) Next fv("_industry_new") = indName + fv("_area_new") = areaNew + Set fv("_area_ids") = areaIds RenderShow id, existing, dupMatch, fv Exit Sub End If @@ -152,7 +174,13 @@ Class BusinessesController_Class ' A typed-in industry wins over the dropdown; it is only created once the save is going ahead. If Len(indName) > 0 Then d("industry_id") = BusinessRepository().FindOrCreateIndustry(indName) + For Each areaName In Split(areaNew, ",") + areaId = AreaRepository().FindOrCreate(areaName) + If Not IsNull(areaId) Then areaIds(CStr(areaId)) = True + Next + BusinessRepository().Update CLng(id), d + AreaRepository().SetForBusiness CLng(id), areaIds Flash().Success = "Saved." Response.Redirect "/businesses/" & id End Sub diff --git a/app/controllers/ExportController.asp b/app/controllers/ExportController.asp index 00a626b..82df023 100644 --- a/app/controllers/ExportController.asp +++ b/app/controllers/ExportController.asp @@ -28,10 +28,10 @@ Class ExportController_Class ' GET /export/businesses : CSV of everything matching the list's search/filters (opens directly in Excel) '------------------------------------------------------------------------------------------------------------------- Public Sub businesses() - Dim f, k, rs, line, n, fileName + Dim f, k, rs, line, n, fileName, areaNames, bizAreas Set f = CreateObject("Scripting.Dictionary") - For Each k In Array("q", "industry", "status", "show", "contacted", "phone", "sort", "dir") + For Each k In Array("q", "industry", "area", "status", "show", "contacted", "phone", "sort", "dir") f(k) = Trim(CStr(Request.QueryString(k))) Next f("q") = Left(f("q"), 100) @@ -47,12 +47,16 @@ Class ExportController_Class Response.AddHeader "Content-Disposition", "attachment; filename=""" & fileName & """" Response.Write ChrW(&HFEFF) ' UTF-8 BOM so Excel reads accents correctly - Response.Write "Business,Contact,Industry,Phone,Email,Website,Address,City,State,Zip,Status,Customer,Last contacted,Next action" & vbCrLf + Response.Write "Business,Contact,Industry,Areas,Phone,Email,Website,Address,City,State,Zip,Status,Customer,Last contacted,Next action" & vbCrLf + Set areaNames = AreaRepository().NamesByBusiness("") Set rs = BusinessRepository().ExportRows(f) n = 0 Do While Not rs.EOF + bizAreas = "" + If areaNames.Exists(CStr(rs("id"))) Then bizAreas = areaNames(CStr(rs("id"))) line = Csv_Cell(rs("name")) & "," & Csv_Cell(rs("contact_name")) & "," & Csv_Cell(rs("industry_name")) & "," & _ + Csv_Cell(bizAreas) & "," & _ Csv_Cell(rs("phone")) & "," & Csv_Cell(rs("email")) & "," & Csv_Cell(rs("website")) & "," & _ Csv_Cell(rs("address1")) & "," & Csv_Cell(rs("city")) & "," & Csv_Cell(rs("state")) & "," & _ Csv_Cell(rs("zip")) & "," & Csv_Cell(Businesses_StatusLabel(rs("status"))) & "," & _ diff --git a/app/controllers/autoload_controllers.asp b/app/controllers/autoload_controllers.asp index c52e8d3..5a3f906 100644 --- a/app/controllers/autoload_controllers.asp +++ b/app/controllers/autoload_controllers.asp @@ -6,12 +6,14 @@ + + diff --git a/app/lib/lib.BusinessHelpers.asp b/app/lib/lib.BusinessHelpers.asp index f87bbae..7d53b43 100644 --- a/app/lib/lib.BusinessHelpers.asp +++ b/app/lib/lib.BusinessHelpers.asp @@ -62,7 +62,7 @@ End Function Function Businesses_QS(f, page, sort, dir) Dim k, s s = "" - For Each k In Array("q", "industry", "status", "show", "contacted", "phone") + For Each k In Array("q", "industry", "area", "status", "show", "contacted", "phone") If f.Exists(k) Then If Len(f(k)) > 0 Then s = s & "&" & k & "=" & Server.URLEncode(f(k)) End If diff --git a/app/repositories/AreaRepository.asp b/app/repositories/AreaRepository.asp new file mode 100644 index 0000000..f9e1ef9 --- /dev/null +++ b/app/repositories/AreaRepository.asp @@ -0,0 +1,190 @@ +<% +'======================================================================================================================= +' AREA REPOSITORY +' Areas (cities, regions...) and which businesses cover them. A business can be linked to any number of areas +' through business_areas (business_id, area_id). +'======================================================================================================================= +Class AreaRepository_Class + + ' All areas with the number of businesses in each: list of Dictionary(id, name, n) + Public Function All() + Dim list, rs, d + Set list = CreateObject("Scripting.Dictionary") + Set rs = Db_Query("SELECT a.id, a.name, COUNT(ba.business_id) AS n FROM areas a LEFT JOIN business_areas ba ON ba.area_id = a.id GROUP BY a.id, a.name ORDER BY a.name", Array()) + Do While Not rs.EOF + Set d = CreateObject("Scripting.Dictionary") + d("id") = CLng(rs(0)) : d("name") = rs(1) & "" : d("n") = CLng(rs(2)) + list.Add list.Count, d + rs.MoveNext + Loop + rs.Close + Set All = list + End Function + + ' Businesses not linked to any area. + Public Function UnassignedCount() + Dim rs + Set rs = Db_Query("SELECT COUNT(*) FROM businesses b WHERE NOT EXISTS (SELECT ba.area_id FROM business_areas ba WHERE ba.business_id = b.id)", Array()) + UnassignedCount = CLng(rs(0)) + rs.Close + End Function + + Public Function Find(id) + Dim rs + Set rs = Db_Query("SELECT a.id, a.name, (SELECT COUNT(*) FROM business_areas ba WHERE ba.area_id = a.id) AS n FROM areas a WHERE a.id = ?", Array(Db_PI(id))) + If rs.EOF Then + Set Find = Nothing + Else + Dim d : Set d = CreateObject("Scripting.Dictionary") + d("id") = CLng(rs(0)) : d("name") = rs(1) & "" : d("n") = CLng(rs(2)) + Set Find = d + End If + rs.Close + End Function + + ' True when another area (not excludeId) already has this name (case-insensitive). + Public Function NameTaken(name, excludeId) + Dim rs + Set rs = Db_Query("SELECT COUNT(*) FROM areas WHERE name = ? AND id <> ?", Array(Db_PS(name), Db_PI(excludeId))) + NameTaken = (CLng(rs(0)) > 0) + rs.Close + End Function + + Public Sub Create(name) + Db_Exec "INSERT INTO areas (name) VALUES (?)", Array(Db_PS(name)) + End Sub + + ' Returns the area id for a name, creating the area when it does not exist. Null for a blank name. + Public Function FindOrCreate(ByVal name) + Dim rs + name = Left(Trim(name), 100) + If Len(name) = 0 Then + FindOrCreate = Null + Exit Function + End If + Set rs = Db_Query("SELECT id FROM areas WHERE name = ?", Array(Db_PS(name))) + If Not rs.EOF Then + FindOrCreate = CLng(rs(0)) + rs.Close + Exit Function + End If + rs.Close + Create name + Set rs = Db_Query("SELECT @@IDENTITY", Array()) + FindOrCreate = CLng(rs(0)) + rs.Close + End Function + + Public Sub Rename(id, name) + Db_Exec "UPDATE areas SET name = ? WHERE id = ?", Array(Db_PS(name), Db_PI(id)) + End Sub + + ' Moves every business from sourceId to targetId (a business already in both keeps one link), then deletes + ' sourceId. Returns the number of businesses that were in sourceId. + Public Function Merge(sourceId, targetId) + Dim conn, moved, rs, msg + Set conn = DAL().Connection + Set rs = Db_Query("SELECT COUNT(*) FROM business_areas WHERE area_id = ?", Array(Db_PI(sourceId))) + moved = CLng(rs(0)) + rs.Close + + conn.BeginTrans + On Error Resume Next + ' Ids inlined as CLng: Access binds ? markers inside a subquery ahead of the outer ones. + Db_Exec "DELETE FROM business_areas WHERE area_id = " & CLng(sourceId) & _ + " AND business_id IN (SELECT x.business_id FROM business_areas x WHERE x.area_id = " & CLng(targetId) & ")", Array() + If Err.Number = 0 Then Db_Exec "UPDATE business_areas SET area_id = ? WHERE area_id = ?", Array(Db_PI(targetId), Db_PI(sourceId)) + If Err.Number = 0 Then Db_Exec "DELETE FROM areas WHERE id = ?", Array(Db_PI(sourceId)) + If Err.Number <> 0 Then + msg = Err.Description + Err.Clear + conn.RollbackTrans + On Error GoTo 0 + Err.Raise 5, "AreaRepository.Merge", msg + End If + conn.CommitTrans + On Error GoTo 0 + Merge = moved + End Function + + Public Sub Delete(id) + Db_Exec "DELETE FROM areas WHERE id = ?", Array(Db_PI(id)) + End Sub + + '--------------------------------------------------------------------------------------------------------------------- + ' Links to businesses + '--------------------------------------------------------------------------------------------------------------------- + + ' Areas of one business: list of Dictionary(id, name), by name. + Public Function ForBusiness(businessId) + Dim list, rs, d + Set list = CreateObject("Scripting.Dictionary") + Set rs = Db_Query("SELECT a.id, a.name FROM business_areas ba INNER JOIN areas a ON a.id = ba.area_id WHERE ba.business_id = ? ORDER BY a.name", Array(Db_PI(businessId))) + Do While Not rs.EOF + Set d = CreateObject("Scripting.Dictionary") + d("id") = CLng(rs(0)) : d("name") = rs(1) & "" + list.Add list.Count, d + rs.MoveNext + Loop + rs.Close + Set ForBusiness = list + End Function + + ' Area names for many businesses at once: Dictionary CStr(business id) -> "Name, Name". ids is a comma-separated + ' list of numeric ids (callers build it from CLng values) or "" for every business. + Public Function NamesByBusiness(ids) + Dim names, rs, sql, k + Set names = CreateObject("Scripting.Dictionary") + sql = "SELECT ba.business_id, a.name FROM business_areas ba INNER JOIN areas a ON a.id = ba.area_id" + If Len(ids) > 0 Then sql = sql & " WHERE ba.business_id IN (" & ids & ")" + Set rs = Db_Query(sql & " ORDER BY ba.business_id, a.name", Array()) + Do While Not rs.EOF + k = CStr(rs(0)) + If names.Exists(k) Then names(k) = names(k) & ", " & rs(1) Else names(k) = rs(1) & "" + rs.MoveNext + Loop + rs.Close + Set NamesByBusiness = names + End Function + + ' Replaces the areas of a business with areaIds (Dictionary whose keys are area ids). Unknown ids are ignored. + Public Sub SetForBusiness(businessId, areaIds) + Dim conn, known, rs, k, msg + Set known = CreateObject("Scripting.Dictionary") + Set rs = Db_Query("SELECT id FROM areas", Array()) + Do While Not rs.EOF + known(CStr(rs(0))) = True + rs.MoveNext + Loop + rs.Close + + Set conn = DAL().Connection + conn.BeginTrans + On Error Resume Next + Db_Exec "DELETE FROM business_areas WHERE business_id = ?", Array(Db_PI(businessId)) + For Each k In areaIds.Keys + If Err.Number <> 0 Then Exit For + If known.Exists(CStr(k)) Then + Db_Exec "INSERT INTO business_areas (business_id, area_id) VALUES (?, ?)", Array(Db_PI(businessId), Db_PI(k)) + End If + Next + If Err.Number <> 0 Then + msg = Err.Description + Err.Clear + conn.RollbackTrans + On Error GoTo 0 + Err.Raise 5, "AreaRepository.SetForBusiness", msg + End If + conn.CommitTrans + On Error GoTo 0 + End Sub +End Class + +Dim AreaRepository_Class__Singleton +Function AreaRepository() + If IsEmpty(AreaRepository_Class__Singleton) Then + Set AreaRepository_Class__Singleton = New AreaRepository_Class + End If + Set AreaRepository = AreaRepository_Class__Singleton +End Function +%> diff --git a/app/repositories/BusinessRepository.asp b/app/repositories/BusinessRepository.asp index ac77d20..90314e4 100644 --- a/app/repositories/BusinessRepository.asp +++ b/app/repositories/BusinessRepository.asp @@ -7,11 +7,13 @@ ' List filters (Scripting.Dictionary, all optional): ' q free text: every whitespace-separated term must match name, contact, email, city or phone ' industry "" = any, "0" = no industry, otherwise an industries.id +' area "" = any, "0" = no area, otherwise an areas.id (businesses linked to that area) ' status "active" (default, hides removed), "all", or new | contacted | follow_up | removed ' show "" = everyone, "prospects", "customers" ' contacted "never" = only businesses with no contact yet ' phone "1" = only businesses with a phone number -' sort name | city | industry | status | last_contacted | next_action | added (default name) +' sort name | city | industry | area | status | last_contacted | next_action | added (default name) +' area sorts by each business's alphabetically first area; businesses with none sort first ' dir asc | desc '======================================================================================================================= Class BusinessRepository_Class @@ -38,8 +40,7 @@ Class BusinessRepository_Class ' Access has no OFFSET: read TOP (skip + perPage) rows and step past the first `skip` of them. sql = "SELECT TOP " & CLng(skip + perPage) & " b.id, b.name, b.contact_name, b.phone, b.email, b.website, " & _ "b.city, b.state, b.status, b.is_customer, b.last_contacted_at, b.next_action_date, i.name AS industry_name " & _ - "FROM businesses b LEFT JOIN industries i ON i.id = b.industry_id " & _ - "WHERE " & m_where & " ORDER BY " & OrderByClause(f) + "FROM " & ListFrom() & " WHERE " & m_where & " ORDER BY " & OrderByClause(f) Set rs = Run(sql) For i = 1 To skip @@ -49,6 +50,20 @@ Class BusinessRepository_Class Set rows = ReadRows(rs) rs.Close + ' Each row gets "areas": its area names, comma-separated. + Dim ids, names, k + ids = "" + For i = 0 To rows.Count - 1 + If Len(ids) > 0 Then ids = ids & "," + ids = ids & CLng(rows(i)("id")) + Next + Set names = CreateObject("Scripting.Dictionary") + If Len(ids) > 0 Then Set names = AreaRepository().NamesByBusiness(ids) + For i = 0 To rows.Count - 1 + k = CStr(rows(i)("id")) + If names.Exists(k) Then rows(i)("areas") = names(k) Else rows(i)("areas") = "" + Next + Set result = CreateObject("Scripting.Dictionary") Set result("rows") = rows result("total") = total @@ -58,10 +73,9 @@ Class BusinessRepository_Class ' Every business matching the filters (no paging), as a forward-only recordset for streaming to CSV. Public Function ExportRows(f) BuildWhere f - Set ExportRows = Run("SELECT b.name, b.contact_name, i.name AS industry_name, b.phone, b.email, b.website, " & _ + Set ExportRows = Run("SELECT b.id, b.name, b.contact_name, i.name AS industry_name, b.phone, b.email, b.website, " & _ "b.address1, b.city, b.state, b.zip, b.status, b.is_customer, b.last_contacted_at, b.next_action_date " & _ - "FROM businesses b LEFT JOIN industries i ON i.id = b.industry_id " & _ - "WHERE " & m_where & " ORDER BY " & OrderByClause(f)) + "FROM " & ListFrom() & " WHERE " & m_where & " ORDER BY " & OrderByClause(f)) End Function ' Businesses with a next-contact date on or before throughDate (removed ones excluded), oldest first, @@ -296,7 +310,7 @@ Class BusinessRepository_Class End Sub Private Sub BuildWhere(f) - Dim terms, t, d, q, n, frag, ind, st + Dim terms, t, d, q, n, frag, ind, ar, st m_where = "1=1" Set m_params = CreateObject("Scripting.Dictionary") @@ -331,6 +345,15 @@ Class BusinessRepository_Class AddParam 3, CLng(ind) End If + ar = Val_(f, "area") + If ar = "0" Then + m_where = m_where & " AND NOT EXISTS (SELECT ba.area_id FROM business_areas ba WHERE ba.business_id = b.id)" + ElseIf Valid_Id(ar) Then + ' Inlined as a validated CLng, not a ? marker: Access binds markers inside a subquery ahead of the outer + ' ones, so a parameter here would swap values with the search terms before it. + 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) & ")" + End If + st = Val_(f, "status") Select Case st Case "all" @@ -352,11 +375,20 @@ Class BusinessRepository_Class If Val_(f, "phone") = "1" Then m_where = m_where & " AND b.phone_digits IS NOT NULL" End Sub + ' FROM clause for the list and export: the business, its industry, and fa.first_area (its alphabetically first + ' area name) for sorting. Access won't take a subquery in ORDER BY, hence the derived table. + Private Function ListFrom() + ListFrom = "(businesses b LEFT JOIN industries i ON i.id = b.industry_id) " & _ + "LEFT JOIN (SELECT ba.business_id, MIN(a.name) AS first_area FROM business_areas ba " & _ + "INNER JOIN areas a ON a.id = ba.area_id GROUP BY ba.business_id) AS fa ON fa.business_id = b.id" + End Function + Private Function OrderByClause(f) Dim col, dirSql Select Case Val_(f, "sort") Case "city": col = "b.city" Case "industry": col = "i.name" + Case "area": col = "fa.first_area" Case "status": col = "b.status" Case "last_contacted": col = "b.last_contacted_at" Case "next_action": col = "b.next_action_date" diff --git a/app/views/Areas/index.asp b/app/views/Areas/index.asp new file mode 100644 index 0000000..2870871 --- /dev/null +++ b/app/views/Areas/index.asp @@ -0,0 +1,86 @@ +<% Flash().ShowErrorsIfPresent : Flash().ShowSuccessIfPresent %> + +
| Name | Businesses | Rename | Merge into… | |
|---|---|---|---|---|
| <%= H(row("name")) %> | +&status=all" class="text-decoration-none"><%= FormatNumber(row("n"), 0) %> | ++ + | ++ + | ++ <% If row("n") = 0 Then %> + + <% End If %> + | +
| (No area) | +<%= FormatNumber(unassigned, 0) %> | ++ | ||