File: D:/web/hyperflight/apps/sales-analytics/products-v9.asp
<%
Option Explicit
Response.Buffer = True
Response.CodePage = 65001
Response.CharSet = "utf-8"
' ======================================================================
' HyperFlight Product Analysis v2
' Companion page for the standalone Sales Map application
' Bootstrap 5 / Classic ASP / MySQL
' ======================================================================
Const adCmdText = 1
Const adParamInput = 1
Const adVarChar = 200
Dim conn, cmd, rs
Dim countryCmd, countryRs, categoryCmd, categoryRs, summaryCmd, summaryRs
Dim subcategoryCmd, subcategoryRs, trendCmd, trendRs
Dim startDate, endDate, requestedStartDate, requestedEndDate, dateRange, dateRangeName
Dim measure, chartView, ukMode, topLimit, breakdown, countryFilter, categoryFilter
Dim trendInterval, trendCategory, trendSubcategory, yearlyTrendAvailable
Dim validationMessage, dataError, rangeLabel
Dim sql, countrySql, categorySql, subcategorySql, summarySql, trendSql, orderBySql
Dim joinsSql, whereSql, categoryExpr, subcategoryExpr, productCodeExpr
Dim effectiveCountryExpr, countryCodeExpr, countryNameExpr
Dim grossBasisExpr, vatAmountExpr, discountFactorExpr, exVatFactorExpr, lineSalesExpr
Dim dataRows, rowCount, countryRows, countryRowCount, categoryRows, categoryRowCount
Dim subcategoryRows, subcategoryRowCount, trendRows, trendRowCount
Dim trendYear, trendMonth
Dim i, tableCount, barCount, pieCount
Dim currentCategory, currentSubcategory, currentGroupLabel
Dim currentOrders, currentQuantity, currentSales, currentValue, shareValue
Dim totalOrders, totalQuantity, totalSales, totalSelected, productCount
Dim topGroupName, topGroupValue, topGroupShare
Dim pieOtherValue, pieDisplayedTotal
Dim measureLabel, valueColumnLabel, groupLabel, chartTitle, chartSubtitle
Dim countryListCode, countryListName, countryFound, countryFilterName
Dim categoryListName, categoryFound, trendCategoryFound
Dim subcategoryListName, trendSubcategoryFound
Function IsoDate(ByVal value)
IsoDate = Year(value) & "-" & Right("0" & Month(value), 2) & "-" & Right("0" & Day(value), 2)
End Function
Function IsValidIsoDate(ByVal value)
Dim parts, testDate
IsValidIsoDate = False
If Len(value) <> 10 Then Exit Function
parts = Split(value, "-")
If UBound(parts) <> 2 Then Exit Function
If Not IsNumeric(parts(0)) Then Exit Function
If Not IsNumeric(parts(1)) Then Exit Function
If Not IsNumeric(parts(2)) Then Exit Function
On Error Resume Next
testDate = DateSerial(CInt(parts(0)), CInt(parts(1)), CInt(parts(2)))
If Err.Number = 0 Then
If Year(testDate) = CInt(parts(0)) And Month(testDate) = CInt(parts(1)) And Day(testDate) = CInt(parts(2)) Then
IsValidIsoDate = True
End If
End If
Err.Clear
On Error GoTo 0
End Function
Function ParseIsoDate(ByVal value)
Dim parts
parts = Split(value, "-")
ParseIsoDate = DateSerial(CInt(parts(0)), CInt(parts(1)), CInt(parts(2)))
End Function
Function FriendlyDate(ByVal value)
FriendlyDate = Day(value) & " " & MonthName(Month(value), True) & " " & Year(value)
End Function
Function Html(ByVal value)
If IsNull(value) Then
Html = ""
Else
Html = Server.HTMLEncode(CStr(value))
End If
End Function
Function JsString(ByVal value)
Dim textValue
If IsNull(value) Then
JsString = ""
Exit Function
End If
textValue = CStr(value)
textValue = Replace(textValue, "\", "\\")
textValue = Replace(textValue, Chr(34), "\" & Chr(34))
textValue = Replace(textValue, "'", "\'")
textValue = Replace(textValue, vbCr, "\r")
textValue = Replace(textValue, vbLf, "\n")
textValue = Replace(textValue, "</", "<\/")
JsString = textValue
End Function
Function JsNumber(ByVal value)
Dim textValue
If IsNull(value) Or IsEmpty(value) Then
JsNumber = "0"
Else
textValue = CStr(CDbl(value))
textValue = Replace(textValue, ",", ".")
JsNumber = textValue
End If
End Function
Function Number0(ByVal value)
If IsNull(value) Or IsEmpty(value) Then
Number0 = "0"
Else
Number0 = FormatNumber(CDbl(value), 0)
End If
End Function
Function Money0(ByVal value)
If IsNull(value) Or IsEmpty(value) Then
Money0 = "£0"
Else
Money0 = "£" & FormatNumber(CDbl(value), 0)
End If
End Function
Function BuildViewUrl(ByVal requestedView)
BuildViewUrl = "?DateRange=" & Server.URLEncode(dateRange) & _
"&StartDate=" & Server.URLEncode(startDate) & _
"&EndDate=" & Server.URLEncode(endDate) & _
"&Measure=" & Server.URLEncode(measure) & _
"&UK=" & Server.URLEncode(ukMode) & _
"&Country=" & Server.URLEncode(countryFilter) & _
"&Breakdown=" & Server.URLEncode(breakdown) & _
"&Category=" & Server.URLEncode(categoryFilter) & _
"&TopLimit=" & topLimit & _
"&TrendInterval=" & Server.URLEncode(trendInterval) & _
"&TrendCategory=" & Server.URLEncode(trendCategory) & _
"&TrendSubcategory=" & Server.URLEncode(trendSubcategory) & _
"&View=" & Server.URLEncode(requestedView)
End Function
Function BuildTrendIntervalUrl(ByVal requestedInterval)
BuildTrendIntervalUrl = "?DateRange=" & Server.URLEncode(dateRange) & _
"&StartDate=" & Server.URLEncode(startDate) & _
"&EndDate=" & Server.URLEncode(endDate) & _
"&Measure=" & Server.URLEncode(measure) & _
"&UK=" & Server.URLEncode(ukMode) & _
"&Country=" & Server.URLEncode(countryFilter) & _
"&Breakdown=" & Server.URLEncode(breakdown) & _
"&Category=" & Server.URLEncode(categoryFilter) & _
"&TopLimit=" & topLimit & _
"&TrendInterval=" & Server.URLEncode(requestedInterval) & _
"&TrendCategory=" & Server.URLEncode(trendCategory) & _
"&TrendSubcategory=" & Server.URLEncode(trendSubcategory) & _
"&View=trend"
End Function
Function BuildBreakdownUrl(ByVal requestedBreakdown)
Dim requestedCategory
requestedCategory = categoryFilter
If requestedBreakdown = "category" Then requestedCategory = "all"
BuildBreakdownUrl = "?DateRange=" & Server.URLEncode(dateRange) & _
"&StartDate=" & Server.URLEncode(startDate) & _
"&EndDate=" & Server.URLEncode(endDate) & _
"&Measure=" & Server.URLEncode(measure) & _
"&UK=" & Server.URLEncode(ukMode) & _
"&Country=" & Server.URLEncode(countryFilter) & _
"&Breakdown=" & Server.URLEncode(requestedBreakdown) & _
"&Category=" & Server.URLEncode(requestedCategory) & _
"&TopLimit=" & topLimit & _
"&TrendInterval=" & Server.URLEncode(trendInterval) & _
"&TrendCategory=" & Server.URLEncode(trendCategory) & _
"&TrendSubcategory=" & Server.URLEncode(trendSubcategory) & _
"&View=" & Server.URLEncode(chartView)
End Function
Function BuildSalesMapUrl()
BuildSalesMapUrl = "index.asp?DateRange=" & Server.URLEncode(dateRange) & _
"&StartDate=" & Server.URLEncode(startDate) & _
"&EndDate=" & Server.URLEncode(endDate) & _
"&Measure=sales" & _
"&UK=" & Server.URLEncode(ukMode) & _
"&TrendCountry=" & Server.URLEncode(countryFilter) & _
"&View=bar"
End Function
Sub AppendCommonParameters(ByRef queryCommand, ByVal includeCountry, ByVal includeCategory)
If dateRange <> "all" Then
queryCommand.Parameters.Append queryCommand.CreateParameter("@DateFrom", adVarChar, adParamInput, 10, startDate)
End If
queryCommand.Parameters.Append queryCommand.CreateParameter("@DateTo", adVarChar, adParamInput, 10, endDate)
If includeCountry And countryFilter <> "all" Then
queryCommand.Parameters.Append queryCommand.CreateParameter("@Country", adVarChar, adParamInput, 10, countryFilter)
End If
If includeCategory And categoryFilter <> "all" Then
queryCommand.Parameters.Append queryCommand.CreateParameter("@Category", adVarChar, adParamInput, 75, categoryFilter)
End If
End Sub
Sub AppendTrendParameters(ByRef queryCommand, ByVal includeCategory, ByVal includeSubcategory)
If dateRange <> "all" Then
queryCommand.Parameters.Append queryCommand.CreateParameter("@TrendDateFrom", adVarChar, adParamInput, 10, startDate)
End If
queryCommand.Parameters.Append queryCommand.CreateParameter("@TrendDateTo", adVarChar, adParamInput, 10, endDate)
If countryFilter <> "all" Then
queryCommand.Parameters.Append queryCommand.CreateParameter("@TrendCountry", adVarChar, adParamInput, 10, countryFilter)
End If
If includeCategory And trendCategory <> "all" Then
queryCommand.Parameters.Append queryCommand.CreateParameter("@TrendCategory", adVarChar, adParamInput, 75, trendCategory)
End If
If includeSubcategory And trendSubcategory <> "all" Then
queryCommand.Parameters.Append queryCommand.CreateParameter("@TrendSubcategory", adVarChar, adParamInput, 75, trendSubcategory)
End If
End Sub
' ----------------------------------------------------------------------
' Filters and defaults
' ----------------------------------------------------------------------
requestedStartDate = Trim(Request.QueryString("StartDate"))
requestedEndDate = Trim(Request.QueryString("EndDate"))
dateRange = LCase(Trim(Request.QueryString("DateRange")))
measure = LCase(Trim(Request.QueryString("Measure")))
chartView = LCase(Trim(Request.QueryString("View")))
ukMode = LCase(Trim(Request.QueryString("UK")))
topLimit = Trim(Request.QueryString("TopLimit"))
breakdown = LCase(Trim(Request.QueryString("Breakdown")))
countryFilter = Trim(Request.QueryString("Country"))
categoryFilter = Trim(Request.QueryString("Category"))
trendInterval = LCase(Trim(Request.QueryString("TrendInterval")))
trendCategory = Trim(Request.QueryString("TrendCategory"))
trendSubcategory = Trim(Request.QueryString("TrendSubcategory"))
If dateRange = "" Then
If requestedStartDate <> "" Or requestedEndDate <> "" Then
dateRange = "custom"
Else
dateRange = "12months"
End If
End If
endDate = IsoDate(Date())
Select Case dateRange
Case "30days"
dateRangeName = "Past 30 days"
startDate = IsoDate(DateAdd("d", -29, Date()))
Case "60days"
dateRangeName = "Past 60 days"
startDate = IsoDate(DateAdd("d", -59, Date()))
Case "90days"
dateRangeName = "Past 90 days"
startDate = IsoDate(DateAdd("d", -89, Date()))
Case "thisquarter"
dateRangeName = "This quarter"
startDate = IsoDate(DateSerial(Year(Date()), ((Month(Date()) - 1) \ 3) * 3 + 1, 1))
Case "previousquarter"
dateRangeName = "Previous quarter"
startDate = IsoDate(DateAdd("m", -3, DateSerial(Year(Date()), ((Month(Date()) - 1) \ 3) * 3 + 1, 1)))
endDate = IsoDate(DateAdd("d", -1, DateSerial(Year(Date()), ((Month(Date()) - 1) \ 3) * 3 + 1, 1)))
Case "6months"
dateRangeName = "Past 6 months"
startDate = IsoDate(DateAdd("m", -6, Date()))
Case "12months"
dateRangeName = "Past 12 months"
startDate = IsoDate(DateAdd("m", -12, Date()))
Case "yeartodate"
dateRangeName = "Year to date"
startDate = IsoDate(DateSerial(Year(Date()), 1, 1))
Case "lastyear"
dateRangeName = "Last calendar year"
startDate = IsoDate(DateSerial(Year(Date()) - 1, 1, 1))
endDate = IsoDate(DateSerial(Year(Date()) - 1, 12, 31))
Case "2years"
dateRangeName = "Past 2 years"
startDate = IsoDate(DateAdd("yyyy", -2, Date()))
Case "3years"
dateRangeName = "Past 3 years"
startDate = IsoDate(DateAdd("yyyy", -3, Date()))
Case "5years"
dateRangeName = "Past 5 years"
startDate = IsoDate(DateAdd("yyyy", -5, Date()))
Case "all"
dateRangeName = "All time"
startDate = ""
Case "custom"
dateRangeName = "Custom dates"
startDate = requestedStartDate
endDate = requestedEndDate
Case Else
dateRange = "12months"
dateRangeName = "Past 12 months"
startDate = IsoDate(DateAdd("m", -12, Date()))
End Select
If measure = "" Then measure = "sales"
If chartView = "" Then chartView = "bar"
If ukMode = "" Then ukMode = "include"
If breakdown = "" Then breakdown = "category"
If topLimit = "" Or Not IsNumeric(topLimit) Then topLimit = 15
If countryFilter = "" Or LCase(countryFilter) = "all" Then countryFilter = "all" Else countryFilter = UCase(countryFilter)
If categoryFilter = "" Or LCase(categoryFilter) = "all" Then categoryFilter = "all"
If trendInterval = "" Then trendInterval = "monthly"
If trendCategory = "" Or LCase(trendCategory) = "all" Then trendCategory = "all"
If trendSubcategory = "" Or LCase(trendSubcategory) = "all" Then trendSubcategory = "all"
If trendCategory = "all" Then trendSubcategory = "all"
topLimit = CInt(topLimit)
If topLimit <> 10 And topLimit <> 15 And topLimit <> 20 And topLimit <> 30 Then topLimit = 15
Select Case measure
Case "sales", "quantity"
' Valid.
Case Else
measure = "sales"
End Select
Select Case chartView
Case "bar", "pie", "trend"
' Valid.
Case Else
chartView = "bar"
End Select
Select Case trendInterval
Case "monthly", "yearly"
' Valid.
Case Else
trendInterval = "monthly"
End Select
Select Case ukMode
Case "include", "exclude"
' Valid.
Case Else
ukMode = "include"
End Select
Select Case breakdown
Case "category", "subcategory"
' Valid.
Case Else
breakdown = "category"
End Select
If breakdown = "category" Then categoryFilter = "all"
validationMessage = ""
If dateRange = "all" Then
If Not IsValidIsoDate(endDate) Then validationMessage = "Please enter a valid end date."
ElseIf Not IsValidIsoDate(startDate) Or Not IsValidIsoDate(endDate) Then
validationMessage = "Please enter valid start and end dates."
ElseIf ParseIsoDate(startDate) > ParseIsoDate(endDate) Then
validationMessage = "The start date must not be later than the end date."
End If
yearlyTrendAvailable = False
If validationMessage = "" Then
If dateRange = "all" Then
yearlyTrendAvailable = True
ElseIf DateDiff("d", ParseIsoDate(startDate), ParseIsoDate(endDate)) >= 365 Then
yearlyTrendAvailable = True
End If
End If
If Not yearlyTrendAvailable And trendInterval = "yearly" Then trendInterval = "monthly"
If validationMessage = "" Then
If dateRange = "all" Then
rangeLabel = Html(dateRangeName)
Else
rangeLabel = Html(dateRangeName) & " · " & _
Html(FriendlyDate(ParseIsoDate(startDate))) & " to " & _
Html(FriendlyDate(ParseIsoDate(endDate)))
End If
Else
rangeLabel = Html(dateRangeName)
End If
If measure = "quantity" Then
orderBySql = "QuantityTotal"
measureLabel = "Quantity sold"
valueColumnLabel = "Quantity"
Else
orderBySql = "SalesTotal"
measureLabel = "Sales value"
valueColumnLabel = "Sales"
End If
If breakdown = "subcategory" Then
groupLabel = "Subcategory"
If categoryFilter = "all" Then
chartTitle = "Top subcategories"
chartSubtitle = measureLabel & " by category and subcategory"
Else
chartTitle = "Top subcategories"
chartSubtitle = measureLabel & " within " & categoryFilter
End If
Else
groupLabel = "Category"
chartTitle = "Top categories"
chartSubtitle = measureLabel & " by main category"
End If
If chartView = "pie" Then
If breakdown = "subcategory" Then
chartTitle = "Subcategory share"
Else
chartTitle = "Category share"
End If
End If
' ----------------------------------------------------------------------
' SQL expressions and joins
' ----------------------------------------------------------------------
effectiveCountryExpr = "COALESCE(NULLIF(TRIM(o.DeliveryCountry), ''), TRIM(o.Country))"
countryCodeExpr = "CASE WHEN LEFT(c.Country, 14) = 'United Kingdom' THEN 'GB' ELSE UPPER(c.CodeA2) END"
countryNameExpr = "CASE WHEN LEFT(c.Country, 14) = 'United Kingdom' THEN 'United Kingdom' ELSE c.Country END"
productCodeExpr = "COALESCE(NULLIF(TRIM(p.ProductCode), ''), TRIM(od.ProductCode))"
categoryExpr = "COALESCE(NULLIF(TRIM(pc.Category), ''), 'Uncategorised')"
subcategoryExpr = "COALESCE(NULLIF(TRIM(pc.Subcategory), ''), 'Uncategorised')"
grossBasisExpr = "(IFNULL(o.Subtotal, 0) + IFNULL(o.Discount, 0) + IFNULL(o.Delivery, 0))"
vatAmountExpr = "(CASE WHEN IFNULL(o.VATDeducted, 0) > 0 THEN IFNULL(o.VATDeducted, 0) ELSE IFNULL(o.VATIncluded, 0) END)"
discountFactorExpr = "(CASE WHEN IFNULL(o.Subtotal, 0) <> 0 THEN 1 + (IFNULL(o.Discount, 0) / o.Subtotal) ELSE 1 END)"
exVatFactorExpr = "(CASE WHEN " & grossBasisExpr & " <> 0 THEN (" & grossBasisExpr & " - " & vatAmountExpr & ") / " & grossBasisExpr & " ELSE 1 END)"
lineSalesExpr = "(IFNULL(od.Qty, 0) * IFNULL(od.PriceEach, 0) * " & discountFactorExpr & " * " & exVatFactorExpr & ")"
joinsSql = "FROM orders o " & _
"INNER JOIN orderdetails od ON od.OrderNo = o.OrderNo " & _
"LEFT JOIN products p ON p.ProductID = od.ProductID " & _
"LEFT JOIN (" & _
"SELECT pc1.ProductCode, pc1.Category, pc1.Subcategory " & _
"FROM productcategories pc1 " & _
"INNER JOIN (" & _
"SELECT ProductCode, MIN(ProductCategoryID) AS ProductCategoryID " & _
"FROM productcategories " & _
"WHERE Main = TRUE " & _
"GROUP BY ProductCode" & _
") pcm ON pcm.ProductCategoryID = pc1.ProductCategoryID" & _
") pc ON pc.ProductCode = " & productCodeExpr & " " & _
"INNER JOIN countries c ON c.Country = " & effectiveCountryExpr & " "
whereSql = "WHERE o.IsInvoice = TRUE " & _
"AND o.PaymentReceived = TRUE " & _
"AND o.Status <> 'CANCELLED' " & _
"AND o.Status <> 'ORDER PLACED' " & _
"AND NOT (IFNULL(od.PriceEach, 0) = 0 AND IFNULL(od.BundlePriceEach, 0) > 0) "
If dateRange <> "all" Then whereSql = whereSql & "AND o.DateTimePaid >= ? "
whereSql = whereSql & "AND o.DateTimePaid < DATE_ADD(?, INTERVAL 1 DAY) " & _
"AND TRIM(IFNULL(c.CodeA2, '')) <> '' "
If ukMode = "exclude" Then whereSql = whereSql & "AND LEFT(c.Country, 14) <> 'United Kingdom' "
' ----------------------------------------------------------------------
' Query data
' ----------------------------------------------------------------------
rowCount = 0
countryRowCount = 0
categoryRowCount = 0
subcategoryRowCount = 0
trendRowCount = 0
totalOrders = 0
totalQuantity = 0
totalSales = 0
totalSelected = 0
productCount = 0
topGroupName = ""
topGroupValue = 0
topGroupShare = 0
pieOtherValue = 0
countryFound = False
countryFilterName = "All countries"
categoryFound = False
trendCategoryFound = False
trendSubcategoryFound = False
dataError = ""
If validationMessage = "" Then
On Error Resume Next
Set conn = Server.CreateObject("ADODB.Connection")
conn.Open "DSN=MySQL_hyperflight;"
If Err.Number <> 0 Then
dataError = Err.Description
Err.Clear
End If
' Countries containing qualifying product lines for the selected period.
If dataError = "" Then
countrySql = "SELECT " & countryCodeExpr & " AS CountryCode, " & _
countryNameExpr & " AS CountryName " & _
joinsSql & whereSql & _
"GROUP BY " & countryCodeExpr & ", " & countryNameExpr & " " & _
"ORDER BY CountryName ASC"
Set countryCmd = Server.CreateObject("ADODB.Command")
Set countryCmd.ActiveConnection = conn
countryCmd.CommandType = adCmdText
countryCmd.CommandText = countrySql
Call AppendCommonParameters(countryCmd, False, False)
Set countryRs = countryCmd.Execute()
If Err.Number <> 0 Then
dataError = Err.Description
Err.Clear
ElseIf Not countryRs.EOF Then
countryRows = countryRs.GetRows()
countryRowCount = UBound(countryRows, 2) + 1
End If
If IsObject(countryRs) Then
If countryRs.State <> 0 Then countryRs.Close
Set countryRs = Nothing
End If
Set countryCmd = Nothing
End If
If dataError = "" Then
If countryFilter = "all" Then
countryFound = True
Else
For i = 0 To countryRowCount - 1
countryListCode = UCase(CStr(countryRows(0, i)))
If countryListCode = countryFilter Then
countryFound = True
countryFilterName = CStr(countryRows(1, i))
Exit For
End If
Next
End If
If Not countryFound Then
countryFilter = "all"
countryFilterName = "All countries"
End If
End If
' Main categories containing qualifying product lines after the country filter.
If dataError = "" Then
categorySql = "SELECT " & categoryExpr & " AS CategoryName " & _
joinsSql & whereSql
If countryFilter <> "all" Then categorySql = categorySql & "AND " & countryCodeExpr & " = ? "
categorySql = categorySql & "GROUP BY " & categoryExpr & " ORDER BY CategoryName ASC"
Set categoryCmd = Server.CreateObject("ADODB.Command")
Set categoryCmd.ActiveConnection = conn
categoryCmd.CommandType = adCmdText
categoryCmd.CommandText = categorySql
Call AppendCommonParameters(categoryCmd, True, False)
Set categoryRs = categoryCmd.Execute()
If Err.Number <> 0 Then
dataError = Err.Description
Err.Clear
ElseIf Not categoryRs.EOF Then
categoryRows = categoryRs.GetRows()
categoryRowCount = UBound(categoryRows, 2) + 1
End If
If IsObject(categoryRs) Then
If categoryRs.State <> 0 Then categoryRs.Close
Set categoryRs = Nothing
End If
Set categoryCmd = Nothing
End If
If dataError = "" Then
If categoryFilter = "all" Then
categoryFound = True
Else
For i = 0 To categoryRowCount - 1
categoryListName = CStr(categoryRows(0, i))
If StrComp(categoryListName, categoryFilter, vbTextCompare) = 0 Then
categoryFound = True
categoryFilter = categoryListName
Exit For
End If
Next
End If
If Not categoryFound Then categoryFilter = "all"
If breakdown = "category" Then categoryFilter = "all"
If trendCategory = "all" Then
trendCategoryFound = True
Else
For i = 0 To categoryRowCount - 1
categoryListName = CStr(categoryRows(0, i))
If StrComp(categoryListName, trendCategory, vbTextCompare) = 0 Then
trendCategoryFound = True
trendCategory = categoryListName
Exit For
End If
Next
End If
If Not trendCategoryFound Then trendCategory = "all"
If trendCategory = "all" Then trendSubcategory = "all"
End If
' Subcategories available to the trend filter for the selected category.
If dataError = "" And chartView = "trend" And trendCategory <> "all" Then
subcategorySql = "SELECT " & subcategoryExpr & " AS SubcategoryName " & _
joinsSql & whereSql
If countryFilter <> "all" Then subcategorySql = subcategorySql & "AND " & countryCodeExpr & " = ? "
subcategorySql = subcategorySql & "AND " & categoryExpr & " = ? " & _
"GROUP BY " & subcategoryExpr & " ORDER BY SubcategoryName ASC"
Set subcategoryCmd = Server.CreateObject("ADODB.Command")
Set subcategoryCmd.ActiveConnection = conn
subcategoryCmd.CommandType = adCmdText
subcategoryCmd.CommandText = subcategorySql
Call AppendTrendParameters(subcategoryCmd, True, False)
Set subcategoryRs = subcategoryCmd.Execute()
If Err.Number <> 0 Then
dataError = Err.Description
Err.Clear
ElseIf Not subcategoryRs.EOF Then
subcategoryRows = subcategoryRs.GetRows()
subcategoryRowCount = UBound(subcategoryRows, 2) + 1
End If
If IsObject(subcategoryRs) Then
If subcategoryRs.State <> 0 Then subcategoryRs.Close
Set subcategoryRs = Nothing
End If
Set subcategoryCmd = Nothing
End If
If dataError = "" Then
If trendSubcategory = "all" Then
trendSubcategoryFound = True
ElseIf trendCategory <> "all" Then
For i = 0 To subcategoryRowCount - 1
subcategoryListName = CStr(subcategoryRows(0, i))
If StrComp(subcategoryListName, trendSubcategory, vbTextCompare) = 0 Then
trendSubcategoryFound = True
trendSubcategory = subcategoryListName
Exit For
End If
Next
End If
If Not trendSubcategoryFound Then trendSubcategory = "all"
End If
' Category or subcategory totals.
If dataError = "" Then
If breakdown = "subcategory" Then
sql = "SELECT " & categoryExpr & " AS CategoryName, " & _
subcategoryExpr & " AS SubcategoryName, " & _
"COUNT(DISTINCT o.OrderNo) AS OrderCount, " & _
"SUM(IFNULL(od.Qty, 0)) AS QuantityTotal, " & _
"SUM(" & lineSalesExpr & ") AS SalesTotal " & _
joinsSql & whereSql
If countryFilter <> "all" Then sql = sql & "AND " & countryCodeExpr & " = ? "
If categoryFilter <> "all" Then sql = sql & "AND " & categoryExpr & " = ? "
sql = sql & "GROUP BY " & categoryExpr & ", " & subcategoryExpr & " " & _
"ORDER BY " & orderBySql & " DESC, CategoryName ASC, SubcategoryName ASC"
Else
sql = "SELECT " & categoryExpr & " AS CategoryName, " & _
"'' AS SubcategoryName, " & _
"COUNT(DISTINCT o.OrderNo) AS OrderCount, " & _
"SUM(IFNULL(od.Qty, 0)) AS QuantityTotal, " & _
"SUM(" & lineSalesExpr & ") AS SalesTotal " & _
joinsSql & whereSql
If countryFilter <> "all" Then sql = sql & "AND " & countryCodeExpr & " = ? "
sql = sql & "GROUP BY " & categoryExpr & " " & _
"ORDER BY " & orderBySql & " DESC, CategoryName ASC"
End If
Set cmd = Server.CreateObject("ADODB.Command")
Set cmd.ActiveConnection = conn
cmd.CommandType = adCmdText
cmd.CommandText = sql
Call AppendCommonParameters(cmd, True, (breakdown = "subcategory"))
Set rs = cmd.Execute()
If Err.Number <> 0 Then
dataError = Err.Description
Err.Clear
ElseIf Not rs.EOF Then
dataRows = rs.GetRows()
rowCount = UBound(dataRows, 2) + 1
End If
If IsObject(rs) Then
If rs.State <> 0 Then rs.Close
Set rs = Nothing
End If
Set cmd = Nothing
End If
' Unique orders and products for the summary cards.
If dataError = "" And rowCount > 0 Then
summarySql = "SELECT COUNT(DISTINCT o.OrderNo) AS OrderCount, " & _
"COUNT(DISTINCT NULLIF(" & productCodeExpr & ", '')) AS ProductCount " & _
joinsSql & whereSql
If countryFilter <> "all" Then summarySql = summarySql & "AND " & countryCodeExpr & " = ? "
If breakdown = "subcategory" And categoryFilter <> "all" Then summarySql = summarySql & "AND " & categoryExpr & " = ? "
Set summaryCmd = Server.CreateObject("ADODB.Command")
Set summaryCmd.ActiveConnection = conn
summaryCmd.CommandType = adCmdText
summaryCmd.CommandText = summarySql
Call AppendCommonParameters(summaryCmd, True, (breakdown = "subcategory"))
Set summaryRs = summaryCmd.Execute()
If Err.Number <> 0 Then
dataError = Err.Description
Err.Clear
ElseIf Not summaryRs.EOF Then
If Not IsNull(summaryRs("OrderCount")) Then totalOrders = CLng(summaryRs("OrderCount"))
If Not IsNull(summaryRs("ProductCount")) Then productCount = CLng(summaryRs("ProductCount"))
End If
If IsObject(summaryRs) Then
If summaryRs.State <> 0 Then summaryRs.Close
Set summaryRs = Nothing
End If
Set summaryCmd = Nothing
End If
' Product trend totals for the selected category or subcategory.
If dataError = "" And rowCount > 0 And chartView = "trend" Then
trendSql = "SELECT YEAR(o.DateTimePaid) AS SalesYear, " & _
"MONTH(o.DateTimePaid) AS SalesMonth, " & _
"SUM(IFNULL(od.Qty, 0)) AS QuantityTotal, " & _
"SUM(" & lineSalesExpr & ") AS SalesTotal " & _
joinsSql & whereSql
If countryFilter <> "all" Then trendSql = trendSql & "AND " & countryCodeExpr & " = ? "
If trendCategory <> "all" Then trendSql = trendSql & "AND " & categoryExpr & " = ? "
If trendSubcategory <> "all" Then trendSql = trendSql & "AND " & subcategoryExpr & " = ? "
trendSql = trendSql & "GROUP BY YEAR(o.DateTimePaid), MONTH(o.DateTimePaid) " & _
"ORDER BY SalesYear ASC, SalesMonth ASC"
Set trendCmd = Server.CreateObject("ADODB.Command")
Set trendCmd.ActiveConnection = conn
trendCmd.CommandType = adCmdText
trendCmd.CommandText = trendSql
Call AppendTrendParameters(trendCmd, True, True)
Set trendRs = trendCmd.Execute()
If Err.Number <> 0 Then
dataError = Err.Description
Err.Clear
ElseIf Not trendRs.EOF Then
trendRows = trendRs.GetRows()
trendRowCount = UBound(trendRows, 2) + 1
End If
If IsObject(trendRs) Then
If trendRs.State <> 0 Then trendRs.Close
Set trendRs = Nothing
End If
Set trendCmd = Nothing
End If
If IsObject(conn) Then
If conn.State <> 0 Then conn.Close
Set conn = Nothing
End If
On Error GoTo 0
End If
If dataError = "" And rowCount > 0 Then
For i = 0 To rowCount - 1
currentQuantity = 0
currentSales = 0
If Not IsNull(dataRows(3, i)) Then currentQuantity = CDbl(dataRows(3, i))
If Not IsNull(dataRows(4, i)) Then currentSales = CDbl(dataRows(4, i))
totalQuantity = totalQuantity + currentQuantity
totalSales = totalSales + currentSales
Next
If measure = "quantity" Then
totalSelected = totalQuantity
Else
totalSelected = totalSales
End If
currentCategory = CStr(dataRows(0, 0))
currentSubcategory = CStr(dataRows(1, 0))
If breakdown = "subcategory" Then
If categoryFilter = "all" Then
topGroupName = currentCategory & " - " & currentSubcategory
Else
topGroupName = currentSubcategory
End If
Else
topGroupName = currentCategory
End If
If measure = "quantity" Then
topGroupValue = CDbl(dataRows(3, 0))
Else
topGroupValue = CDbl(dataRows(4, 0))
End If
If totalSelected > 0 Then topGroupShare = (topGroupValue / totalSelected) * 100
End If
If chartView = "trend" Then
If trendInterval = "yearly" Then
If measure = "quantity" Then
chartTitle = "Yearly quantity trend"
Else
chartTitle = "Yearly product sales trend"
End If
Else
If measure = "quantity" Then
chartTitle = "Monthly quantity trend"
Else
chartTitle = "Monthly product sales trend"
End If
End If
If trendSubcategory <> "all" Then
chartSubtitle = measureLabel & " for " & trendSubcategory & " within " & trendCategory
ElseIf trendCategory <> "all" Then
chartSubtitle = measureLabel & " for " & trendCategory
Else
chartSubtitle = measureLabel & " across all categories"
End If
If countryFilter <> "all" Then chartSubtitle = chartSubtitle & " (" & countryFilterName & ")"
ElseIf countryFilter <> "all" Then
chartSubtitle = chartSubtitle & " (" & countryFilterName & ")"
End If
tableCount = rowCount
If tableCount > topLimit Then tableCount = topLimit
barCount = tableCount
pieCount = rowCount
If pieCount > 10 Then pieCount = 10
If pieCount > topLimit Then pieCount = topLimit
pieDisplayedTotal = 0
If rowCount > 0 Then
For i = 0 To pieCount - 1
If measure = "quantity" Then
pieDisplayedTotal = pieDisplayedTotal + CDbl(dataRows(3, i))
Else
pieDisplayedTotal = pieDisplayedTotal + CDbl(dataRows(4, i))
End If
Next
pieOtherValue = totalSelected - pieDisplayedTotal
If pieOtherValue < 0.000001 Then pieOtherValue = 0
End If
%>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HyperFlight Product Analysis</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<% If validationMessage = "" And dataError = "" And rowCount > 0 And (chartView <> "trend" Or trendRowCount > 0) Then %>
<script src="https://www.gstatic.com/charts/loader.js"></script>
<% End If %>
<style>
body { background: #f5f6f8; }
.report-wrap { max-width: 1500px; }
.metric-card { min-height: 118px; }
.metric-value { font-size: 1.75rem; font-weight: 700; line-height: 1.15; }
.metric-value.metric-text { font-size: 1.15rem; }
.metric-label { color: #6c757d; font-size: 0.9rem; }
.card-header h2 { font-size: 1.05rem; margin: 0; }
.chart-shell { position: relative; min-height: 500px; }
#product-chart { width: 100%; min-height: 500px; }
.chart-loading {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.88);
}
.chart-header-title { min-width: 0; flex: 1 1 auto; }
.chart-header-actions { flex: 0 0 auto; }
.view-nav, .breakdown-nav { flex-wrap: nowrap !important; }
.view-nav .btn { min-width: 92px; white-space: nowrap; }
.breakdown-nav .btn { min-width: 102px; white-space: nowrap; }
.category-select { width: 210px; min-width: 210px; max-width: 210px; }
.trend-category-select { width: 190px; min-width: 190px; max-width: 190px; }
.trend-subcategory-select { width: 210px; min-width: 210px; max-width: 210px; }
.trend-filters-form { flex-wrap: nowrap; }
.group-name { min-width: 190px; }
.table th { white-space: nowrap; }
.small-note { font-size: 0.875rem; color: #6c757d; }
.analysis-nav .btn { min-width: 145px; }
@media (max-width: 1199.98px) {
.chart-header-actions { flex-wrap: wrap; }
}
@media (max-width: 767.98px) {
.chart-shell, #product-chart { min-height: 420px; }
.view-nav, .breakdown-nav { flex-wrap: wrap !important; }
.view-nav .btn, .breakdown-nav .btn { min-width: 0; }
.category-select, .trend-category-select, .trend-subcategory-select { width: 100%; min-width: 180px; max-width: none; }
.trend-filters-form { flex-wrap: wrap; width: 100%; }
}
</style>
</head>
<body class="pb-5">
<div class="container-fluid report-wrap py-4 px-3 px-lg-4">
<div class="card shadow-sm mb-4">
<div class="card-header bg-primary text-white py-3">
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3 align-items-lg-center">
<div>
<h1 class="h4 mb-1">HyperFlight Product Analysis</h1>
<div class="small opacity-75">Sales by current main product category and subcategory</div>
</div>
<div class="d-flex flex-column flex-sm-row align-items-sm-center gap-2">
<div class="btn-group btn-group-sm analysis-nav" role="group" aria-label="Analysis section">
<a class="btn btn-light" href="<%=Html(BuildSalesMapUrl())%>">Geographical analysis</a>
<span class="btn btn-light active" aria-current="page">Product analysis</span>
</div>
<span class="badge text-bg-light"><%=rangeLabel%></span>
</div>
</div>
</div>
<div class="card-body">
<form method="get" action="" class="row g-3 align-items-end">
<div class="col-sm-6 col-xl-2">
<label for="DateRange" class="form-label fw-semibold">Date range</label>
<select class="form-select" id="DateRange" name="DateRange">
<option value="30days" <% If dateRange = "30days" Then Response.Write "selected" %>>Past 30 days</option>
<option value="60days" <% If dateRange = "60days" Then Response.Write "selected" %>>Past 60 days</option>
<option value="90days" <% If dateRange = "90days" Then Response.Write "selected" %>>Past 90 days</option>
<option value="thisquarter" <% If dateRange = "thisquarter" Then Response.Write "selected" %>>This quarter</option>
<option value="previousquarter" <% If dateRange = "previousquarter" Then Response.Write "selected" %>>Previous quarter</option>
<option value="6months" <% If dateRange = "6months" Then Response.Write "selected" %>>Past 6 months</option>
<option value="12months" <% If dateRange = "12months" Then Response.Write "selected" %>>Past 12 months</option>
<option value="yeartodate" <% If dateRange = "yeartodate" Then Response.Write "selected" %>>Year to date</option>
<option value="lastyear" <% If dateRange = "lastyear" Then Response.Write "selected" %>>Last calendar year</option>
<option value="2years" <% If dateRange = "2years" Then Response.Write "selected" %>>Past 2 years</option>
<option value="3years" <% If dateRange = "3years" Then Response.Write "selected" %>>Past 3 years</option>
<option value="5years" <% If dateRange = "5years" Then Response.Write "selected" %>>Past 5 years</option>
<option value="all" <% If dateRange = "all" Then Response.Write "selected" %>>All time</option>
<option value="custom" <% If dateRange = "custom" Then Response.Write "selected" %>>Custom dates</option>
</select>
</div>
<div class="col-sm-6 col-xl-2">
<label for="StartDate" class="form-label fw-semibold">Start date</label>
<input type="date" class="form-control" id="StartDate" name="StartDate" value="<%=Html(startDate)%>"<% If dateRange = "custom" Then Response.Write " required" Else Response.Write " readonly" %>>
</div>
<div class="col-sm-6 col-xl-2">
<label for="EndDate" class="form-label fw-semibold">End date</label>
<input type="date" class="form-control" id="EndDate" name="EndDate" value="<%=Html(endDate)%>"<% If dateRange = "custom" Then Response.Write " required" Else Response.Write " readonly" %>>
</div>
<div class="col-sm-6 col-xl-2">
<label for="Measure" class="form-label fw-semibold">Measure</label>
<select class="form-select" id="Measure" name="Measure">
<option value="sales" <% If measure = "sales" Then Response.Write "selected" %>>Sales value</option>
<option value="quantity" <% If measure = "quantity" Then Response.Write "selected" %>>Quantity sold</option>
</select>
</div>
<div class="col-sm-6 col-xl-2">
<label for="UK" class="form-label fw-semibold">United Kingdom</label>
<select class="form-select" id="UK" name="UK">
<option value="include" <% If ukMode = "include" Then Response.Write "selected" %>>Include</option>
<option value="exclude" <% If ukMode = "exclude" Then Response.Write "selected" %>>Exclude</option>
</select>
</div>
<div class="col-sm-6 col-xl-2">
<label for="Country" class="form-label fw-semibold">Country</label>
<select class="form-select" id="Country" name="Country">
<option value="all"<% If countryFilter = "all" Then Response.Write " selected" %>>All countries</option>
<% For i = 0 To countryRowCount - 1
countryListCode = CStr(countryRows(0, i))
countryListName = CStr(countryRows(1, i))
%>
<option value="<%=Html(countryListCode)%>"<% If countryFilter = UCase(countryListCode) Then Response.Write " selected" %>><%=Html(countryListName)%></option>
<% Next %>
</select>
</div>
<div class="col-sm-6 col-lg-3 col-xl-2">
<label for="Breakdown" class="form-label fw-semibold">Breakdown</label>
<select class="form-select" id="Breakdown" name="Breakdown">
<option value="category" <% If breakdown = "category" Then Response.Write "selected" %>>Category</option>
<option value="subcategory" <% If breakdown = "subcategory" Then Response.Write "selected" %>>Subcategory</option>
</select>
</div>
<div class="col-sm-6 col-lg-5 col-xl-4">
<label for="Category" class="form-label fw-semibold">Category filter</label>
<select class="form-select" id="Category" name="Category"<% If breakdown <> "subcategory" Then Response.Write " disabled" %>>
<option value="all"<% If categoryFilter = "all" Then Response.Write " selected" %>>All categories</option>
<% For i = 0 To categoryRowCount - 1
categoryListName = CStr(categoryRows(0, i))
%>
<option value="<%=Html(categoryListName)%>"<% If categoryFilter = categoryListName Then Response.Write " selected" %>><%=Html(categoryListName)%></option>
<% Next %>
</select>
</div>
<div class="col-6 col-lg-2 col-xl-1">
<label for="TopLimit" class="form-label fw-semibold">Top</label>
<select class="form-select" id="TopLimit" name="TopLimit">
<option value="10" <% If topLimit = 10 Then Response.Write "selected" %>>10</option>
<option value="15" <% If topLimit = 15 Then Response.Write "selected" %>>15</option>
<option value="20" <% If topLimit = 20 Then Response.Write "selected" %>>20</option>
<option value="30" <% If topLimit = 30 Then Response.Write "selected" %>>30</option>
</select>
</div>
<div class="col-6 col-lg-2 col-xl-1 d-grid">
<input type="hidden" name="View" value="<%=Html(chartView)%>">
<input type="hidden" name="TrendInterval" value="<%=Html(trendInterval)%>">
<input type="hidden" name="TrendCategory" value="<%=Html(trendCategory)%>">
<input type="hidden" name="TrendSubcategory" value="<%=Html(trendSubcategory)%>">
<button type="submit" class="btn btn-primary fw-semibold">Run</button>
</div>
</form>
<div class="small-note mt-3">Sales values exclude VAT and delivery. Order discounts are allocated proportionally across product lines. Countries use the delivery address, falling back to the billing address. Partial refunds and restocked quantities are not taken into account.</div>
<div class="small-note mt-1">Historical sales use each product's current main category. Bundle container records are excluded.</div>
</div>
</div>
<% If validationMessage <> "" Then %>
<div class="alert alert-warning shadow-sm"><%=Html(validationMessage)%></div>
<% ElseIf dataError <> "" Then %>
<div class="alert alert-danger shadow-sm">
<strong>The report could not be loaded.</strong>
<div class="small mt-1"><%=Html(dataError)%></div>
</div>
<% ElseIf rowCount = 0 Then %>
<div class="alert alert-info shadow-sm">No product sales were found for the selected filters.</div>
<% Else %>
<div class="row g-3 mb-4">
<div class="col-6 col-lg-4 col-xxl-2">
<div class="card shadow-sm h-100 metric-card"><div class="card-body">
<div class="metric-value"><%=Money0(totalSales)%></div>
<div class="metric-label">Product sales</div>
</div></div>
</div>
<div class="col-6 col-lg-4 col-xxl-2">
<div class="card shadow-sm h-100 metric-card"><div class="card-body">
<div class="metric-value"><%=Number0(totalQuantity)%></div>
<div class="metric-label">Quantity sold</div>
</div></div>
</div>
<div class="col-6 col-lg-4 col-xxl-2">
<div class="card shadow-sm h-100 metric-card"><div class="card-body">
<div class="metric-value"><%=Number0(totalOrders)%></div>
<div class="metric-label">Orders represented</div>
</div></div>
</div>
<div class="col-6 col-lg-4 col-xxl-2">
<div class="card shadow-sm h-100 metric-card"><div class="card-body">
<div class="metric-value"><%=Number0(productCount)%></div>
<div class="metric-label">Products represented</div>
</div></div>
</div>
<div class="col-6 col-lg-4 col-xxl-2">
<div class="card shadow-sm h-100 metric-card border-primary"><div class="card-body">
<div class="metric-value metric-text"><%=Html(topGroupName)%></div>
<div class="metric-label">Largest <%=LCase(groupLabel)%> by <%=LCase(measureLabel)%></div>
</div></div>
</div>
<div class="col-6 col-lg-4 col-xxl-2">
<div class="card shadow-sm h-100 metric-card"><div class="card-body">
<div class="metric-value"><%=FormatNumber(topGroupShare, 1)%>%</div>
<div class="metric-label">Largest <%=LCase(groupLabel)%> share</div>
</div></div>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-header bg-dark text-white py-3">
<div class="d-flex flex-column flex-xl-row justify-content-between align-items-xl-center gap-3">
<div class="chart-header-title">
<h2><%=Html(chartTitle)%></h2>
<div class="small opacity-75 mt-1"><%=Html(chartSubtitle)%></div>
</div>
<div class="d-flex flex-column flex-sm-row align-items-sm-center gap-2 chart-header-actions">
<% If chartView = "trend" Then %>
<form method="get" action="" class="d-flex align-items-center gap-2 trend-filters-form">
<input type="hidden" name="DateRange" value="<%=Html(dateRange)%>">
<input type="hidden" name="StartDate" value="<%=Html(startDate)%>">
<input type="hidden" name="EndDate" value="<%=Html(endDate)%>">
<input type="hidden" name="Measure" value="<%=Html(measure)%>">
<input type="hidden" name="UK" value="<%=Html(ukMode)%>">
<input type="hidden" name="Country" value="<%=Html(countryFilter)%>">
<input type="hidden" name="Breakdown" value="<%=Html(breakdown)%>">
<input type="hidden" name="Category" value="<%=Html(categoryFilter)%>">
<input type="hidden" name="TopLimit" value="<%=topLimit%>">
<input type="hidden" name="TrendInterval" value="<%=Html(trendInterval)%>">
<input type="hidden" name="View" value="trend">
<label for="TrendCategory" class="small fw-semibold text-nowrap mb-0">Category</label>
<select class="form-select form-select-sm trend-category-select" id="TrendCategory" name="TrendCategory" onchange="this.form.submit()">
<option value="all"<% If trendCategory = "all" Then Response.Write " selected" %>>All categories</option>
<% For i = 0 To categoryRowCount - 1
categoryListName = CStr(categoryRows(0, i))
%>
<option value="<%=Html(categoryListName)%>"<% If trendCategory = categoryListName Then Response.Write " selected" %>><%=Html(categoryListName)%></option>
<% Next %>
</select>
<label for="TrendSubcategory" class="small fw-semibold text-nowrap mb-0">Subcategory</label>
<select class="form-select form-select-sm trend-subcategory-select" id="TrendSubcategory" name="TrendSubcategory" onchange="this.form.submit()"<% If trendCategory = "all" Then Response.Write " disabled" %>>
<option value="all"<% If trendSubcategory = "all" Then Response.Write " selected" %>>All subcategories</option>
<% For i = 0 To subcategoryRowCount - 1
subcategoryListName = CStr(subcategoryRows(0, i))
%>
<option value="<%=Html(subcategoryListName)%>"<% If trendSubcategory = subcategoryListName Then Response.Write " selected" %>><%=Html(subcategoryListName)%></option>
<% Next %>
</select>
</form>
<% Else %>
<div class="btn-group btn-group-sm breakdown-nav" role="group" aria-label="Product breakdown">
<a class="btn btn-light<% If breakdown = "category" Then Response.Write " active" %>" href="<%=Html(BuildBreakdownUrl("category"))%>"<% If breakdown = "category" Then Response.Write " aria-current=""page""" %>>Categories</a>
<a class="btn btn-light<% If breakdown = "subcategory" Then Response.Write " active" %>" href="<%=Html(BuildBreakdownUrl("subcategory"))%>"<% If breakdown = "subcategory" Then Response.Write " aria-current=""page""" %>>Subcategories</a>
</div>
<% If breakdown = "subcategory" Then %>
<form method="get" action="" class="d-flex align-items-center gap-2">
<input type="hidden" name="DateRange" value="<%=Html(dateRange)%>">
<input type="hidden" name="StartDate" value="<%=Html(startDate)%>">
<input type="hidden" name="EndDate" value="<%=Html(endDate)%>">
<input type="hidden" name="Measure" value="<%=Html(measure)%>">
<input type="hidden" name="UK" value="<%=Html(ukMode)%>">
<input type="hidden" name="Country" value="<%=Html(countryFilter)%>">
<input type="hidden" name="Breakdown" value="subcategory">
<input type="hidden" name="TopLimit" value="<%=topLimit%>">
<input type="hidden" name="TrendInterval" value="<%=Html(trendInterval)%>">
<input type="hidden" name="TrendCategory" value="<%=Html(trendCategory)%>">
<input type="hidden" name="TrendSubcategory" value="<%=Html(trendSubcategory)%>">
<input type="hidden" name="View" value="<%=Html(chartView)%>">
<label for="HeaderCategory" class="small fw-semibold text-nowrap mb-0">Category</label>
<select class="form-select form-select-sm category-select" id="HeaderCategory" name="Category" onchange="this.form.submit()">
<option value="all"<% If categoryFilter = "all" Then Response.Write " selected" %>>All categories</option>
<% For i = 0 To categoryRowCount - 1
categoryListName = CStr(categoryRows(0, i))
%>
<option value="<%=Html(categoryListName)%>"<% If categoryFilter = categoryListName Then Response.Write " selected" %>><%=Html(categoryListName)%></option>
<% Next %>
</select>
</form>
<% End If %>
<% End If %>
<div class="btn-group btn-group-sm view-nav" role="group" aria-label="Chart view">
<a class="btn btn-light<% If chartView = "bar" Then Response.Write " active" %>" href="<%=Html(BuildViewUrl("bar"))%>"<% If chartView = "bar" Then Response.Write " aria-current=""page""" %>>Bar chart</a>
<a class="btn btn-light<% If chartView = "pie" Then Response.Write " active" %>" href="<%=Html(BuildViewUrl("pie"))%>"<% If chartView = "pie" Then Response.Write " aria-current=""page""" %>>Pie chart</a>
<a class="btn btn-light<% If chartView = "trend" Then Response.Write " active" %>" href="<%=Html(BuildViewUrl("trend"))%>"<% If chartView = "trend" Then Response.Write " aria-current=""page""" %>>Sales trend</a>
</div>
</div>
</div>
</div>
<div class="card-body p-2 p-md-3">
<% If chartView = "trend" Then %>
<div class="d-flex justify-content-end align-items-center gap-2 px-2 pb-2">
<span class="small fw-semibold text-muted">Trend interval</span>
<div class="btn-group btn-group-sm" role="group" aria-label="Trend interval">
<a class="btn btn-outline-secondary<% If trendInterval = "monthly" Then Response.Write " active" %>" href="<%=Html(BuildTrendIntervalUrl("monthly"))%>"<% If trendInterval = "monthly" Then Response.Write " aria-current=""page""" %>>Monthly</a>
<% If yearlyTrendAvailable Then %>
<a class="btn btn-outline-secondary<% If trendInterval = "yearly" Then Response.Write " active" %>" href="<%=Html(BuildTrendIntervalUrl("yearly"))%>"<% If trendInterval = "yearly" Then Response.Write " aria-current=""page""" %>>Yearly</a>
<% Else %>
<span class="btn btn-outline-secondary disabled" aria-disabled="true" title="Yearly trend is available for date ranges of at least 12 months">Yearly</span>
<% End If %>
</div>
</div>
<% End If %>
<% If chartView = "trend" And trendRowCount = 0 Then %>
<div class="alert alert-info mb-0">No trend data were found for the selected category and subcategory.</div>
<% Else %>
<div class="chart-shell">
<div id="chart-loading" class="chart-loading">
<div class="text-center">
<div class="spinner-border text-primary mb-2" role="status"><span class="visually-hidden">Loading chart</span></div>
<div class="small text-muted">Loading chart…</div>
</div>
</div>
<div id="product-chart" aria-label="<%=Html(chartTitle)%>"></div>
</div>
<% End If %>
<% If chartView = "pie" And rowCount > pieCount Then %>
<div class="small-note px-2 pb-1">The pie chart shows the top <%=pieCount%> <%=LCase(groupLabel)%>s; the remainder are grouped as Other.</div>
<% ElseIf chartView = "bar" And rowCount > barCount Then %>
<div class="small-note px-2 pb-1">The bar chart shows the top <%=barCount%> <%=LCase(groupLabel)%>s.</div>
<% ElseIf chartView = "trend" And trendRowCount > 0 Then %>
<% If trendInterval = "yearly" Then %>
<div class="small-note px-2 pb-1">The yearly trend groups results by calendar year. The first and last years may be partial where the selected dates do not cover complete calendar years.</div>
<% Else %>
<div class="small-note px-2 pb-1">The trend chart groups results by calendar month.</div>
<% End If %>
<% End If %>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<div class="d-flex justify-content-between align-items-center gap-2">
<h2><%=Html(groupLabel)%> ranking</h2>
<span class="badge text-bg-light">Top <%=tableCount%> of <%=rowCount%></span>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="text-end">#</th>
<th class="group-name">Category</th>
<% If breakdown = "subcategory" Then %><th class="group-name">Subcategory</th><% End If %>
<th class="text-end">Orders</th>
<th class="text-end<% If measure = "quantity" Then Response.Write " table-primary" %>">Quantity</th>
<th class="text-end<% If measure = "sales" Then Response.Write " table-primary" %>">Sales</th>
<th class="text-end">Share of <%=LCase(valueColumnLabel)%></th>
</tr>
</thead>
<tbody>
<% For i = 0 To tableCount - 1
currentCategory = CStr(dataRows(0, i))
currentSubcategory = CStr(dataRows(1, i))
currentOrders = CLng(dataRows(2, i))
currentQuantity = CDbl(dataRows(3, i))
currentSales = CDbl(dataRows(4, i))
If measure = "quantity" Then currentValue = currentQuantity Else currentValue = currentSales
shareValue = 0
If totalSelected > 0 Then shareValue = (currentValue / totalSelected) * 100
%>
<tr>
<td class="text-end text-muted"><%=i + 1%></td>
<td class="fw-semibold"><%=Html(currentCategory)%></td>
<% If breakdown = "subcategory" Then %><td><%=Html(currentSubcategory)%></td><% End If %>
<td class="text-end"><%=Number0(currentOrders)%></td>
<td class="text-end"><%=Number0(currentQuantity)%></td>
<td class="text-end"><%=Money0(currentSales)%></td>
<td class="text-end"><%=FormatNumber(shareValue, 1)%>%</td>
</tr>
<% Next %>
</tbody>
</table>
</div>
<div class="card-footer small-note">Order counts can overlap because one order may contain products from more than one <%=LCase(groupLabel)%>.</div>
</div>
<% End If %>
</div>
<script>
(function () {
'use strict';
var rangeSelect = document.getElementById('DateRange');
var startInput = document.getElementById('StartDate');
var endInput = document.getElementById('EndDate');
var breakdownSelect = document.getElementById('Breakdown');
var categorySelect = document.getElementById('Category');
if (breakdownSelect && categorySelect) {
breakdownSelect.addEventListener('change', function () {
categorySelect.disabled = breakdownSelect.value !== 'subcategory';
if (categorySelect.disabled) categorySelect.value = 'all';
});
}
if (!rangeSelect || !startInput || !endInput) return;
function formatDate(date) {
var year = date.getFullYear();
var month = String(date.getMonth() + 1).padStart(2, '0');
var day = String(date.getDate()).padStart(2, '0');
return year + '-' + month + '-' + day;
}
function addDays(date, days) {
var result = new Date(date.getFullYear(), date.getMonth(), date.getDate());
result.setDate(result.getDate() + days);
return result;
}
function addMonths(date, months) {
var day = date.getDate();
var result = new Date(date.getFullYear(), date.getMonth(), 1);
result.setMonth(result.getMonth() + months);
var lastDay = new Date(result.getFullYear(), result.getMonth() + 1, 0).getDate();
result.setDate(Math.min(day, lastDay));
return result;
}
function setCustomMode(isCustom) {
startInput.readOnly = !isCustom;
endInput.readOnly = !isCustom;
startInput.required = isCustom;
endInput.required = isCustom;
}
function applySelectedRange() {
var today = new Date();
var value = rangeSelect.value;
var start = null;
var end = today;
if (value === 'custom') {
setCustomMode(true);
if (!startInput.value) startInput.value = formatDate(addMonths(today, -12));
if (!endInput.value) endInput.value = formatDate(today);
return;
}
setCustomMode(false);
switch (value) {
case '30days': start = addDays(today, -29); break;
case '60days': start = addDays(today, -59); break;
case '90days': start = addDays(today, -89); break;
case 'thisquarter':
start = new Date(today.getFullYear(), Math.floor(today.getMonth() / 3) * 3, 1);
break;
case 'previousquarter':
end = new Date(today.getFullYear(), Math.floor(today.getMonth() / 3) * 3, 0);
start = new Date(end.getFullYear(), end.getMonth() - 2, 1);
break;
case '6months': start = addMonths(today, -6); break;
case '12months': start = addMonths(today, -12); break;
case 'yeartodate': start = new Date(today.getFullYear(), 0, 1); break;
case 'lastyear':
start = new Date(today.getFullYear() - 1, 0, 1);
end = new Date(today.getFullYear() - 1, 11, 31);
break;
case '2years': start = addMonths(today, -24); break;
case '3years': start = addMonths(today, -36); break;
case '5years': start = addMonths(today, -60); break;
case 'all':
startInput.value = '';
endInput.value = formatDate(today);
return;
default: start = addMonths(today, -12);
}
startInput.value = formatDate(start);
endInput.value = formatDate(end);
}
rangeSelect.addEventListener('change', applySelectedRange);
}());
</script>
<% If validationMessage = "" And dataError = "" And rowCount > 0 And (chartView <> "trend" Or trendRowCount > 0) Then %>
<script>
(function () {
'use strict';
var currentView = '<%=JsString(chartView)%>';
var currentMeasure = '<%=JsString(measure)%>';
var currentTrendInterval = '<%=JsString(trendInterval)%>';
var currentCalendarYear = <%=Year(Date())%>;
var trendEndsToday = <%=LCase(CStr(endDate = IsoDate(Date())))%>;
var chartElement = document.getElementById('product-chart');
var loadingElement = document.getElementById('chart-loading');
var resizeTimer = null;
var chartsLoaded = false;
google.charts.load('current', {packages: ['corechart'], language: 'en-GB'});
google.charts.setOnLoadCallback(function () {
chartsLoaded = true;
drawProductChart();
});
function addFormatting(data) {
var formatter;
if (currentMeasure === 'quantity') {
formatter = new google.visualization.NumberFormat({fractionDigits: 0});
} else {
formatter = new google.visualization.NumberFormat({prefix: '£', fractionDigits: 0});
}
formatter.format(data, 1);
}
function buildBarData() {
var data = new google.visualization.DataTable();
data.addColumn('string', '<%=JsString(groupLabel)%>');
data.addColumn('number', '<%=JsString(valueColumnLabel)%>');
<% For i = 0 To barCount - 1
currentCategory = CStr(dataRows(0, i))
currentSubcategory = CStr(dataRows(1, i))
If breakdown = "subcategory" Then
If categoryFilter = "all" Then
currentGroupLabel = currentCategory & " - " & currentSubcategory
Else
currentGroupLabel = currentSubcategory
End If
Else
currentGroupLabel = currentCategory
End If
If measure = "quantity" Then currentValue = CDbl(dataRows(3, i)) Else currentValue = CDbl(dataRows(4, i))
%>
data.addRow(['<%=JsString(currentGroupLabel)%>', <%=JsNumber(currentValue)%>]);
<% Next %>
addFormatting(data);
return data;
}
function buildPieData() {
var data = new google.visualization.DataTable();
data.addColumn('string', '<%=JsString(groupLabel)%>');
data.addColumn('number', '<%=JsString(valueColumnLabel)%>');
<% For i = 0 To pieCount - 1
currentCategory = CStr(dataRows(0, i))
currentSubcategory = CStr(dataRows(1, i))
If breakdown = "subcategory" Then
If categoryFilter = "all" Then
currentGroupLabel = currentCategory & " - " & currentSubcategory
Else
currentGroupLabel = currentSubcategory
End If
Else
currentGroupLabel = currentCategory
End If
If measure = "quantity" Then currentValue = CDbl(dataRows(3, i)) Else currentValue = CDbl(dataRows(4, i))
%>
data.addRow(['<%=JsString(currentGroupLabel)%>', <%=JsNumber(currentValue)%>]);
<% Next %>
<% If pieOtherValue > 0 Then %>
data.addRow(['Other', <%=JsNumber(pieOtherValue)%>]);
<% End If %>
addFormatting(data);
return data;
}
function buildTrendData() {
var data = new google.visualization.DataTable();
<% If trendInterval = "yearly" Then %>
var yearlyValues = {};
var yearOrder = [];
var yearKey;
var i;
data.addColumn('string', 'Year');
data.addColumn('number', '<%=JsString(valueColumnLabel)%>');
data.addColumn({type: 'string', role: 'style'});
<% For i = 0 To trendRowCount - 1
trendYear = CInt(trendRows(0, i))
If measure = "quantity" Then currentValue = CDbl(trendRows(2, i)) Else currentValue = CDbl(trendRows(3, i))
%>
yearKey = '<%=trendYear%>';
if (!Object.prototype.hasOwnProperty.call(yearlyValues, yearKey)) {
yearlyValues[yearKey] = 0;
yearOrder.push(yearKey);
}
yearlyValues[yearKey] += <%=JsNumber(currentValue)%>;
<% Next %>
for (i = 0; i < yearOrder.length; i += 1) {
yearKey = yearOrder[i];
data.addRow([
trendEndsToday && yearKey === String(currentCalendarYear) ? yearKey + ' YTD' : yearKey,
yearlyValues[yearKey],
trendEndsToday && yearKey === String(currentCalendarYear) ? 'color: #fd7e14' : 'color: #0d6efd'
]);
}
addFormatting(data);
<% Else %>
data.addColumn('date', 'Month');
data.addColumn('number', '<%=JsString(valueColumnLabel)%>');
<% For i = 0 To trendRowCount - 1
trendYear = CInt(trendRows(0, i))
trendMonth = CInt(trendRows(1, i))
If measure = "quantity" Then currentValue = CDbl(trendRows(2, i)) Else currentValue = CDbl(trendRows(3, i))
%>
data.addRow([new Date(<%=trendYear%>, <%=trendMonth - 1%>, 1), <%=JsNumber(currentValue)%>]);
<% Next %>
new google.visualization.DateFormat({pattern: 'MMM yyyy'}).format(data, 0);
addFormatting(data);
<% End If %>
return data;
}
function hideLoading() {
if (loadingElement) loadingElement.classList.add('d-none');
}
function drawProductChart() {
if (!chartsLoaded || !chartElement) return;
var width = Math.max(chartElement.clientWidth, 320);
var height;
var data;
var chart;
var options;
if (currentView === 'pie') {
height = Math.max(480, Math.min(650, Math.round(width * 0.56)));
chartElement.style.height = height + 'px';
data = buildPieData();
chart = new google.visualization.PieChart(chartElement);
options = {
width: width,
height: height,
backgroundColor: 'transparent',
chartArea: {left: 20, top: 25, width: '88%', height: '86%'},
legend: {position: width < 700 ? 'bottom' : 'right', textStyle: {fontSize: 13}},
pieSliceText: 'percentage',
sliceVisibilityThreshold: 0,
tooltip: {text: 'both'},
colors: ['#0d6efd', '#dc3545', '#fd7e14', '#198754', '#6f42c1', '#0dcaf0', '#ffc107', '#6610f2', '#20c997', '#6c757d', '#adb5bd']
};
} else if (currentView === 'trend') {
height = Math.max(480, Math.min(720, Math.round(width * 0.50)));
chartElement.style.height = height + 'px';
data = buildTrendData();
chart = currentTrendInterval === 'yearly'
? new google.visualization.ColumnChart(chartElement)
: new google.visualization.LineChart(chartElement);
options = {
width: width,
height: height,
backgroundColor: 'transparent',
legend: 'none',
chartArea: {left: width < 700 ? 72 : 95, top: 25, width: width < 700 ? '76%' : '84%', height: '72%'},
hAxis: {
<% If trendInterval = "monthly" Then %>format: 'MMM yyyy',
gridlines: {count: Math.min(12, Math.max(4, <%=trendRowCount%>))},
minorGridlines: {count: 0},<% End If %>
textStyle: {fontSize: 12}
},
vAxis: {
minValue: 0,
format: currentMeasure === 'quantity' ? '#,##0' : '£#,##0',
textStyle: {fontSize: 12}
},
<% If trendInterval = "yearly" Then %>
bar: {groupWidth: '62%'},
colors: ['#0d6efd'],
<% Else %>
lineWidth: 3,
pointSize: <% If trendRowCount <= 36 Then Response.Write 5 Else Response.Write 2 %>,
colors: ['#0d6efd'],
<% End If %>
focusTarget: 'category'
};
} else {
height = Math.max(440, <%=barCount%> * 38 + 90);
chartElement.style.height = height + 'px';
data = buildBarData();
chart = new google.visualization.BarChart(chartElement);
options = {
width: width,
height: height,
backgroundColor: 'transparent',
legend: 'none',
chartArea: {left: width < 700 ? 150 : 255, top: 25, width: width < 700 ? '55%' : '68%', height: '86%'},
hAxis: {
minValue: 0,
format: currentMeasure === 'quantity' ? '#,##0' : '£#,##0',
textStyle: {fontSize: 12}
},
vAxis: {textStyle: {fontSize: 12}},
colors: ['#0d6efd']
};
}
google.visualization.events.addListener(chart, 'ready', hideLoading);
chart.draw(data, options);
}
window.addEventListener('resize', function () {
if (!chartsLoaded) return;
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(drawProductChart, 180);
});
}());
</script>
<% End If %>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
</body>
</html>