| 1 |
:-( |
defmodule DaProductAppWeb.AnalyticsLive do |
| 2 |
|
@moduledoc """ |
| 3 |
|
LiveView for Analytics Dashboard with comprehensive insights. |
| 4 |
|
|
| 5 |
|
Features: |
| 6 |
|
- Transaction analytics and metrics |
| 7 |
|
- QR validation analytics |
| 8 |
|
- Performance metrics |
| 9 |
|
- Daily trends and charts |
| 10 |
|
- Corridor performance analysis |
| 11 |
|
- Real-time data updates |
| 12 |
|
""" |
| 13 |
|
use DaProductAppWeb, :live_view |
| 14 |
|
|
| 15 |
|
alias DaProductApp.Accounts |
| 16 |
|
alias DaProductApp.Monitoring |
| 17 |
|
|
| 18 |
|
@impl true |
| 19 |
:-( |
def mount(_params, session, socket) do |
| 20 |
:-( |
case get_current_user(session) do |
| 21 |
:-( |
nil -> |
| 22 |
|
{:ok, redirect(socket, to: ~p"/login")} |
| 23 |
|
|
| 24 |
|
user -> |
| 25 |
:-( |
if has_access?(user) do |
| 26 |
:-( |
if connected?(socket) do |
| 27 |
|
# Subscribe to analytics events for real-time updates |
| 28 |
:-( |
Phoenix.PubSub.subscribe(DaProductApp.PubSub, "analytics") |
| 29 |
:-( |
Phoenix.PubSub.subscribe(DaProductApp.PubSub, "transactions") |
| 30 |
:-( |
Phoenix.PubSub.subscribe(DaProductApp.PubSub, "qr_validations") |
| 31 |
|
end |
| 32 |
|
|
| 33 |
|
# Default to last 30 days |
| 34 |
:-( |
end_date = Date.utc_today() |
| 35 |
:-( |
start_date = Date.add(end_date, -30) |
| 36 |
|
|
| 37 |
:-( |
socket = |
| 38 |
|
socket |
| 39 |
|
|> assign(:current_user, user) |
| 40 |
|
|> assign(:page_title, "Analytics Dashboard") |
| 41 |
|
|> assign(:current_page, :analytics) |
| 42 |
|
|> assign(:date_from, start_date) |
| 43 |
|
|> assign(:date_to, end_date) |
| 44 |
|
|> assign(:analytics_data, %{}) |
| 45 |
|
|> assign(:selected_period, "30_days") |
| 46 |
|
|> load_analytics() |
| 47 |
|
|
| 48 |
|
{:ok, socket} |
| 49 |
|
else |
| 50 |
|
{:ok, |
| 51 |
|
socket |
| 52 |
|
|> put_flash(:error, "Access denied") |
| 53 |
|
|> redirect(to: ~p"/login")} |
| 54 |
|
end |
| 55 |
|
end |
| 56 |
|
end |
| 57 |
|
|
| 58 |
|
@impl true |
| 59 |
:-( |
def handle_params(params, _url, socket) do |
| 60 |
:-( |
{:noreply, apply_action(socket, socket.assigns.live_action, params)} |
| 61 |
|
end |
| 62 |
|
|
| 63 |
|
defp apply_action(socket, :index, _params) do |
| 64 |
|
socket |
| 65 |
:-( |
|> assign(:page_title, "Analytics Dashboard") |
| 66 |
|
end |
| 67 |
|
|
| 68 |
|
@impl true |
| 69 |
:-( |
def handle_event("change_period", %{"period" => period}, socket) do |
| 70 |
:-( |
{start_date, end_date} = get_date_range_for_period(period) |
| 71 |
|
|
| 72 |
:-( |
socket = |
| 73 |
|
socket |
| 74 |
|
|> assign(:selected_period, period) |
| 75 |
|
|> assign(:date_from, start_date) |
| 76 |
|
|> assign(:date_to, end_date) |
| 77 |
|
|> load_analytics() |
| 78 |
|
|
| 79 |
|
{:noreply, socket} |
| 80 |
|
end |
| 81 |
|
|
| 82 |
:-( |
def handle_event("custom_date_range", %{"date_from" => date_from, "date_to" => date_to}, socket) do |
| 83 |
:-( |
parsed_date_from = if date_from != "", do: Date.from_iso8601!(date_from), else: Date.utc_today() |
| 84 |
:-( |
parsed_date_to = if date_to != "", do: Date.from_iso8601!(date_to), else: Date.utc_today() |
| 85 |
|
|
| 86 |
:-( |
socket = |
| 87 |
|
socket |
| 88 |
|
|> assign(:selected_period, "custom") |
| 89 |
|
|> assign(:date_from, parsed_date_from) |
| 90 |
|
|> assign(:date_to, parsed_date_to) |
| 91 |
|
|> load_analytics() |
| 92 |
|
|
| 93 |
|
{:noreply, socket} |
| 94 |
|
end |
| 95 |
|
|
| 96 |
:-( |
def handle_event("refresh", _params, socket) do |
| 97 |
:-( |
socket = |
| 98 |
|
socket |
| 99 |
|
|> load_analytics() |
| 100 |
|
|> put_flash(:info, "Analytics data refreshed") |
| 101 |
|
|
| 102 |
|
{:noreply, socket} |
| 103 |
|
end |
| 104 |
|
|
| 105 |
|
# Handle real-time updates from PubSub |
| 106 |
|
@impl true |
| 107 |
:-( |
def handle_info({:transaction_created, _transaction}, socket) do |
| 108 |
|
{:noreply, load_analytics(socket)} |
| 109 |
|
end |
| 110 |
|
|
| 111 |
:-( |
def handle_info({:transaction_updated, _transaction}, socket) do |
| 112 |
|
{:noreply, load_analytics(socket)} |
| 113 |
|
end |
| 114 |
|
|
| 115 |
:-( |
def handle_info({:qr_validation_created, _qr_validation}, socket) do |
| 116 |
|
{:noreply, load_analytics(socket)} |
| 117 |
|
end |
| 118 |
|
|
| 119 |
:-( |
def handle_info({:qr_validation_updated, _qr_validation}, socket) do |
| 120 |
|
{:noreply, load_analytics(socket)} |
| 121 |
|
end |
| 122 |
|
|
| 123 |
|
defp load_analytics(socket) do |
| 124 |
:-( |
analytics_data = Monitoring.get_dashboard_analytics(socket.assigns.date_from, socket.assigns.date_to) |
| 125 |
:-( |
assign(socket, :analytics_data, analytics_data) |
| 126 |
|
end |
| 127 |
|
|
| 128 |
:-( |
defp get_date_range_for_period(period) do |
| 129 |
:-( |
end_date = Date.utc_today() |
| 130 |
|
|
| 131 |
:-( |
start_date = case period do |
| 132 |
:-( |
"7_days" -> Date.add(end_date, -7) |
| 133 |
:-( |
"30_days" -> Date.add(end_date, -30) |
| 134 |
:-( |
"90_days" -> Date.add(end_date, -90) |
| 135 |
:-( |
"1_year" -> Date.add(end_date, -365) |
| 136 |
:-( |
_ -> Date.add(end_date, -30) |
| 137 |
|
end |
| 138 |
|
|
| 139 |
|
{start_date, end_date} |
| 140 |
|
end |
| 141 |
|
|
| 142 |
|
# Helper functions for the template |
| 143 |
:-( |
defp format_amount(nil), do: "0" |
| 144 |
|
defp format_amount(amount) when is_binary(amount) do |
| 145 |
:-( |
case Decimal.parse(amount) do |
| 146 |
:-( |
{decimal, _} -> format_decimal(decimal) |
| 147 |
:-( |
:error -> amount |
| 148 |
|
end |
| 149 |
|
end |
| 150 |
:-( |
defp format_amount(%Decimal{} = amount), do: format_decimal(amount) |
| 151 |
:-( |
defp format_amount(amount) when is_number(amount), do: :erlang.float_to_binary(amount, decimals: 2) |
| 152 |
|
|
| 153 |
|
defp format_decimal(%Decimal{} = decimal) do |
| 154 |
|
decimal |
| 155 |
|
|> Decimal.round(2) |
| 156 |
:-( |
|> Decimal.to_string() |
| 157 |
|
end |
| 158 |
|
|
| 159 |
|
defp format_currency(amount, currency) when currency in ["USD", "SGD", "AED"] do |
| 160 |
|
case currency do |
| 161 |
|
"USD" -> "$#{format_amount(amount)}" |
| 162 |
|
"SGD" -> "S$#{format_amount(amount)}" |
| 163 |
|
"AED" -> "AED #{format_amount(amount)}" |
| 164 |
|
_ -> "#{format_amount(amount)} #{currency}" |
| 165 |
|
end |
| 166 |
|
end |
| 167 |
|
defp format_currency(amount, currency), do: "#{format_amount(amount)} #{currency || "INR"}" |
| 168 |
|
|
| 169 |
|
defp format_number(number) when is_integer(number) do |
| 170 |
|
number |
| 171 |
|
|> Integer.to_string() |
| 172 |
|
|> String.reverse() |
| 173 |
|
|> String.replace(~r/(\d{3})(?=\d)/, "\\1,") |
| 174 |
:-( |
|> String.reverse() |
| 175 |
|
end |
| 176 |
:-( |
defp format_number(number), do: to_string(number) |
| 177 |
|
|
| 178 |
|
defp calculate_percentage(part, total) when total > 0 do |
| 179 |
:-( |
Float.round(part / total * 100, 1) |
| 180 |
|
end |
| 181 |
:-( |
defp calculate_percentage(_, _), do: 0.0 |
| 182 |
|
|
| 183 |
:-( |
defp period_options do |
| 184 |
|
[ |
| 185 |
|
{"Last 7 Days", "7_days"}, |
| 186 |
|
{"Last 30 Days", "30_days"}, |
| 187 |
|
{"Last 90 Days", "90_days"}, |
| 188 |
|
{"Last Year", "1_year"}, |
| 189 |
|
{"Custom Range", "custom"} |
| 190 |
|
] |
| 191 |
|
end |
| 192 |
|
|
| 193 |
|
defp prepare_chart_data(trends) do |
| 194 |
|
# Prepare data for chart.js or similar charting library |
| 195 |
|
trends |
| 196 |
|
|> Enum.map(fn {date, count} -> |
| 197 |
|
%{ |
| 198 |
|
date: Date.to_iso8601(date), |
| 199 |
|
value: count |
| 200 |
|
} |
| 201 |
|
end) |
| 202 |
|
|> Jason.encode!() |
| 203 |
|
end |
| 204 |
|
|
| 205 |
|
defp get_trend_comparison(current_data, previous_data) do |
| 206 |
|
current_total = Enum.reduce(current_data, 0, fn {_, count}, acc -> acc + count end) |
| 207 |
|
previous_total = Enum.reduce(previous_data, 0, fn {_, count}, acc -> acc + count end) |
| 208 |
|
|
| 209 |
|
if previous_total > 0 do |
| 210 |
|
change_percent = ((current_total - previous_total) / previous_total * 100) |> Float.round(1) |
| 211 |
|
{current_total - previous_total, change_percent} |
| 212 |
|
else |
| 213 |
|
{current_total, 0.0} |
| 214 |
|
end |
| 215 |
|
end |
| 216 |
|
|
| 217 |
|
# Helper functions for authentication and access control |
| 218 |
:-( |
defp get_current_user(session) do |
| 219 |
:-( |
case session do |
| 220 |
|
%{"user_id" => user_id} when is_binary(user_id) or is_integer(user_id) -> |
| 221 |
:-( |
DaProductApp.Accounts.get_user!(user_id) |
| 222 |
:-( |
_ -> |
| 223 |
|
nil |
| 224 |
|
end |
| 225 |
|
rescue |
| 226 |
:-( |
_ -> nil |
| 227 |
|
end |
| 228 |
|
|
| 229 |
:-( |
defp has_access?(_user), do: true |
| 230 |
|
end |