File: D:/web/hyperflight/apps/sales-analytics/index-v5.asp
<%
Option Explicit
Response.Buffer = True
Response.CodePage = 65001
Response.CharSet = "utf-8"
' ======================================================================
' HyperFlight Sales Map v10
' Standalone Bootstrap 5 / Classic ASP / MySQL application
' Based on the original inc-template-sales-map.asp report
' ======================================================================
Const adCmdText = 1
Const adParamInput = 1
Const adVarChar = 200
Dim conn, cmd, rs
Dim trendConn, trendCmd, trendRs
Dim startDate, endDate, requestedStartDate, requestedEndDate, dateRange, dateRangeName
Dim measure, chartView, ukMode, topLimit
Dim validationMessage, dataError, rangeLabel
Dim sql, trendSql, orderBySql, countryCodeExpr, countryNameExpr
Dim dataRows, rowCount, i, tableCount, barCount, pieCount
Dim trendRows, trendRowCount, trendYear, trendMonth
Dim trendComparisonMode, trendPreviousYear, trendSelectedYear, trendCurrentYear, trendCurrentMonth
Dim trendQueryStartDate, trendQueryEndDate, trendSeriesYear, trendSeriesMonth, trendSeriesValue
Dim trendPreviousValues(12), trendSelectedValues(12), trendCurrentValues(12)
Dim countryCode, countryName, currentOrders, currentSales, currentValue
Dim totalOrders, totalSales, totalSelected, countryCount
Dim topCountryName, topCountryValue, topCountryShare, averageOrderValue
Dim pieOtherValue, pieDisplayedTotal, shareValue
Dim chartTitle, chartSubtitle, measureLabel, valueColumnLabel
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) & _
"&TopLimit=" & topLimit & _
"&View=" & Server.URLEncode(requestedView)
End Function
' ----------------------------------------------------------------------
' 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"))
' Existing bookmarked URLs containing dates but no DateRange remain valid.
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 topLimit = "" Or Not IsNumeric(topLimit) Then topLimit = 15
topLimit = CInt(topLimit)
If topLimit <> 10 And topLimit <> 15 And topLimit <> 20 And topLimit <> 30 Then topLimit = 15
Select Case measure
Case "sales", "orders"
' Valid.
Case Else
measure = "sales"
End Select
Select Case chartView
Case "world", "europe", "bar", "pie", "trend"
' Valid.
Case Else
chartView = "bar"
End Select
Select Case ukMode
Case "include", "exclude"
' Valid.
Case Else
ukMode = "include"
End Select
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
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 = "orders" Then
orderBySql = "OrderCount"
measureLabel = "Orders"
valueColumnLabel = "Orders"
Else
orderBySql = "SalesTotal"
measureLabel = "Sales value"
valueColumnLabel = "Sales"
End If
Select Case chartView
Case "europe"
chartTitle = "Europe map"
Case "bar"
chartTitle = "Top countries"
Case "pie"
chartTitle = "Country share"
Case "trend"
If measure = "orders" Then
chartTitle = "Monthly order trend"
Else
chartTitle = "Monthly sales trend"
End If
Case Else
chartTitle = "World map"
End Select
trendComparisonMode = False
If chartView = "trend" And dateRange = "lastyear" Then
trendComparisonMode = True
trendCurrentYear = Year(Date())
trendSelectedYear = trendCurrentYear - 1
trendPreviousYear = trendSelectedYear - 1
trendCurrentMonth = Month(Date())
End If
If chartView = "trend" Then
If trendComparisonMode Then
chartSubtitle = measureLabel & " by month: " & trendPreviousYear & ", " & trendSelectedYear & " and " & trendCurrentYear & " year to date"
Else
chartSubtitle = measureLabel & " by month"
End If
Else
chartSubtitle = measureLabel & " by country"
End If
' ----------------------------------------------------------------------
' Query and aggregate data
' ----------------------------------------------------------------------
rowCount = 0
totalOrders = 0
totalSales = 0
totalSelected = 0
countryCount = 0
topCountryName = ""
topCountryValue = 0
topCountryShare = 0
averageOrderValue = 0
pieOtherValue = 0
trendRowCount = 0
dataError = ""
If validationMessage = "" Then
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"
sql = "SELECT " & countryCodeExpr & " AS CountryCode, " & _
countryNameExpr & " AS CountryName, " & _
"COUNT(*) AS OrderCount, " & _
"SUM(IFNULL(o.GrandTotal, 0) - IFNULL(o.VATIncluded, 0)) AS SalesTotal " & _
"FROM orders o " & _
"INNER JOIN countries c ON c.Country = o.Country " & _
"WHERE o.IsInvoice = TRUE " & _
"AND o.PaymentReceived = TRUE " & _
"AND o.Status <> 'CANCELLED' " & _
"AND o.Status <> 'ORDER PLACED' "
If dateRange <> "all" Then
sql = sql & "AND o.DateTimePaid >= ? "
End If
sql = sql & "AND o.DateTimePaid < DATE_ADD(?, INTERVAL 1 DAY) " & _
"AND TRIM(IFNULL(c.CodeA2, '')) <> '' "
If ukMode = "exclude" Then
sql = sql & "AND LEFT(c.Country, 14) <> 'United Kingdom' "
End If
sql = sql & "GROUP BY " & countryCodeExpr & ", " & countryNameExpr & " " & _
"ORDER BY " & orderBySql & " DESC, CountryName ASC"
On Error Resume Next
Set conn = Server.CreateObject("ADODB.Connection")
conn.Open "DSN=MySQL_hyperflight;"
If Err.Number = 0 Then
Set cmd = Server.CreateObject("ADODB.Command")
Set cmd.ActiveConnection = conn
cmd.CommandType = adCmdText
cmd.CommandText = sql
If dateRange <> "all" Then
cmd.Parameters.Append cmd.CreateParameter("@DateFrom", adVarChar, adParamInput, 10, startDate)
End If
cmd.Parameters.Append cmd.CreateParameter("@DateTo", adVarChar, adParamInput, 10, endDate)
Set rs = cmd.Execute()
End If
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
If IsObject(conn) Then
If conn.State <> 0 Then conn.Close
Set conn = Nothing
End If
On Error GoTo 0
If dataError = "" And rowCount > 0 Then
For i = 0 To rowCount - 1
currentOrders = 0
currentSales = 0
If Not IsNull(dataRows(2, i)) Then currentOrders = CLng(dataRows(2, i))
If Not IsNull(dataRows(3, i)) Then currentSales = CDbl(dataRows(3, i))
totalOrders = totalOrders + currentOrders
totalSales = totalSales + currentSales
Next
If measure = "orders" Then
totalSelected = CDbl(totalOrders)
Else
totalSelected = totalSales
End If
countryCount = rowCount
topCountryName = CStr(dataRows(1, 0))
If measure = "orders" Then
topCountryValue = CDbl(dataRows(2, 0))
Else
topCountryValue = CDbl(dataRows(3, 0))
End If
If totalSelected > 0 Then topCountryShare = (topCountryValue / totalSelected) * 100
If totalOrders > 0 Then averageOrderValue = totalSales / totalOrders
End If
End If
' Load monthly totals only when the trend view is requested.
If validationMessage = "" And dataError = "" And rowCount > 0 And chartView = "trend" Then
trendQueryStartDate = startDate
trendQueryEndDate = endDate
If trendComparisonMode Then
trendQueryStartDate = IsoDate(DateSerial(trendPreviousYear, 1, 1))
trendQueryEndDate = IsoDate(Date())
End If
trendSql = "SELECT YEAR(o.DateTimePaid) AS SalesYear, " & _
"MONTH(o.DateTimePaid) AS SalesMonth, " & _
"COUNT(*) AS OrderCount, " & _
"SUM(IFNULL(o.GrandTotal, 0) - IFNULL(o.VATIncluded, 0)) AS SalesTotal " & _
"FROM orders o " & _
"INNER JOIN countries c ON c.Country = o.Country " & _
"WHERE o.IsInvoice = TRUE " & _
"AND o.PaymentReceived = TRUE " & _
"AND o.Status <> 'CANCELLED' " & _
"AND o.Status <> 'ORDER PLACED' "
If dateRange <> "all" Then
trendSql = trendSql & "AND o.DateTimePaid >= ? "
End If
trendSql = trendSql & "AND o.DateTimePaid < DATE_ADD(?, INTERVAL 1 DAY) " & _
"AND TRIM(IFNULL(c.CodeA2, '')) <> '' "
If ukMode = "exclude" Then
trendSql = trendSql & "AND LEFT(c.Country, 14) <> 'United Kingdom' "
End If
trendSql = trendSql & "GROUP BY YEAR(o.DateTimePaid), MONTH(o.DateTimePaid) " & _
"ORDER BY SalesYear ASC, SalesMonth ASC"
On Error Resume Next
Set trendConn = Server.CreateObject("ADODB.Connection")
trendConn.Open "DSN=MySQL_hyperflight;"
If Err.Number = 0 Then
Set trendCmd = Server.CreateObject("ADODB.Command")
Set trendCmd.ActiveConnection = trendConn
trendCmd.CommandType = adCmdText
trendCmd.CommandText = trendSql
If dateRange <> "all" Then
trendCmd.Parameters.Append trendCmd.CreateParameter("@TrendDateFrom", adVarChar, adParamInput, 10, trendQueryStartDate)
End If
trendCmd.Parameters.Append trendCmd.CreateParameter("@TrendDateTo", adVarChar, adParamInput, 10, trendQueryEndDate)
Set trendRs = trendCmd.Execute()
End If
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
If IsObject(trendConn) Then
If trendConn.State <> 0 Then trendConn.Close
Set trendConn = Nothing
End If
On Error GoTo 0
If trendComparisonMode And trendRowCount > 0 Then
For i = 0 To trendRowCount - 1
trendSeriesYear = CInt(trendRows(0, i))
trendSeriesMonth = CInt(trendRows(1, i))
If measure = "orders" Then
trendSeriesValue = CDbl(trendRows(2, i))
Else
trendSeriesValue = CDbl(trendRows(3, i))
End If
Select Case trendSeriesYear
Case trendPreviousYear
trendPreviousValues(trendSeriesMonth) = trendSeriesValue
Case trendSelectedYear
trendSelectedValues(trendSeriesMonth) = trendSeriesValue
Case trendCurrentYear
trendCurrentValues(trendSeriesMonth) = trendSeriesValue
End Select
Next
End If
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 = "orders" Then
pieDisplayedTotal = pieDisplayedTotal + CDbl(dataRows(2, i))
Else
pieDisplayedTotal = pieDisplayedTotal + CDbl(dataRows(3, 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 Sales Map</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 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.25rem; }
.metric-label { color: #6c757d; font-size: 0.9rem; }
.card-header h2 { font-size: 1.05rem; margin: 0; }
.chart-shell { position: relative; min-height: 500px; }
#sales-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);
}
.view-nav .btn { min-width: 110px; }
.country-name { min-width: 190px; }
.table th { white-space: nowrap; }
.small-note { font-size: 0.875rem; color: #6c757d; }
@media (max-width: 767.98px) {
.chart-shell, #sales-chart { min-height: 420px; }
.view-nav .btn { min-width: 0; }
}
</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-2 align-items-lg-center">
<div>
<h1 class="h4 mb-1">HyperFlight Sales Map</h1>
<div class="small opacity-75">Geographical distribution of orders and sales</div>
</div>
<span class="badge text-bg-light"><%=rangeLabel%></span>
</div>
</div>
<div class="card-body">
<form method="get" action="" class="row g-3 align-items-end">
<div class="col-sm-6 col-lg-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-lg-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-lg-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-lg-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="orders" <% If measure = "orders" Then Response.Write "selected" %>>Number of orders</option>
</select>
</div>
<div class="col-sm-6 col-lg-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-3 col-lg-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-sm-3 col-lg-1 d-grid">
<input type="hidden" name="View" value="<%=Html(chartView)%>">
<button type="submit" class="btn btn-primary fw-semibold">Run</button>
</div>
</form>
<div class="small-note mt-3">Sales values exclude VAT. Partial refunds are not taken into account.</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 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">Sales total</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</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(countryCount)%></div>
<div class="metric-label">Countries 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(topCountryName)%></div>
<div class="metric-label">Largest market 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(topCountryShare, 1)%>%</div>
<div class="metric-label">Largest market share</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"><%=Money0(averageOrderValue)%></div>
<div class="metric-label">Average order value</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>
<h2><%=Html(chartTitle)%></h2>
<div class="small opacity-75 mt-1"><%=Html(chartSubtitle)%></div>
</div>
<div class="btn-group btn-group-sm view-nav flex-wrap" 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>
<a class="btn btn-light<% If chartView = "world" Then Response.Write " active" %>" href="<%=Html(BuildViewUrl("world"))%>"<% If chartView = "world" Then Response.Write " aria-current=""page""" %>>World map</a>
<a class="btn btn-light<% If chartView = "europe" Then Response.Write " active" %>" href="<%=Html(BuildViewUrl("europe"))%>"<% If chartView = "europe" Then Response.Write " aria-current=""page""" %>>Europe map</a>
</div>
</div>
</div>
<div class="card-body p-2 p-md-3">
<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="sales-chart" aria-label="<%=Html(chartTitle)%>"></div>
</div>
<% If chartView = "pie" And rowCount > pieCount Then %>
<div class="small-note px-2 pb-1">The pie chart shows the top <%=pieCount%> countries; 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%> countries.</div>
<% ElseIf chartView = "trend" Then %>
<% If trendComparisonMode Then %>
<div class="small-note px-2 pb-1">The trend chart compares <%=trendPreviousYear%>, <%=trendSelectedYear%> and <%=trendCurrentYear%> year to date. <%=MonthName(trendCurrentMonth)%> <%=trendCurrentYear%> is a partial month.</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>Country ranking</h2>
<span class="badge text-bg-light">Top <%=tableCount%></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="country-name">Country</th>
<th>Code</th>
<th class="text-end<% If measure = "orders" Then Response.Write " table-primary" %>">Orders</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
countryCode = CStr(dataRows(0, i))
countryName = CStr(dataRows(1, i))
currentOrders = CLng(dataRows(2, i))
currentSales = CDbl(dataRows(3, i))
If measure = "orders" Then
currentValue = CDbl(currentOrders)
Else
currentValue = currentSales
End If
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(countryName)%></td>
<td><span class="badge text-bg-secondary"><%=Html(countryCode)%></span></td>
<td class="text-end"><%=Number0(currentOrders)%></td>
<td class="text-end"><%=Money0(currentSales)%></td>
<td class="text-end"><%=FormatNumber(shareValue, 1)%>%</td>
</tr>
<% Next %>
</tbody>
</table>
</div>
</div>
<% End If %>
</div>
<script>
(function () {
'use strict';
var rangeSelect = document.getElementById('DateRange');
var startInput = document.getElementById('StartDate');
var endInput = document.getElementById('EndDate');
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 Then %>
<script>
(function () {
'use strict';
var currentView = '<%=JsString(chartView)%>';
var currentMeasure = '<%=JsString(measure)%>';
var chartElement = document.getElementById('sales-chart');
var loadingElement = document.getElementById('chart-loading');
var resizeTimer = null;
var chartsLoaded = false;
google.charts.load('current', {
packages: ['geochart', 'corechart'],
language: 'en-GB'
});
google.charts.setOnLoadCallback(function () {
chartsLoaded = true;
drawSalesChart();
});
function formatValueColumns(data, columnIndexes) {
var formatter;
var i;
if (currentMeasure === 'orders') {
formatter = new google.visualization.NumberFormat({fractionDigits: 0});
} else {
formatter = new google.visualization.NumberFormat({prefix: '£', fractionDigits: 0});
}
for (i = 0; i < columnIndexes.length; i += 1) {
formatter.format(data, columnIndexes[i]);
}
}
function addFormatting(data) {
formatValueColumns(data, [1]);
}
function buildMapData() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Country');
data.addColumn('number', '<%=JsString(valueColumnLabel)%>');
<% For i = 0 To rowCount - 1
countryCode = CStr(dataRows(0, i))
countryName = CStr(dataRows(1, i))
If measure = "orders" Then
currentValue = CDbl(dataRows(2, i))
Else
currentValue = CDbl(dataRows(3, i))
End If
%>
data.addRow([{v: '<%=JsString(countryCode)%>', f: '<%=JsString(countryName)%>'}, <%=JsNumber(currentValue)%>]);
<% Next %>
addFormatting(data);
return data;
}
function buildBarData() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Country');
data.addColumn('number', '<%=JsString(valueColumnLabel)%>');
<% For i = 0 To barCount - 1
countryName = CStr(dataRows(1, i))
If measure = "orders" Then
currentValue = CDbl(dataRows(2, i))
Else
currentValue = CDbl(dataRows(3, i))
End If
%>
data.addRow(['<%=JsString(countryName)%>', <%=JsNumber(currentValue)%>]);
<% Next %>
addFormatting(data);
return data;
}
function buildPieData() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Country');
data.addColumn('number', '<%=JsString(valueColumnLabel)%>');
<% For i = 0 To pieCount - 1
countryName = CStr(dataRows(1, i))
If measure = "orders" Then
currentValue = CDbl(dataRows(2, i))
Else
currentValue = CDbl(dataRows(3, i))
End If
%>
data.addRow(['<%=JsString(countryName)%>', <%=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 trendComparisonMode Then %>
data.addColumn('string', 'Month');
data.addColumn('number', '<%=trendPreviousYear%>');
data.addColumn('number', '<%=trendSelectedYear%>');
data.addColumn('number', '<%=trendCurrentYear%> year to date');
data.addColumn({type: 'string', role: 'annotation'});
<% For i = 1 To 12 %>
data.addRow([
'<%=MonthName(i, True)%>',
<%=JsNumber(trendPreviousValues(i))%>,
<%=JsNumber(trendSelectedValues(i))%>,
<% If i <= trendCurrentMonth Then %><%=JsNumber(trendCurrentValues(i))%><% Else %>null<% End If %>,
<% If i = trendCurrentMonth Then %>'Partial'<% Else %>null<% End If %>
]);
<% Next %>
formatValueColumns(data, [1, 2, 3]);
<% 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 = "orders" Then
currentValue = CDbl(trendRows(2, i))
Else
currentValue = CDbl(trendRows(3, i))
End If
%>
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 drawSalesChart() {
if (!chartsLoaded || !chartElement) return;
var width = Math.max(chartElement.clientWidth, 320);
var height;
var data;
var chart;
var options;
if (currentView === 'bar') {
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: 180, top: 25, width: '70%', height: '86%'},
hAxis: {
minValue: 0,
format: currentMeasure === 'orders' ? '#,##0' : '£#,##0',
textStyle: {fontSize: 12}
},
vAxis: {textStyle: {fontSize: 13}},
colors: ['#0d6efd']
};
} else if (currentView === 'pie') {
height = Math.max(480, Math.min(620, 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(620, Math.round(width * 0.50)));
chartElement.style.height = height + 'px';
data = buildTrendData();
chart = new google.visualization.LineChart(chartElement);
options = {
width: width,
height: height,
backgroundColor: 'transparent',
legend: <% If trendComparisonMode Then %>{position: 'top', alignment: 'center', textStyle: {fontSize: 12}}<% Else %>'none'<% End If %>,
chartArea: {left: width < 700 ? 72 : 95, top: <% If trendComparisonMode Then Response.Write 55 Else Response.Write 25 %>, width: width < 700 ? '76%' : '84%', height: <% If trendComparisonMode Then Response.Write "67" Else Response.Write "72" %> + '%'},
hAxis: {
<% If Not trendComparisonMode 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 === 'orders' ? '#,##0' : '£#,##0',
textStyle: {fontSize: 12}
},
<% If trendComparisonMode Then %>
series: {
0: {lineWidth: 2, pointSize: 4},
1: {lineWidth: 4, pointSize: 5},
2: {lineWidth: 3, pointSize: 5, lineDashStyle: [8, 4]}
},
annotations: {
style: 'point',
textStyle: {fontSize: 11, auraColor: 'none'}
},
colors: ['#adb5bd', '#0d6efd', '#fd7e14'],
interpolateNulls: false,
<% Else %>
lineWidth: 3,
pointSize: <% If trendRowCount <= 36 Then Response.Write 5 Else Response.Write 2 %>,
colors: ['#0d6efd'],
<% End If %>
focusTarget: 'category'
};
} else {
height = currentView === 'europe'
? Math.max(480, Math.min(720, Math.round(width * 0.62)))
: Math.max(460, Math.min(680, Math.round(width * 0.52)));
chartElement.style.height = height + 'px';
data = buildMapData();
chart = new google.visualization.GeoChart(chartElement);
options = {
width: width,
height: height,
region: currentView === 'europe' ? '150' : 'world',
backgroundColor: '#eaf6fd',
datalessRegionColor: '#f3f4f6',
defaultColor: '#f3f4f6',
colorAxis: {minValue: 0, colors: ['#d9f4d2', '#6aaa5d', '#1f6417']},
legend: {textStyle: {fontSize: 12}},
tooltip: {textStyle: {fontSize: 13}}
};
}
google.visualization.events.addListener(chart, 'ready', hideLoading);
chart.draw(data, options);
}
window.addEventListener('resize', function () {
if (!chartsLoaded) return;
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(drawSalesChart, 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>