Packages
phoenix_kit
1.4.4
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.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title="Emails"
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 (Left aligned) --%>
<.link
navigate={Routes.path("/admin/dashboard")}
class="btn btn-outline btn-primary btn-sm absolute left-0 top-0 -mb-12"
>
<.icon_arrow_left /> Back to Dashboard
</.link>
<%!-- Title Section --%>
<div class="text-center">
<h1 class="text-4xl font-bold text-base-content mb-3">Emails</h1>
<p class="text-lg text-base-content">Monitor and track all outgoing emails</p>
</div>
</header>
<%!-- Statistics Summary --%>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={@stats[:total_sent] || 0}
title="Total Sent"
subtitle="Last 30 days"
>
<:icon>
<.icon name="hero-envelope" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={@stats[:delivered] || 0}
title="Delivered"
subtitle={"#{@stats[:delivery_rate] || 0}% rate"}
>
<:icon>
<.icon name="hero-check-circle" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={@stats[:bounced] || 0}
title="Bounced"
subtitle={"#{@stats[:bounce_rate] || 0}% rate"}
>
<:icon>
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={@stats[:opened] || 0}
title="Opened"
subtitle={"#{@stats[:open_rate] || 0}% rate"}
>
<:icon>
<.icon name="hero-envelope-open" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
</div>
<%!-- Filters & Search --%>
<div class="bg-base-200 rounded-lg p-6 mb-6">
<.form for={%{}} phx-change="filter" class="space-y-4">
<%!-- Search Bar --%>
<div class="flex gap-2 items-center">
<input
type="text"
name="search[query]"
value={@filters.search}
placeholder="Search by recipient, subject, or campaign..."
class="input input-bordered flex-1"
phx-debounce="300"
/>
<button type="button" phx-click="clear_filters" class="btn btn-ghost btn-sm">
Clear
</button>
</div>
<%!-- Filter Row --%>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<%!-- Status Filter --%>
<div class="form-control">
<label class="label">
<span class="label-text">Status</span>
</label>
<select name="filter[status]" class="select select-bordered">
<option value="">All Statuses</option>
<option value="sent" selected={@filters.status == "sent"}>Sent</option>
<option value="delivered" selected={@filters.status == "delivered"}>
Delivered
</option>
<option value="bounced" selected={@filters.status == "bounced"}>Bounced</option>
<option value="opened" selected={@filters.status == "opened"}>Opened</option>
<option value="clicked" selected={@filters.status == "clicked"}>Clicked</option>
<option value="failed" selected={@filters.status == "failed"}>Failed</option>
</select>
</div>
<%!-- Message Tags Filter --%>
<div class="form-control">
<label class="label">
<span class="label-text">Message Type</span>
</label>
<select name="filter[message_tag]" class="select select-bordered">
<option value="">All Types</option>
<option value="authentication" selected={@filters.message_tag == "authentication"}>
Authentication
</option>
<option value="registration" selected={@filters.message_tag == "registration"}>
Registration
</option>
<option value="marketing" selected={@filters.message_tag == "marketing"}>
Marketing
</option>
<option value="notification" selected={@filters.message_tag == "notification"}>
Notification
</option>
<option value="transactional" selected={@filters.message_tag == "transactional"}>
Transactional
</option>
</select>
</div>
<%!-- Date Range --%>
<div class="form-control">
<label class="label">
<span class="label-text">From Date</span>
</label>
<input
type="date"
name="filter[from_date]"
value={@filters.from_date}
class="input input-bordered"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">To Date</span>
</label>
<input
type="date"
name="filter[to_date]"
value={@filters.to_date}
class="input input-bordered"
/>
</div>
</div>
<%!-- Quick Actions Row --%>
<div class="w-full">
<label class="label">
<span class="label-text">Quick Actions</span>
</label>
<div class="flex flex-wrap gap-2">
<button
type="button"
phx-click="show_column_modal"
class="btn btn-info btn-sm"
title="Customize columns"
>
<.icon name="hero-view-columns" class="h-5 w-5" />
<span class="hidden sm:inline">Columns</span>
</button>
<.link
navigate={Routes.path("/admin/modules/emails/templates")}
class="btn btn-outline btn-secondary btn-sm"
>
<.icon name="hero-document-text" class="w-4 h-4" />
<span class="hidden sm:inline ml-1">Templates</span>
</.link>
<button
type="button"
phx-click="show_test_email_modal"
class="btn btn-primary btn-sm"
>
<.icon name="hero-envelope" class="w-4 h-4" />
<span class="hidden sm:inline ml-1">Send Test</span>
</button>
<.link
href={build_export_url(@filters)}
target="_blank"
class="btn btn-outline btn-sm"
>
<.icon name="hero-arrow-down-tray" class="w-4 h-4" />
<span class="hidden sm:inline ml-1">Export CSV</span>
</.link>
<button type="button" phx-click="refresh" class="btn btn-outline btn-sm">
<.icon name="hero-arrow-path" class="w-4 h-4" />
<span class="hidden sm:inline ml-1">Refresh</span>
</button>
</div>
</div>
</.form>
</div>
<%!-- Emails Table --%>
<div class="bg-base-100">
<%= if @loading do %>
<div class="flex justify-center items-center h-32">
<span class="loading loading-spinner loading-md"></span>
<span class="ml-2">Loading emails...</span>
</div>
<% else %>
<%= if length(@logs) > 0 do %>
<.table_default variant="zebra" class="w-full">
<.table_default_header>
<.table_default_row>
<%= for field <- @selected_columns do %>
<% column = Enum.find(@available_columns, fn col -> col.field == field end) %>
<%= if column do %>
<.table_default_header_cell>
<%= if field == "activity" do %>
<div class="flex items-center gap-1">
<span>{column.label}</span>
<div
class="tooltip tooltip-right"
data-tip="🔵 Sent (date+time) | 🟢 Delivered | 🟡 Opened | 🟣 Clicked | 🔴 Bounced/Failed | ⚠️ Complaint"
>
<.icon
name="hero-information-circle"
class="w-4 h-4 text-base-content/50 hover:text-base-content cursor-help"
/>
</div>
</div>
<% else %>
{column.label}
<% end %>
</.table_default_header_cell>
<% end %>
<% end %>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<%= for log <- @logs do %>
<.table_default_row>
<%= for field <- @selected_columns do %>
<.table_default_cell>
<%= case field do %>
<% "to" -> %>
<div class="space-y-1">
<div class="font-medium text-sm">{log.to}</div>
<%= if log.user_id do %>
<div class="badge badge-ghost badge-xs">User #{log.user_id}</div>
<% end %>
</div>
<% "subject" -> %>
<div class="text-sm truncate" title={log.subject}>
{log.subject || "(no subject)"}
</div>
<% "status" -> %>
<div class="space-y-1">
<div class={status_badge_class(log.status)}>
{log.status}
</div>
<%= if log.error_message do %>
<div class="text-xs text-error truncate" title={log.error_message}>
Error
</div>
<% end %>
</div>
<% "activity" -> %>
<div class="flex flex-wrap gap-1">
<%!-- Sent badge (blue) - always shows date + time --%>
<%= if log.sent_at do %>
<div class="badge badge-info badge-xs">
{format_sent_badge(log.sent_at)}
</div>
<% end %>
<%!-- Activity badges with smart date display --%>
<%!-- Each badge shows date only when different from previous event --%>
<%= for {badge_class, formatted_text, _event_type} <- get_activity_badges(log) do %>
<div class={"badge #{badge_class} badge-xs"}>
{formatted_text}
</div>
<% end %>
</div>
<% "details" -> %>
<div class="space-y-1">
<%= if log.campaign_id do %>
<div class="badge badge-primary badge-xs">{log.campaign_id}</div>
<% end %>
<%= if get_message_tag(log.message_tags) do %>
<div class="badge badge-secondary badge-xs">
{get_message_tag(log.message_tags)}
</div>
<% end %>
<%= if log.template_name do %>
<div class="badge badge-outline badge-xs">{log.template_name}</div>
<% end %>
<%= if !log.campaign_id && !get_message_tag(log.message_tags) && !log.template_name do %>
<span class="text-xs text-base-content/50">—</span>
<% end %>
</div>
<% "actions" -> %>
<button
phx-click="view_details"
phx-value-id={log.id}
class="btn btn-xs btn-outline btn-primary"
>
<.icon name="hero-eye" class="w-3 h-3 mr-1" /> View
</button>
<% _ -> %>
<span class="text-xs text-base-content/50">—</span>
<% end %>
</.table_default_cell>
<% end %>
</.table_default_row>
<% end %>
</.table_default_body>
</.table_default>
<% else %>
<%!-- Empty state --%>
<div class="text-center py-12">
<.icon name="hero-envelope" class="h-16 w-16 mx-auto text-base-content/40 mb-4" />
<h3 class="text-lg font-medium text-base-content mb-2">
No emails found
</h3>
<p class="text-base-content/70 mb-4">
No emails found matching your criteria
</p>
</div>
<% end %>
<%!-- Pagination --%>
<%= if @total_count > @per_page do %>
<div class="flex justify-center p-4 border-t border-base-300">
<div class="join">
<%= if @page > 1 do %>
<.link patch={build_page_url(@page - 1, assigns)} class="join-item btn btn-sm">
« Prev
</.link>
<% end %>
<%= for page_num <- pagination_pages(@page, @total_pages) do %>
<.link
patch={build_page_url(page_num, assigns)}
class={[
"join-item btn btn-sm",
page_num == @page && "btn-active"
]}
>
{page_num}
</.link>
<% end %>
<%= if @page < @total_pages do %>
<.link patch={build_page_url(@page + 1, assigns)} class="join-item btn btn-sm">
Next »
</.link>
<% end %>
</div>
</div>
<% end %>
<% end %>
</div>
<%!-- Test Email Modal --%>
<div
:if={@show_test_email_modal}
class="modal modal-open"
phx-click-away="hide_test_email_modal"
>
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Send Test Email</h3>
<p class="text-sm text-base-content/70 mb-4">
Send a test email to verify that email is working correctly.
The test email will include test links.
</p>
<.form
for={%{}}
phx-submit="send_test_email"
phx-change="validate_test_email"
class="space-y-4"
>
<div class="form-control">
<label class="label">
<span class="label-text">Recipient Email Address</span>
</label>
<input
type="email"
name="test_email[recipient]"
value={@test_email_form[:recipient] || ""}
placeholder="admin@example.com"
class={[
"input input-bordered w-full",
@test_email_form[:errors][:recipient] && "input-error"
]}
required
/>
<%= if @test_email_form[:errors][:recipient] do %>
<label class="label">
<span class="label-text-alt text-error">
{@test_email_form[:errors][:recipient]}
</span>
</label>
<% end %>
</div>
<div class="alert alert-info">
<.icon name="hero-information-circle" class="w-5 h-5" />
<div class="text-sm">
<div class="font-semibold">This test email will:</div>
<ul class="mt-1 list-disc list-inside">
<li>Verify AWS SES configuration (if enabled)</li>
<li>Test email delivery management</li>
<li>Include trackable links for click testing</li>
<li>Appear in your emails with "TEST" campaign</li>
</ul>
</div>
</div>
<div class="modal-action">
<button
type="button"
phx-click="hide_test_email_modal"
class="btn btn-ghost"
>
Cancel
</button>
<button
type="submit"
class={[
"btn btn-primary",
@test_email_sending && "loading"
]}
disabled={@test_email_sending}
>
<%= if @test_email_sending do %>
Sending...
<% else %>
Send Test Email
<% end %>
</button>
</div>
</.form>
</div>
</div>
<%!-- Column Selection Modal --%>
<%= if @show_column_modal do %>
<div class="modal modal-open" phx-click-away="hide_column_modal">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-xl mb-4">Customize Table Columns</h3>
<p class="text-base-content/70 mb-6">
Select which columns to display in the emails table. Required columns (Email, Subject, Status, Actions) cannot be hidden.
</p>
<div class="space-y-2 max-h-96 overflow-y-auto">
<%= for column <- @available_columns do %>
<div class="flex items-center p-3 rounded-lg hover:bg-base-200 transition-colors">
<input
type="checkbox"
class="checkbox checkbox-primary"
id={"column-#{column.field}"}
checked={column.field in @selected_columns}
disabled={column.required}
phx-click="toggle_column"
phx-value-field={column.field}
/>
<label for={"column-#{column.field}"} class="ml-3 flex-1 cursor-pointer">
<div class="font-medium text-sm text-base-content">
{column.label}
</div>
<%= if column.required do %>
<div class="text-xs text-base-content/50">
Required column
</div>
<% end %>
</label>
<%= if column.required do %>
<div class="badge badge-sm badge-ghost">Required</div>
<% end %>
</div>
<% end %>
</div>
<div class="modal-action">
<button
type="button"
phx-click="reset_columns"
class="btn btn-outline"
>
Reset to Default
</button>
<button
type="button"
phx-click="hide_column_modal"
class="btn btn-primary"
>
Close
</button>
</div>
</div>
</div>
<% end %>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>