Packages
phoenix_kit
1.3.0
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/live/modules/emails/metrics.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title="Email Analytics"
current_path={@url_path}
project_title={@project_title}
>
<div class="container flex-col mx-auto px-4 py-6">
<%!-- Header Section --%>
<header class="w-full relative mb-6">
<%!-- Back Button --%>
<.link
navigate={Routes.path("/admin")}
class="btn btn-outline btn-primary btn-sm absolute left-0 top-0 -mb-12"
>
<.icon_arrow_left /> Back to Admin
</.link>
<%!-- Title Section --%>
<div class="text-center">
<h1 class="text-4xl font-bold text-base-content mb-3">
Email Dashboard
</h1>
<p class="text-lg text-base-content/70">
Comprehensive email performance metrics and insights
</p>
</div>
</header>
<%!-- Controls Section --%>
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between mb-6 gap-4">
<%!-- Period Selection --%>
<div class="flex flex-wrap gap-2">
<button
class={[
"btn btn-sm",
(@period == :last_24_hours && "btn-primary") || "btn-outline"
]}
phx-click="change_period"
phx-value-period="last_24_hours"
>
24 Hours
</button>
<button
class={[
"btn btn-sm",
(@period == :last_7_days && "btn-primary") || "btn-outline"
]}
phx-click="change_period"
phx-value-period="last_7_days"
>
7 Days
</button>
<button
class={[
"btn btn-sm",
(@period == :last_30_days && "btn-primary") || "btn-outline"
]}
phx-click="change_period"
phx-value-period="last_30_days"
>
30 Days
</button>
<button
class={[
"btn btn-sm",
(@period == :last_90_days && "btn-primary") || "btn-outline"
]}
phx-click="change_period"
phx-value-period="last_90_days"
>
90 Days
</button>
<button
class="btn btn-sm btn-outline"
phx-click="toggle_custom_range"
>
Custom Range
</button>
</div>
<%!-- Action Buttons --%>
<div class="flex gap-2">
<button
class="btn btn-sm btn-outline"
phx-click="refresh"
disabled={@loading}
>
<%= if @loading do %>
<span class="loading loading-spinner loading-xs"></span>
<% else %>
<PhoenixKitWeb.Components.Core.Icons.icon_refresh class="w-4 h-4" />
<% end %>
Refresh
</button>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-sm btn-outline">
<PhoenixKitWeb.Components.Core.Icons.icon_download class="w-4 h-4" /> Export
</div>
<ul
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<li>
<a phx-click="export_metrics" phx-value-format="csv">
Export as CSV
</a>
</li>
<li>
<a phx-click="export_metrics" phx-value-format="json">
Export as JSON
</a>
</li>
</ul>
</div>
</div>
</div>
<%!-- Custom Date Range Modal --%>
<%= if @custom_range do %>
<div class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Select Custom Date Range</h3>
<form phx-submit="apply_custom_range">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="label">
<span class="label-text">Start Date</span>
</label>
<input
type="date"
name="start_date"
class="input input-bordered w-full"
max={Date.utc_today() |> Date.to_iso8601()}
required
/>
</div>
<div>
<label class="label">
<span class="label-text">End Date</span>
</label>
<input
type="date"
name="end_date"
class="input input-bordered w-full"
max={Date.utc_today() |> Date.to_iso8601()}
required
/>
</div>
</div>
<div class="modal-action">
<button type="submit" class="btn btn-primary">Apply</button>
<button type="button" class="btn" phx-click="toggle_custom_range">Cancel</button>
</div>
</form>
</div>
</div>
<% end %>
<%!-- Loading State --%>
<%= if @loading do %>
<div class="flex justify-center items-center h-32">
<span class="loading loading-spinner loading-lg"></span>
<span class="ml-3 text-lg">Loading analytics data...</span>
</div>
<% else %>
<%!-- KPI Cards --%>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<%!-- Total Sent --%>
<div class="card bg-primary text-primary-content shadow-sm">
<div class="card-body p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-primary-content/70 text-sm">Total Sent</p>
<p class="text-2xl font-bold">{format_number(@metrics.total_sent || 0)}</p>
</div>
<div class="text-primary-content/70">
<PhoenixKitWeb.Components.Core.Icons.icon_email class="w-8 h-8" />
</div>
</div>
</div>
</div>
<%!-- Delivery Rate --%>
<div class="card bg-success text-success-content shadow-sm">
<div class="card-body p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-success-content/70 text-sm">Delivery Rate</p>
<p class="text-2xl font-bold">{format_percentage(@metrics.delivery_rate || 0)}</p>
</div>
<div class="text-success-content/70">
<PhoenixKitWeb.Components.Core.Icons.icon_check class="w-8 h-8" />
</div>
</div>
</div>
</div>
<%!-- Bounce Rate --%>
<div class="card bg-warning text-warning-content shadow-sm">
<div class="card-body p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-warning-content/70 text-sm">Bounce Rate</p>
<p class="text-2xl font-bold">{format_percentage(@metrics.bounce_rate || 0)}</p>
</div>
<div class="text-warning-content/70">
<PhoenixKitWeb.Components.Core.Icons.icon_warning_filled class="w-8 h-8" />
</div>
</div>
</div>
</div>
<%!-- Open Rate --%>
<div class="card bg-info text-info-content shadow-sm">
<div class="card-body p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-info-content/70 text-sm">Open Rate</p>
<p class="text-2xl font-bold">{format_percentage(@metrics.open_rate || 0)}</p>
</div>
<div class="text-info-content/70">
<PhoenixKitWeb.Components.Core.Icons.icon_eye class="w-8 h-8" />
</div>
</div>
</div>
</div>
</div>
<%!-- Charts Section --%>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<%!-- Delivery Trends Chart --%>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<h2 class="card-title text-lg mb-4">Delivery Trends</h2>
<div
class="w-full h-64"
id="delivery-chart-container"
phx-update="ignore"
data-chart-data={Jason.encode!(Map.get(@charts_data, :delivery_trend, %{}))}
>
<canvas id="delivery-trend-chart" width="400" height="200"></canvas>
</div>
</div>
</div>
<%!-- Engagement Chart --%>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<h2 class="card-title text-lg mb-4">Engagement Metrics</h2>
<div
class="w-full h-64"
id="engagement-chart-container"
phx-update="ignore"
data-chart-data={Jason.encode!(Map.get(@charts_data, :engagement, %{}))}
>
<canvas id="engagement-chart" width="400" height="200"></canvas>
</div>
</div>
</div>
</div>
<%!-- Additional Analytics --%>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<%!-- Provider Performance --%>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<h2 class="card-title text-lg mb-4">Provider Performance</h2>
<%= if @metrics.by_provider && length(Map.keys(@metrics.by_provider)) > 0 do %>
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th>Provider</th>
<th>Sent</th>
<th>Delivery Rate</th>
<th>Bounce Rate</th>
</tr>
</thead>
<tbody>
<%= for {provider, stats} <- @metrics.by_provider do %>
<tr>
<td class="font-medium">{provider}</td>
<td>{format_number(stats.total_sent || 0)}</td>
<td>
<span class={[
"badge badge-sm",
((stats.delivery_rate || 0) >= 95 && "badge-success") ||
((stats.delivery_rate || 0) >= 85 && "badge-warning") ||
"badge-error"
]}>
{format_percentage(stats.delivery_rate || 0)}
</span>
</td>
<td>{format_percentage(stats.bounce_rate || 0)}</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% else %>
<div class="text-center text-base-content/50 py-8">
No provider data available
</div>
<% end %>
</div>
</div>
<%!-- Recent Activity --%>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<h2 class="card-title text-lg mb-4">System Status</h2>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm">Email</span>
<span class="badge badge-success">Active</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm">Last Updated</span>
<span class="text-sm text-base-content/70">
{UtilsDate.format_datetime_with_user_format(@last_updated)}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm">Total Emails Today</span>
<span class="text-sm font-medium">
{format_number(@metrics.today_count || 0)}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm">Data Retention</span>
<span class="text-sm text-base-content/70">
{PhoenixKit.Emails.get_retention_days()} days
</span>
</div>
</div>
</div>
</div>
</div>
<% end %>
</div>
<%!-- Chart.js CDN and Simple Chart System --%>
<script src="https://cdn.jsdelivr.net/npm/chart.js">
</script>
<script>
(() => {
// Simple Chart Manager for PhoenixKit
const PhoenixKitCharts = {
deliveryChart: null,
engagementChart: null,
initializeCharts() {
this.initDeliveryChart();
this.initEngagementChart();
},
initDeliveryChart() {
const container = document.getElementById('delivery-chart-container');
const canvas = document.getElementById('delivery-trend-chart');
if (!canvas || !container) return;
// Destroy existing chart
if (this.deliveryChart) {
this.deliveryChart.destroy();
}
// Get data from HTML attribute
let chartData = {};
try {
const dataAttr = container.getAttribute('data-chart-data');
chartData = dataAttr ? JSON.parse(dataAttr) : {};
console.log('Delivery chart data loaded:', chartData);
} catch (e) {
console.warn('Failed to parse delivery chart data:', e);
}
// Create chart with data or defaults
this.deliveryChart = new Chart(canvas, {
type: 'line',
data: chartData.labels ? chartData : {
labels: [],
datasets: [
{
label: "Delivered",
data: [],
borderColor: "rgb(34, 197, 94)",
backgroundColor: "rgba(34, 197, 94, 0.1)",
tension: 0.1,
fill: true
},
{
label: "Bounced",
data: [],
borderColor: "rgb(239, 68, 68)",
backgroundColor: "rgba(239, 68, 68, 0.1)",
tension: 0.1,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
grid: { color: 'rgba(0, 0, 0, 0.1)' }
},
x: {
grid: { color: 'rgba(0, 0, 0, 0.1)' }
}
},
plugins: {
legend: { position: 'top' },
tooltip: {
mode: 'index',
intersect: false,
}
}
}
});
},
initEngagementChart() {
const container = document.getElementById('engagement-chart-container');
const canvas = document.getElementById('engagement-chart');
if (!canvas || !container) return;
// Destroy existing chart
if (this.engagementChart) {
this.engagementChart.destroy();
}
// Get data from HTML attribute
let chartData = {};
try {
const dataAttr = container.getAttribute('data-chart-data');
chartData = dataAttr ? JSON.parse(dataAttr) : {};
console.log('Engagement chart data loaded:', chartData);
} catch (e) {
console.warn('Failed to parse engagement chart data:', e);
}
// Create chart with data or defaults
this.engagementChart = new Chart(canvas, {
type: 'doughnut',
data: chartData.labels ? chartData : {
labels: ["Opens", "Clicks", "Bounces", "Complaints"],
datasets: [{
data: [0, 0, 0, 0],
backgroundColor: [
"rgb(59, 130, 246)",
"rgb(34, 197, 94)",
"rgb(251, 191, 36)",
"rgb(239, 68, 68)"
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom' },
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
return `${label}: ${value} (${percentage}%)`;
}
}
}
}
}
});
},
updateCharts(chartsData) {
// Update delivery chart
if (this.deliveryChart && chartsData.delivery_trend) {
this.deliveryChart.data = chartsData.delivery_trend;
this.deliveryChart.update();
}
// Update engagement chart
if (this.engagementChart && chartsData.engagement) {
this.engagementChart.data = chartsData.engagement;
this.engagementChart.update();
}
},
cleanup() {
if (this.deliveryChart) {
this.deliveryChart.destroy();
this.deliveryChart = null;
}
if (this.engagementChart) {
this.engagementChart.destroy();
this.engagementChart = null;
}
}
};
// Listen for LiveView updates
window.addEventListener('phx:email-charts-update', (event) => {
if (event.detail && event.detail.charts) {
PhoenixKitCharts.updateCharts(event.detail.charts);
}
});
// Initialize on page load
const init = () => {
// Small delay to ensure DOM is ready
setTimeout(() => {
if (document.getElementById('delivery-trend-chart') &&
document.getElementById('engagement-chart')) {
PhoenixKitCharts.initializeCharts();
}
}, 100);
};
// Handle different loading states
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Handle LiveView navigation
window.addEventListener('phx:page-loading-start', () => {
PhoenixKitCharts.cleanup();
});
window.addEventListener('phx:page-loading-stop', init);
// Make available for debugging
window.PhoenixKitCharts = PhoenixKitCharts;
})();
</script>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>