cover/Elixir.DaProductAppWeb.DashboardLive.html

1
:-(
defmodule DaProductAppWeb.DashboardLive do
2 @moduledoc """
3 Real-time Dashboard LiveView with role-based access and live updates.
4 """
5 use DaProductAppWeb, :live_view
6 import Phoenix.LiveView.Helpers
7 import DaProductAppWeb.CoreComponents
8 alias DaProductApp.Accounts
9 alias DaProductApp.Monitoring
10
11 @impl true
12
:-(
def mount(_params, session, socket) do
13
:-(
case get_current_user(session) do
14
:-(
nil ->
15 {:ok, redirect(socket, to: ~p"/login")}
16
17 user ->
18
:-(
if has_access?(user) do
19
:-(
if connected?(socket) do
20 # Subscribe to real-time updates
21
:-(
Phoenix.PubSub.subscribe(DaProductApp.PubSub, "dashboard_updates")
22
:-(
Phoenix.PubSub.subscribe(DaProductApp.PubSub, "transaction_updates")
23
24 # Set up periodic updates
25
:-(
:timer.send_interval(30_000, self(), :refresh_stats)
26 end
27
28
:-(
socket =
29 socket
30 |> assign(:current_user, user)
31 |> assign(:page_title, get_page_title())
32 |> assign(:current_page, :dashboard)
33 |> assign(:stats, load_dashboard_stats(user))
34 |> assign(:recent_transactions, load_recent_transactions(user))
35 |> assign(:activities, load_recent_activities(user))
36 |> assign(:quick_actions, get_quick_actions(user))
37
38 {:ok, socket}
39 else
40 {:ok,
41 socket
42 |> put_flash(:error, "Access denied")
43 |> redirect(to: ~p"/login")}
44 end
45 end
46 end
47
48
:-(
def get_page_title(), do: "Dashboard"
49
50 def mount_with_user(socket, user) do
51
:-(
if connected?(socket) do
52 # Subscribe to real-time updates
53
:-(
Phoenix.PubSub.subscribe(DaProductApp.PubSub, "dashboard_updates")
54
:-(
Phoenix.PubSub.subscribe(DaProductApp.PubSub, "transaction_updates")
55
56 # Set up periodic updates
57
:-(
:timer.send_interval(30_000, self(), :refresh_stats)
58 end
59
60
:-(
socket =
61 socket
62 |> assign(:stats, load_dashboard_stats(user))
63 |> assign(:recent_transactions, load_recent_transactions(user))
64 |> assign(:activities, load_recent_activities(user))
65 |> assign(:quick_actions, get_quick_actions(user))
66
67
:-(
socket
68 end
69
70 @impl true
71
:-(
def handle_info(:refresh_stats, socket) do
72
:-(
user = socket.assigns.current_user
73
74 {:noreply,
75 socket
76 |> assign(:stats, load_dashboard_stats(user))
77 |> assign(:recent_transactions, load_recent_transactions(user))
78 |> assign(:activities, load_recent_activities(user))}
79 end
80
81 @impl true
82
:-(
def handle_info({:transaction_update, _transaction}, socket) do
83
:-(
user = socket.assigns.current_user
84
85 {:noreply,
86 socket
87 |> assign(:stats, load_dashboard_stats(user))
88 |> assign(:recent_transactions, load_recent_transactions(user))
89 |> assign(:activities, load_recent_activities(user))}
90 end
91
92 @impl true
93
:-(
def handle_info({:req_pay_update, _req_pay}, socket) do
94
:-(
user = socket.assigns.current_user
95
96 {:noreply,
97 socket
98 |> assign(:stats, load_dashboard_stats(user))
99 |> assign(:recent_transactions, load_recent_transactions(user))
100 |> assign(:activities, load_recent_activities(user))}
101 end
102
103 @impl true
104
:-(
def handle_info({:dashboard_update, _payload}, socket) do
105
:-(
user = socket.assigns.current_user
106
107 {:noreply,
108 socket
109 |> assign(:stats, load_dashboard_stats(user))
110 |> assign(:recent_transactions, load_recent_transactions(user))
111 |> assign(:activities, load_recent_activities(user))}
112 end
113
114 @impl true
115
:-(
def handle_event("refresh_dashboard", _params, socket) do
116
:-(
user = socket.assigns.current_user
117
118 {:noreply,
119 socket
120 |> assign(:stats, load_dashboard_stats(user))
121 |> assign(:recent_transactions, load_recent_transactions(user))
122 |> assign(:activities, load_recent_activities(user))
123 |> put_flash(:info, "Dashboard refreshed successfully!")}
124 end
125
126 # Handle theme switching
127
:-(
def handle_event("toggle_theme", %{"theme" => _theme}, socket) do
128 {:noreply, socket}
129 end
130
131 # Handle the hide user menu event
132
:-(
def handle_event("hide_user_menu", _params, socket) do
133 {:noreply, socket}
134 end
135
136 # Private helper functions
137 defp load_dashboard_stats(_user) do
138 # Fetch analytics from Monitoring context
139
:-(
analytics = Monitoring.get_dashboard_analytics()
140
141
:-(
tx_stats = Map.get(analytics, :transactions, %{})
142
:-(
req_pay_stats = Map.get(analytics, :req_pays, %{})
143
144
:-(
total_tx = Map.get(tx_stats, :total_count, 0)
145
:-(
success_rate = Map.get(tx_stats, :success_rate, 0.0)
146
147 # Count active users (simple implementation).
148
:-(
total_users = Accounts.list_users() |> length()
149
150 # Compute total revenue from req_pays corridor_stats (sum of amounts)
151
:-(
total_revenue =
152 req_pay_stats
153 |> Map.get(:corridor_stats, [])
154 |> Enum.reduce(Decimal.new(0), fn
155
:-(
{_corridor, _count, amt}, acc when is_binary(amt) -> Decimal.add(acc, Decimal.new(amt))
156
:-(
{_corridor, _count, %Decimal{} = amt}, acc -> Decimal.add(acc, amt)
157
:-(
{_corridor, _count, nil}, acc -> acc
158
:-(
{_corridor, _count, amt}, acc when is_number(amt) -> Decimal.add(acc, Decimal.new(amt))
159 end)
160
161
:-(
%{
162 total_transactions: %{
163 title: "Total Transactions",
164 value: Integer.to_string(total_tx),
165 icon: "credit-card",
166 trend: nil,
167 color: "blue"
168 },
169 total_users: %{
170 title: "Active Users",
171 value: Integer.to_string(total_users),
172 icon: "users",
173 trend: nil,
174 color: "green"
175 },
176 total_revenue: %{
177 title: "Revenue",
178 value: "₹" <> Decimal.to_string(total_revenue, :normal),
179 icon: "chart-bar",
180 trend: nil,
181 color: "yellow"
182 },
183 success_rate: %{
184 title: "Success Rate",
185 value: :erlang.float_to_binary(success_rate, [decimals: 2]) <> "%",
186 icon: "check-circle",
187 trend: nil,
188 color: "green"
189 }
190 }
191 end
192
193 defp load_recent_transactions(_user) do
194 # Load recent transactions from Monitoring for the last 24 hours (limit 5)
195
:-(
now = DateTime.utc_now()
196
:-(
date_from = DateTime.add(now, -86_400, :second)
197
198
:-(
transactions =
199 Monitoring.list_transactions(page_size: 5, date_from: date_from, date_to: now)
200
201
:-(
Enum.map(transactions, fn t ->
202
:-(
%{
203
:-(
db_id: t.id,
204
:-(
id: t.org_txn_id || "TXN#{t.id}",
205
:-(
amount: if(t.inr_amount, do: "₹" <> format_amount_for_dashboard(t.inr_amount), else: "N/A"),
206
:-(
status: t.status || "unknown",
207
:-(
type: t.transaction_type || "N/A",
208
:-(
merchant: t.payee_name || t.payee_addr || t.payee_mid || "N/A",
209
:-(
description: (t.payee_name || t.payee_addr || "Payment"),
210
:-(
time: relative_time(t.inserted_at, now)
211 }
212 end)
213 end
214
215 # Format amount for dashboard (simple, round to 2 decimals)
216 defp format_amount_for_dashboard(%Decimal{} = d) do
217 d
218 |> Decimal.round(2)
219
:-(
|> Decimal.to_string(:normal)
220 end
221
222
:-(
defp format_amount_for_dashboard(amount) when is_binary(amount), do: amount
223
:-(
defp format_amount_for_dashboard(amount) when is_number(amount), do: :erlang.float_to_binary(amount, decimals: 2)
224
225 # human readable relative time
226
:-(
defp relative_time(nil, _now), do: "N/A"
227 defp relative_time(%NaiveDateTime{} = ndt, now) do
228
:-(
dt = DateTime.from_naive!(ndt, "Etc/UTC")
229
:-(
relative_time(dt, now)
230 end
231
232 defp relative_time(%DateTime{} = dt, now) do
233
:-(
seconds = DateTime.diff(now, dt, :second)
234
:-(
cond do
235
:-(
seconds < 60 -> "#{seconds}s ago"
236
:-(
seconds < 3600 -> "#{div(seconds, 60)} mins ago"
237
:-(
seconds < 86_400 -> "#{div(seconds, 3600)} hours ago"
238
:-(
true -> "#{div(seconds, 86_400)} days ago"
239 end
240 end
241
242
:-(
defp load_recent_activities(_user) do
243 [
244 %{
245 title: "New merchant onboarded",
246 description: "ABC Electronics registered successfully",
247 time: "10 mins ago",
248 icon: "user-plus",
249 status: "success"
250 },
251 %{
252 title: "Payment gateway maintenance",
253 description: "Scheduled maintenance completed",
254 time: "1 hour ago",
255 icon: "cog",
256 status: "info"
257 },
258 %{
259 title: "High volume alert",
260 description: "Transaction volume exceeded threshold",
261 time: "2 hours ago",
262 icon: "exclamation",
263 status: "warning"
264 }
265 ]
266 end
267
268
:-(
defp get_quick_actions(_user) do
269 [
270
271 %{
272 title: "View Reports",
273 description: "Generate analytics reports",
274 path: "/reports",
275 icon: "chart-bar"
276 },
277 %{
278 title: "Manage Users",
279 description: "User management panel",
280 path: "/users",
281 icon: "users"
282 },
283 %{
284 title: "System Settings",
285 description: "Configure system settings",
286 path: "/settings",
287 icon: "cog"
288 }
289 ]
290 end
291
292 # Component definitions
293 attr :title, :string, required: true
294 attr :value, :string, required: true
295 attr :icon, :string, required: true
296 attr :trend, :map, default: nil
297 attr :color, :string, default: "blue"
298
299 def stat_card(assigns) do
300
:-(
~H"""
301 <div class="stats shadow bg-white">
302 <div class="stat">
303 <div class="stat-figure">
304 <div class={[
305 "w-8 h-8 rounded-md flex items-center justify-center",
306
:-(
case @color do
307
:-(
"blue" -> "bg-blue-500"
308
:-(
"green" -> "bg-green-500"
309
:-(
"yellow" -> "bg-yellow-500"
310
:-(
"red" -> "bg-red-500"
311
:-(
_ -> "bg-gray-500"
312 end
313 ]}>
314 <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
315
:-(
<%= case @icon do %>
316 <% "credit-card" -> %>
317 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"></path>
318 <% "users" -> %>
319 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"></path>
320 <% "chart-bar" -> %>
321 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
322 <% "check-circle" -> %>
323 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
324 <% _ -> %>
325 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
326 <% end %>
327 </svg>
328 </div>
329 </div>
330
:-(
<div class="stat-title"><%= @title %></div>
331
:-(
<div class="stat-value text-lg"><%= @value %></div>
332
:-(
<%= if @trend do %>
333 <div class="stat-desc">
334 <span class={[
335 "font-medium",
336
:-(
if(@trend.direction == "up", do: "text-green-600", else: "text-red-600")
337 ]}>
338
:-(
<%= @trend.value %>
339 </span>
340 <span class="text-gray-500"> from last month</span>
341 </div>
342 <% end %>
343 </div>
344 </div>
345 """
346 end
347
348 # Modern Quick Action Card Component
349 attr :title, :string, required: true
350 attr :description, :string, required: true
351 attr :path, :string, required: true
352 attr :icon, :string, required: true
353
354 def modern_quick_action_card(assigns) do
355
:-(
~H"""
356
:-(
<a href={@path} class="btn btn-outline btn-sm justify-start h-auto p-4 normal-case hover:btn-primary group">
357 <div class="flex items-center space-x-3 w-full">
358 <div class="flex-shrink-0">
359 <div class="w-8 h-8 bg-primary/10 group-hover:bg-primary/20 rounded-lg flex items-center justify-center transition-colors">
360 <svg class="w-4 h-4 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
361
:-(
<%= case @icon do %>
362 <% "plus" -> %>
363 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
364 <% "chart-bar" -> %>
365 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
366 <% "users" -> %>
367 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"></path>
368 <% "cog" -> %>
369 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
370 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
371 <% _ -> %>
372 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
373 <% end %>
374 </svg>
375 </div>
376 </div>
377 <div class="flex-1 text-left">
378
:-(
<p class="font-medium text-sm"><%= @title %></p>
379
:-(
<p class="text-xs opacity-70"><%= @description %></p>
380 </div>
381 </div>
382 </a>
383 """
384 end
385
386 # Modern Activity Item Component
387 attr :title, :string, required: true
388 attr :description, :string, required: true
389 attr :time, :string, required: true
390 attr :icon, :string, required: true
391 attr :status, :string, default: "info"
392
393 def modern_activity_item(assigns) do
394
:-(
~H"""
395 <div class="flex items-start space-x-3 p-3 bg-base-200/30 rounded-lg">
396 <div class="flex-shrink-0">
397 <div class={[
398 "w-8 h-8 rounded-full flex items-center justify-center",
399
:-(
@status == "success" && "bg-success/20 text-success",
400
:-(
@status == "warning" && "bg-warning/20 text-warning",
401
:-(
@status == "info" && "bg-info/20 text-info",
402
:-(
@status == "error" && "bg-error/20 text-error"
403 ]}>
404 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
405
:-(
<%= case @icon do %>
406 <% "user-plus" -> %>
407 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"></path>
408 <% "cog" -> %>
409 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
410 <% "exclamation" -> %>
411 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
412 <% _ -> %>
413 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
414 <% end %>
415 </svg>
416 </div>
417 </div>
418 <div class="flex-1 min-w-0">
419
:-(
<p class="text-sm font-medium text-base-content"><%= @title %></p>
420
:-(
<p class="text-sm text-base-content/70"><%= @description %></p>
421
:-(
<p class="text-xs text-base-content/50 mt-1"><%= @time %></p>
422 </div>
423 </div>
424 """
425 end
426
427 # Helper functions for authentication and access control
428
:-(
defp get_current_user(session) do
429
:-(
case session do
430 %{"user_id" => user_id} when is_binary(user_id) or is_integer(user_id) ->
431
:-(
Accounts.get_user!(user_id)
432
:-(
_ ->
433 nil
434 end
435 rescue
436
:-(
_ -> nil
437 end
438
439
:-(
defp has_access?(_user), do: true
440 end
Line Hits Source