Packages
phoenix_kit
1.3.0
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/emails.ex
defmodule PhoenixKitWeb.Live.Modules.Emails.Emails do
@moduledoc """
LiveView for displaying and managing emails in PhoenixKit admin panel.
Provides comprehensive email interface with filtering, searching,
and detailed analytics for sent emails.
## Features
- **Real-time Log List**: Live updates of emails
- **Advanced Filtering**: By status, date range, recipient, campaign, template
- **Search Functionality**: Search across recipients, subjects, campaigns
- **Pagination**: Handle large volumes of emails
- **Export**: CSV export functionality
- **Quick Actions**: Resend, view details, mark as reviewed
- **Statistics Summary**: Key metrics at the top of the page
## Route
This LiveView is mounted at `{prefix}/admin/emails` and requires
appropriate admin permissions.
Note: `{prefix}` is your configured PhoenixKit URL prefix (default: `/phoenix_kit`).
## Usage
# In your Phoenix router
live "/admin/emails", PhoenixKitWeb.Live.Modules.Emails.Emails, :index
## Permissions
Access is restricted to users with admin or owner roles in PhoenixKit.
"""
use PhoenixKitWeb, :live_view
alias PhoenixKit.Emails
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Date, as: UtilsDate
alias PhoenixKit.Utils.Routes
import PhoenixKitWeb.Components.Core.Icons, only: [icon_arrow_left: 1]
@default_per_page 25
@max_per_page 100
## --- Lifecycle Callbacks ---
@impl true
def mount(_params, _session, socket) do
# Check if email tracking is enabled
if Emails.enabled?() do
# Get project title from settings
project_title = Settings.get_setting("project_title", "PhoenixKit")
socket =
socket
|> assign(:page_title, "Emails")
|> assign(:project_title, project_title)
|> assign(:logs, [])
|> assign(:total_count, 0)
|> assign(:stats, %{})
|> assign(:loading, true)
|> assign(:show_test_email_modal, false)
|> assign(:test_email_sending, false)
|> assign(:test_email_form, %{recipient: "", errors: %{}})
|> assign_filter_defaults()
|> assign_pagination_defaults()
{:ok, socket}
else
{:ok,
socket
|> put_flash(:error, "Email management is not enabled")
|> push_navigate(to: Routes.path("/admin/dashboard"))}
end
end
@impl true
def handle_params(params, _url, socket) do
socket =
socket
|> apply_params(params)
|> load_email_logs()
|> load_stats()
{:noreply, socket}
end
## --- Event Handlers ---
@impl true
def handle_event("filter", params, socket) do
# Handle both search and filter parameters
combined_params = %{}
# Extract search parameters
combined_params =
case Map.get(params, "search") do
%{"query" => query} -> Map.put(combined_params, "search", String.trim(query || ""))
_ -> combined_params
end
# Extract filter parameters
combined_params =
case Map.get(params, "filter") do
filter_params when is_map(filter_params) -> Map.merge(combined_params, filter_params)
_ -> combined_params
end
# Reset to first page when filtering
combined_params = Map.put(combined_params, "page", "1")
# Build new URL parameters
new_params = build_url_params(socket.assigns, combined_params)
{:noreply,
socket
|> push_patch(to: Routes.path("/admin/emails?#{new_params}"))}
end
@impl true
def handle_event("clear_filters", _params, socket) do
{:noreply,
socket
|> push_patch(to: Routes.path("/admin/emails"))}
end
@impl true
def handle_event("view_details", %{"id" => log_id}, socket) do
{:noreply,
socket
|> push_navigate(to: Routes.path("/admin/emails/email/#{log_id}"))}
end
@impl true
def handle_event("refresh", _params, socket) do
{:noreply,
socket
|> assign(:loading, true)
|> load_email_logs()
|> load_stats()}
end
@impl true
def handle_event("show_test_email_modal", _params, socket) do
{:noreply,
socket
|> assign(:show_test_email_modal, true)
|> assign(:test_email_form, %{recipient: "", errors: %{}})}
end
@impl true
def handle_event("hide_test_email_modal", _params, socket) do
{:noreply,
socket
|> assign(:show_test_email_modal, false)
|> assign(:test_email_sending, false)
|> assign(:test_email_form, %{recipient: "", errors: %{}})}
end
@impl true
def handle_event("validate_test_email", %{"test_email" => %{"recipient" => recipient}}, socket) do
errors = validate_test_email_form(recipient)
form = %{
recipient: recipient,
errors: errors
}
{:noreply, assign(socket, :test_email_form, form)}
end
@impl true
def handle_event("send_test_email", %{"test_email" => %{"recipient" => recipient}}, socket) do
errors = validate_test_email_form(recipient)
if map_size(errors) == 0 do
# Start sending process
socket = assign(socket, :test_email_sending, true)
# Send the test email asynchronously
send(self(), {:send_test_email, String.trim(recipient)})
{:noreply, socket}
else
# Show validation errors
form = %{
recipient: recipient,
errors: errors
}
{:noreply, assign(socket, :test_email_form, form)}
end
end
## --- Info Handlers ---
@impl true
def handle_info({:send_test_email, recipient}, socket) do
case PhoenixKit.Mailer.send_test_tracking_email(recipient) do
{:ok, _email} ->
{:noreply,
socket
|> assign(:test_email_sending, false)
|> assign(:show_test_email_modal, false)
|> put_flash(
:info,
"Test email sent successfully to #{recipient}! Check your emails to see the management data."
)
|> load_email_logs()
|> load_stats()}
{:error, reason} ->
{:noreply,
socket
|> assign(:test_email_sending, false)
|> put_flash(:error, "Failed to send test email: #{inspect(reason)}")}
end
rescue
error ->
{:noreply,
socket
|> assign(:test_email_sending, false)
|> put_flash(:error, "Error sending test email: #{Exception.message(error)}")}
end
## --- Private Helper Functions ---
# Apply default filter values
defp assign_filter_defaults(socket) do
filters = %{
search: "",
status: "",
message_tag: "",
campaign_id: "",
from_date: "",
to_date: ""
}
assign(socket, :filters, filters)
end
# Apply default pagination values
defp assign_pagination_defaults(socket) do
socket
|> assign(:page, 1)
|> assign(:per_page, @default_per_page)
|> assign(:total_pages, 0)
end
# Apply URL parameters to socket assigns
defp apply_params(socket, params) do
filters = %{
search: params["search"] || "",
status: params["status"] || "",
message_tag: params["message_tag"] || "",
campaign_id: params["campaign_id"] || "",
from_date: params["from_date"] || "",
to_date: params["to_date"] || ""
}
page = String.to_integer(params["page"] || "1")
per_page = min(String.to_integer(params["per_page"] || "#{@default_per_page}"), @max_per_page)
socket
|> assign(:filters, filters)
|> assign(:page, page)
|> assign(:per_page, per_page)
end
# Load emails based on current filters and pagination
defp load_email_logs(socket) do
%{filters: filters, page: page, per_page: per_page} = socket.assigns
# Build filters for EmailLog query
query_filters = build_query_filters(filters, page, per_page)
logs = Emails.list_logs(query_filters)
# Get total count for pagination (efficient count without loading all records)
total_count =
Emails.count_logs(build_query_filters(filters, 1, 1) |> Map.drop([:limit, :offset]))
total_pages = ceil(total_count / per_page)
socket
|> assign(:logs, logs)
|> assign(:total_count, total_count)
|> assign(:total_pages, total_pages)
|> assign(:loading, false)
end
# Load summary statistics
defp load_stats(socket) do
stats = Emails.get_system_stats(:last_30_days)
assign(socket, :stats, stats)
end
# Build query filters from form filters
defp build_query_filters(filters, page, per_page) do
query_filters = %{
limit: per_page,
offset: (page - 1) * per_page
}
# Add non-empty filters
query_filters =
filters
|> Enum.reduce(query_filters, fn
{:search, search}, acc when search != "" ->
# Search in recipient, subject, and campaign fields
Map.put(acc, :search, search)
{:status, status}, acc when status != "" ->
Map.put(acc, :status, status)
{:message_tag, message_tag}, acc when message_tag != "" ->
Map.put(acc, :message_tag, message_tag)
{:campaign_id, campaign_id}, acc when campaign_id != "" ->
Map.put(acc, :campaign_id, campaign_id)
{:from_date, from_date}, acc when from_date != "" ->
case Date.from_iso8601(from_date) do
{:ok, date} -> Map.put(acc, :from_date, DateTime.new!(date, ~T[00:00:00]))
_ -> acc
end
{:to_date, to_date}, acc when to_date != "" ->
case Date.from_iso8601(to_date) do
{:ok, date} -> Map.put(acc, :to_date, DateTime.new!(date, ~T[23:59:59]))
_ -> acc
end
_, acc ->
acc
end)
query_filters
end
# Build URL parameters from current state
defp build_url_params(assigns, additional_params) do
base_params = %{
"search" => assigns.filters.search,
"status" => assigns.filters.status,
"message_tag" => assigns.filters.message_tag,
"campaign_id" => assigns.filters.campaign_id,
"from_date" => assigns.filters.from_date,
"to_date" => assigns.filters.to_date,
"page" => assigns.page,
"per_page" => assigns.per_page
}
Map.merge(base_params, additional_params)
|> Enum.reject(fn {_key, value} -> value == "" or is_nil(value) end)
|> Map.new()
|> URI.encode_query()
end
# Build export URL with current filters
defp build_export_url(filters) do
# Convert filters to query parameters
params =
filters
|> Enum.reject(fn {_key, value} -> value == "" or is_nil(value) end)
|> Enum.into(%{})
|> URI.encode_query()
base_url = Routes.path("/admin/emails/export")
if params != "" do
"#{base_url}?#{params}"
else
base_url
end
end
# Helper functions for template
defp status_badge_class(status) do
case status do
"sent" -> "badge badge-info badge-sm"
"delivered" -> "badge badge-success badge-sm"
"bounced" -> "badge badge-error badge-sm"
"opened" -> "badge badge-primary badge-sm"
"clicked" -> "badge badge-secondary badge-sm"
"failed" -> "badge badge-error badge-sm"
_ -> "badge badge-ghost badge-sm"
end
end
defp pagination_pages(current_page, total_pages) do
start_page = max(1, current_page - 2)
end_page = min(total_pages, current_page + 2)
start_page..end_page
end
defp pagination_class(page_num, current_page) do
if page_num == current_page do
"btn btn-sm btn-active"
else
"btn btn-sm"
end
end
defp build_page_url(page, assigns) do
params = build_url_params(assigns, %{"page" => page})
Routes.path("/admin/emails?#{params}")
end
# Extract email_type from message_tags
defp get_message_tag(message_tags) when is_map(message_tags) do
Map.get(message_tags, "email_type")
end
defp get_message_tag(_), do: nil
# Validate test email form
defp validate_test_email_form(recipient) do
errors = %{}
# Validate recipient email
errors =
case String.trim(recipient || "") do
"" ->
Map.put(errors, :recipient, "Email address is required")
email ->
if Regex.match?(~r/^[^\s@]+@[^\s@]+\.[^\s@]+$/, email) do
errors
else
Map.put(errors, :recipient, "Please enter a valid email address")
end
end
errors
end
end