Packages

phoenix_kit

1.7.21
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
phoenix_kit lib modules emails web emails.ex
Raw

lib/modules/emails/web/emails.ex

defmodule PhoenixKit.Modules.Emails.Web.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
require Logger
alias PhoenixKit.Modules.Emails
alias PhoenixKit.Modules.Emails.TableColumns
alias PhoenixKit.Settings
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")
# Load table columns configuration
selected_columns = TableColumns.get_user_table_columns()
available_columns = TableColumns.get_available_columns()
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(:selected_columns, selected_columns)
|> assign(:available_columns, available_columns)
|> assign(:show_column_modal, false)
|> 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"))}
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
@impl true
def handle_event("show_column_modal", _params, socket) do
{:noreply, assign(socket, :show_column_modal, true)}
end
@impl true
def handle_event("hide_column_modal", _params, socket) do
{:noreply, assign(socket, :show_column_modal, false)}
end
@impl true
def handle_event("toggle_column", %{"field" => field}, socket) do
current_columns = socket.assigns.selected_columns
available_columns = socket.assigns.available_columns
# Check if column is required
column_meta = Enum.find(available_columns, fn col -> col.field == field end)
if column_meta && column_meta.required do
# Cannot toggle required columns
{:noreply, socket}
else
# Toggle column visibility
updated_columns =
if field in current_columns do
List.delete(current_columns, field)
else
# Add column at the end
current_columns ++ [field]
end
# Save to settings
case TableColumns.update_user_table_columns(updated_columns) do
{:ok, _} ->
{:noreply, assign(socket, :selected_columns, updated_columns)}
{:error, _} ->
{:noreply,
socket
|> put_flash(:error, "Failed to save column preferences")}
end
end
end
@impl true
def handle_event("reorder_columns", params, socket) do
current_columns = socket.assigns.selected_columns
reordered = TableColumns.reorder_columns(current_columns, params)
# Save to settings
case TableColumns.update_user_table_columns(reordered) do
{:ok, _} ->
{:noreply, assign(socket, :selected_columns, reordered)}
{:error, _} ->
{:noreply,
socket
|> put_flash(:error, "Failed to save column order")}
end
end
@impl true
def handle_event("reset_columns", _params, socket) do
default_columns = TableColumns.reset_columns()
# Save to settings
case TableColumns.update_user_table_columns(default_columns) do
{:ok, _} ->
{:noreply,
socket
|> assign(:selected_columns, default_columns)
|> put_flash(:info, "Columns reset to default")}
{:error, _} ->
{:noreply,
socket
|> put_flash(:error, "Failed to reset columns")}
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} ->
Logger.info("Test email sent successfully", %{
recipient: recipient,
module: __MODULE__
})
{: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} ->
Logger.error("Failed to send test email", %{
recipient: recipient,
reason: inspect(reason),
module: __MODULE__
})
{:noreply,
socket
|> assign(:test_email_sending, false)
|> put_flash(:error, "Failed to send test email: #{inspect(reason)}")}
end
rescue
error ->
Logger.error("Exception while sending test email", %{
recipient: recipient,
error_message: Exception.message(error),
error_type: error.__struct__,
stacktrace: Exception.format_stacktrace(__STACKTRACE__),
module: __MODULE__
})
{: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: "",
category: "",
source_module: "",
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"] || "",
category: params["category"] || "",
source_module: params["source_module"] || "",
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)
{:category, category}, acc when category != "" ->
Map.put(acc, :category, category)
{:source_module, source_module}, acc when source_module != "" ->
Map.put(acc, :source_module, source_module)
{: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,
"category" => assigns.filters.category,
"source_module" => assigns.filters.source_module,
"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
# 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