Current section

Files

Jump to
phoenix_kit_emails lib phoenix_kit modules emails web templates.html.heex
Raw

lib/phoenix_kit/modules/emails/web/templates.html.heex

<div class="container flex-col mx-auto px-4 py-6">
<%!-- 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_templates] || 0}
title={gettext("Total Templates")}
subtitle={gettext("All templates")}
>
<:icon>
<.icon name="hero-document-text" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={@stats[:active_templates] || 0}
title={gettext("Active")}
subtitle={gettext("Ready to use")}
>
<: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[:system_templates] || 0}
title={gettext("System")}
subtitle={gettext("Core templates")}
>
<:icon>
<.icon name="hero-cog-6-tooth" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={if @stats[:most_used], do: @stats[:most_used].usage_count, else: 0}
title={gettext("Most Used")}
subtitle={
if @stats[:most_used], do: @stats[:most_used].name, else: gettext("No usage yet")
}
>
<:icon>
<.icon name="hero-star" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
</div>
<%!-- Templates Table (filters/search/actions/sort live in the toolbar) --%>
<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">{gettext("Loading templates...")}</span>
</div>
<% else %>
<% category_options = [
{"", gettext("All Categories")},
{"system", gettext("System")},
{"transactional", gettext("Transactional")},
{"marketing", gettext("Marketing")},
{"newsletters", gettext("Newsletters")}
] %>
<% status_options = [
{"", gettext("All Statuses")},
{"active", gettext("Active")},
{"draft", gettext("Draft")},
{"archived", gettext("Archived")}
] %>
<% type_options = [
{"", gettext("All Types")},
{"true", gettext("System")},
{"false", gettext("Custom")}
] %>
<% category_label =
category_options
|> Enum.find(fn {v, _} -> v == @filters.category end)
|> then(&(&1 && elem(&1, 1))) %>
<% status_label =
status_options
|> Enum.find(fn {v, _} -> v == @filters.status end)
|> then(&(&1 && elem(&1, 1))) %>
<% type_label =
type_options
|> Enum.find(fn {v, _} -> v == @filters.is_system end)
|> then(&(&1 && elem(&1, 1))) %>
<.table_default
id="templates-table"
variant="zebra"
size="sm"
class="w-full"
toggleable={true}
items={@templates}
card_fields={
fn t ->
subject =
PhoenixKit.Modules.Emails.Template.get_translation(t.subject, @display_locale)
last_used =
if t.last_used_at,
do: UtilsDate.format_date_with_user_format(t.last_used_at),
else: gettext("Never")
[
%{label: gettext("Subject"), value: subject},
%{label: gettext("Category"), value: String.capitalize(t.category || "")},
%{label: gettext("Status"), value: t.status},
%{
label: gettext("Usage"),
value: "#{t.usage_count} (#{gettext("Last")}: #{last_used})"
}
]
end
}
>
<:card_header :let={template}>
<div class="space-y-0.5">
<div class="flex flex-wrap items-center gap-2">
<span class="font-medium text-sm">
{PhoenixKit.Modules.Emails.Template.get_translation(
template.display_name,
@display_locale
)}
</span>
<%= if template.is_system do %>
<span class="badge badge-info badge-xs h-auto">{gettext("System")}</span>
<% end %>
</div>
<div class="text-xs text-base-content/70">{template.name}</div>
</div>
</:card_header>
<:card_actions :let={template}>
<div class="flex flex-wrap gap-1">
<%!-- Edit --%>
<button
phx-click="edit_template"
phx-value-uuid={template.uuid}
class="btn btn-xs btn-outline btn-primary tooltip tooltip-bottom"
data-tip={gettext("Edit Template")}
>
<.icon name="hero-pencil" class="w-3 h-3" />
</button>
<%!-- Clone --%>
<button
phx-click="show_clone_modal"
phx-value-uuid={template.uuid}
class="btn btn-xs btn-outline tooltip tooltip-bottom"
data-tip={gettext("Clone Template")}
>
<.icon name="hero-document-duplicate" class="w-3 h-3" />
</button>
<%!-- Archive / Activate (non-system only) --%>
<%= unless template.is_system do %>
<%= if template.status == "active" do %>
<button
phx-click="archive_template"
phx-value-uuid={template.uuid}
class="btn btn-xs btn-outline tooltip tooltip-bottom"
data-tip={gettext("Archive Template")}
>
<.icon name="hero-archive-box" class="w-3 h-3" />
</button>
<% else %>
<button
phx-click="activate_template"
phx-value-uuid={template.uuid}
class="btn btn-xs btn-outline btn-success tooltip tooltip-bottom"
data-tip={gettext("Activate Template")}
>
<.icon name="hero-check-circle" class="w-3 h-3" />
</button>
<% end %>
<%!-- Delete --%>
<button
phx-click="request_delete"
phx-value-uuid={template.uuid}
phx-value-name={template.name}
class="btn btn-xs btn-outline text-error hover:btn-error tooltip tooltip-bottom"
data-tip={gettext("Delete Template")}
>
<.icon name="hero-trash" class="w-3 h-3" />
</button>
<% end %>
</div>
</:card_actions>
<:toolbar_title>
<div class="flex flex-wrap items-center gap-2">
<%!-- Category Filter (dropdown) --%>
<div class="dropdown">
<div
tabindex="0"
role="button"
class={["btn btn-sm gap-2", @filters.category != "" && "btn-active"]}
>
<.icon name="hero-tag" class="w-4 h-4" />
<span class={["sm:inline", @filters.category == "" && "hidden"]}>
{category_label || gettext("Category")}
</span>
<.icon
name="hero-chevron-down"
class="hidden sm:inline-block w-3.5 h-3.5 opacity-60"
/>
</div>
<ul
tabindex="0"
onclick="document.activeElement.blur()"
class="dropdown-content menu bg-base-100 rounded-box z-20 w-52 p-2 shadow border border-base-200"
>
<%= for {val, label} <- category_options do %>
<li>
<button
phx-click="set_filter"
phx-value-field="category"
phx-value-value={val}
class={@filters.category == val && "active"}
>
{label}
</button>
</li>
<% end %>
</ul>
</div>
<%!-- Status Filter (dropdown) --%>
<div class="dropdown">
<div
tabindex="0"
role="button"
class={["btn btn-sm gap-2", @filters.status != "" && "btn-active"]}
>
<.icon name="hero-signal" class="w-4 h-4" />
<span class={["sm:inline", @filters.status == "" && "hidden"]}>
{status_label || gettext("Status")}
</span>
<.icon
name="hero-chevron-down"
class="hidden sm:inline-block w-3.5 h-3.5 opacity-60"
/>
</div>
<ul
tabindex="0"
onclick="document.activeElement.blur()"
class="dropdown-content menu bg-base-100 rounded-box z-20 w-52 p-2 shadow border border-base-200"
>
<%= for {val, label} <- status_options do %>
<li>
<button
phx-click="set_filter"
phx-value-field="status"
phx-value-value={val}
class={@filters.status == val && "active"}
>
{label}
</button>
</li>
<% end %>
</ul>
</div>
<%!-- Type (System/Custom) Filter (dropdown) --%>
<div class="dropdown">
<div
tabindex="0"
role="button"
class={["btn btn-sm gap-2", @filters.is_system != "" && "btn-active"]}
>
<.icon name="hero-squares-2x2" class="w-4 h-4" />
<span class={["sm:inline", @filters.is_system == "" && "hidden"]}>
{type_label || gettext("Type")}
</span>
<.icon
name="hero-chevron-down"
class="hidden sm:inline-block w-3.5 h-3.5 opacity-60"
/>
</div>
<ul
tabindex="0"
onclick="document.activeElement.blur()"
class="dropdown-content menu bg-base-100 rounded-box z-20 w-52 p-2 shadow border border-base-200"
>
<%= for {val, label} <- type_options do %>
<li>
<button
phx-click="set_filter"
phx-value-field="is_system"
phx-value-value={val}
class={@filters.is_system == val && "active"}
>
{label}
</button>
</li>
<% end %>
</ul>
</div>
</div>
</:toolbar_title>
<:toolbar_actions>
<.form for={%{}} phx-change="filter">
<label class="input input-sm input-bordered flex items-center gap-2 w-44 sm:w-56">
<.icon name="hero-magnifying-glass" class="w-4 h-4 opacity-60" />
<input
type="text"
name="search[query]"
value={@filters.search}
class="grow"
placeholder={gettext("Search...")}
phx-debounce="300"
/>
<button
:if={@filters.search != ""}
type="button"
phx-click="clear_search"
class="btn btn-ghost btn-xs btn-circle"
aria-label={gettext("Clear search")}
>
<.icon name="hero-x-mark" class="w-4 h-4" />
</button>
</label>
</.form>
<.link
navigate={Routes.path("/admin/emails/templates/new")}
class="btn btn-primary btn-sm"
>
<.icon name="hero-plus" class="w-4 h-4" />
<span class="hidden sm:inline ml-1">{gettext("New Template")}</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">{gettext("Refresh")}</span>
</button>
</:toolbar_actions>
<.table_default_header>
<.table_default_row>
<.sort_header_cell
field={:name}
sort={%{by: @sort_by, dir: @sort_dir}}
event="toggle_sort"
>
{gettext("Template")}
</.sort_header_cell>
<.table_default_header_cell>{gettext("Subject")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Category")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Status")}</.table_default_header_cell>
<.sort_header_cell
field={:usage_count}
sort={%{by: @sort_by, dir: @sort_dir}}
event="toggle_sort"
>
{gettext("Usage")}
</.sort_header_cell>
<.table_default_header_cell>{gettext("Actions")}</.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<%= if @templates == [] do %>
<.table_default_row hover={false}>
<.table_default_cell colspan={6}>
<div class="text-center py-12">
<.icon
name="hero-document-text"
class="h-16 w-16 mx-auto text-base-content/40 mb-4"
/>
<h3 class="text-lg font-medium text-base-content mb-2">
{gettext("No templates found")}
</h3>
<p class="text-base-content/70">
{gettext("No templates found matching your criteria")}
</p>
</div>
</.table_default_cell>
</.table_default_row>
<% end %>
<%= for template <- @templates do %>
<.table_default_row
class="cursor-pointer"
phx-click="edit_template"
phx-value-uuid={template.uuid}
>
<%!-- Template Column --%>
<.table_default_cell class="max-w-xs">
<div class="space-y-1">
<div class="font-medium text-sm truncate">
{PhoenixKit.Modules.Emails.Template.get_translation(
template.display_name,
@display_locale
)}
</div>
<div class="text-xs text-base-content/70">{template.name}</div>
<%= if template.is_system do %>
<div class="badge badge-info badge-xs h-auto">{gettext("System")}</div>
<% end %>
</div>
</.table_default_cell>
<%!-- Subject Column --%>
<.table_default_cell class="max-w-xs">
<% subj =
PhoenixKit.Modules.Emails.Template.get_translation(
template.subject,
@display_locale
) %>
<div class="text-sm truncate" title={subj}>
{subj}
</div>
<% desc =
PhoenixKit.Modules.Emails.Template.get_translation(
template.description || %{},
@display_locale
) %>
<%= if desc != "" do %>
<div
class="text-xs text-base-content/70 truncate"
title={desc}
>
{desc}
</div>
<% end %>
</.table_default_cell>
<%!-- Category Column --%>
<.table_default_cell>
<%= if template.is_system do %>
<div class="badge badge-ghost badge-sm h-auto">
{String.capitalize(template.category)}
</div>
<% else %>
<.category_badge category={template.category} />
<% end %>
</.table_default_cell>
<%!-- Status Column --%>
<.table_default_cell>
<.status_badge status={template.status} />
</.table_default_cell>
<%!-- Usage Column --%>
<.table_default_cell>
<div class="space-y-1 text-xs">
<div class="font-medium">
{gettext("%{count} uses", count: template.usage_count)}
</div>
<%= if template.last_used_at do %>
<div class="text-base-content/70">
{gettext("Last:")} {UtilsDate.format_date_with_user_format(template.last_used_at)}
</div>
<% else %>
<div class="text-base-content/70">{gettext("Never used")}</div>
<% end %>
</div>
</.table_default_cell>
<%!-- Actions Column --%>
<.table_default_cell>
<%!-- stopPropagation so action buttons don't also trigger
the row's edit navigation. --%>
<div class="flex gap-1" onclick="event.stopPropagation()">
<%!-- Edit Button --%>
<button
phx-click="edit_template"
phx-value-uuid={template.uuid}
class="btn btn-xs btn-outline btn-primary tooltip tooltip-bottom"
data-tip={gettext("Edit Template")}
>
<.icon name="hero-pencil" class="w-3 h-3 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Edit")}</span>
</button>
<%!-- Clone Button --%>
<button
phx-click="show_clone_modal"
phx-value-uuid={template.uuid}
class="btn btn-xs btn-outline tooltip tooltip-bottom"
data-tip={gettext("Clone Template")}
>
<.icon name="hero-document-duplicate" class="w-3 h-3 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Clone")}</span>
</button>
<%!-- Archive/Activate Button (not for system templates) --%>
<%= unless template.is_system do %>
<%= if template.status == "active" do %>
<button
phx-click="archive_template"
phx-value-uuid={template.uuid}
class="btn btn-xs btn-outline tooltip tooltip-bottom"
data-tip={gettext("Archive Template")}
>
<.icon name="hero-archive-box" class="w-3 h-3 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Archive")}</span>
</button>
<% else %>
<button
phx-click="activate_template"
phx-value-uuid={template.uuid}
class="btn btn-xs btn-outline btn-success tooltip tooltip-bottom"
data-tip={gettext("Activate Template")}
>
<.icon name="hero-check-circle" class="w-3 h-3 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Activate")}</span>
</button>
<% end %>
<%!-- Delete Button --%>
<button
phx-click="request_delete"
phx-value-uuid={template.uuid}
phx-value-name={template.name}
class="btn btn-xs btn-outline text-error hover:btn-error tooltip tooltip-bottom"
data-tip={gettext("Delete Template")}
>
<.icon name="hero-trash" class="w-3 h-3 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Delete")}</span>
</button>
<% end %>
</div>
</.table_default_cell>
</.table_default_row>
<% end %>
</.table_default_body>
</.table_default>
<%!-- Pagination --%>
<%= if @total_count > @per_page do %>
<.pagination
current_page={@page}
total_pages={@total_pages}
base_path={Routes.path("/admin/emails/templates")}
params={
%{
"search" => @filters.search,
"category" => @filters.category,
"status" => @filters.status,
"is_system" => @filters.is_system,
"sort_by" => to_string(@sort_by),
"sort_dir" => to_string(@sort_dir),
"per_page" => @per_page
}
}
/>
<% end %>
<% end %>
</div>
<%!-- Clone Template Modal --%>
<div
:if={@show_clone_modal}
class="modal modal-open"
phx-click-away="hide_clone_modal"
>
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">{gettext("Clone Template")}</h3>
<%= if @clone_template do %>
<p class="text-sm text-base-content/70 mb-4">
{gettext("Creating a copy of")} "<strong>{PhoenixKit.Modules.Emails.Template.get_translation(@clone_template.display_name, @display_locale)}</strong>"
</p>
<% end %>
<.form
for={%{}}
phx-submit="clone_template"
phx-change="validate_clone"
class="space-y-4"
>
<div class="form-control">
<label class="label">
<span class="label-text">{gettext("New Template Name")}</span>
</label>
<input
type="text"
name="clone[name]"
value={@clone_form[:name] || ""}
placeholder="welcome_email_copy"
class={[
"input input-bordered w-full",
@clone_form[:errors][:name] && "input-error"
]}
required
/>
<%= if @clone_form[:errors][:name] do %>
<label class="label">
<span class="label-text-alt text-error">
{@clone_form[:errors][:name]}
</span>
</label>
<% end %>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{gettext("New Display Name")}</span>
</label>
<input
type="text"
name="clone[display_name]"
value={@clone_form[:display_name] || ""}
placeholder="Welcome Email (Copy)"
class={[
"input input-bordered w-full",
@clone_form[:errors][:display_name] && "input-error"
]}
required
/>
<%= if @clone_form[:errors][:display_name] do %>
<label class="label">
<span class="label-text-alt text-error">
{@clone_form[:errors][:display_name]}
</span>
</label>
<% end %>
</div>
<div class="modal-action">
<button
type="button"
phx-click="hide_clone_modal"
class="btn btn-ghost"
>
{gettext("Cancel")}
</button>
<button type="submit" class="btn btn-primary">
{gettext("Clone Template")}
</button>
</div>
</.form>
</div>
</div>
<%!-- Confirmation Modal --%>
<%= if assigns[:confirmation_modal] && @confirmation_modal.show do %>
<div class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg">{@confirmation_modal.title}</h3>
<p class="py-4">{@confirmation_modal.message}</p>
<div class="modal-action">
<button class="btn btn-ghost" phx-click="cancel_confirmation">
{gettext("Cancel")}
</button>
<button
class="btn btn-error"
phx-click="confirm_action"
phx-value-action={@confirmation_modal.action}
phx-value-uuid={@confirmation_modal.uuid}
>
{@confirmation_modal.button_text}
</button>
</div>
</div>
</div>
<% end %>
</div>