Packages

phoenix_kit

1.7.78
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
phoenix_kit lib modules customer_service web list.html.heex
Raw

lib/modules/customer_service/web/list.html.heex

<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title="Support Tickets"
current_path={@url_path}
project_title={@project_title}
>
<div class="container flex-col mx-auto px-4 py-6">
<.admin_page_header
back={PhoenixKit.Utils.Routes.path("/admin")}
title="Support Tickets"
subtitle="Manage customer support requests"
>
<:actions>
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/customer-service/tickets/new")}
class="btn btn-primary"
>
<.icon name="hero-plus" class="h-5 w-5" /> New Ticket
</.link>
</:actions>
</.admin_page_header>
<%!-- Stats Cards --%>
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
color="primary"
value={@stats.total}
title="Total"
subtitle="All support requests"
>
<:icon>
<.icon name="hero-list-bullet" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
color="info"
value={@stats.open}
title="Open"
subtitle="Awaiting response"
>
<:icon>
<.icon name="hero-inbox" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
color="warning"
value={@stats.in_progress}
title="In Progress"
subtitle="Being worked on"
>
<:icon>
<.icon name="hero-clock" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
color="success"
value={@stats.resolved}
title="Resolved"
subtitle="Solution provided"
>
<: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"
color="neutral"
value={@stats.closed}
title="Closed"
subtitle="No further action"
>
<:icon>
<.icon name="hero-archive-box" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
</div>
<%!-- Filters --%>
<div class="bg-base-200 rounded-lg p-4 mb-6">
<form phx-change="filter" phx-submit="filter" class="flex flex-col md:flex-row gap-4">
<div class="flex-1">
<input
type="text"
name="search[query]"
value={@search_query}
placeholder="Search tickets..."
class="input input-bordered w-full"
phx-debounce="300"
/>
</div>
<div class="w-full md:w-48">
<select name="filters[status]" class="select select-bordered w-full">
<option value="">All Statuses</option>
<option value="open" selected={@status_filter == "open"}>Open</option>
<option value="in_progress" selected={@status_filter == "in_progress"}>
In Progress
</option>
<option value="resolved" selected={@status_filter == "resolved"}>Resolved</option>
<option value="closed" selected={@status_filter == "closed"}>Closed</option>
</select>
</div>
<div class="w-full md:w-48">
<select name="filters[assigned_to]" class="select select-bordered w-full">
<option value="">All Assigned</option>
<option value="unassigned" selected={@assigned_to_filter == "unassigned"}>
Unassigned
</option>
</select>
</div>
<button type="button" phx-click="clear_filters" class="btn btn-ghost">
Clear
</button>
</form>
</div>
<%!-- Loading --%>
<%= if @loading do %>
<div class="flex justify-center items-center py-12">
<span class="loading loading-spinner loading-lg"></span>
</div>
<% else %>
<%!-- Empty State --%>
<%= if Enum.empty?(@tickets) do %>
<div class="text-center py-12">
<.icon name="hero-ticket" class="h-16 w-16 mx-auto text-base-content/30 mb-4" />
<h3 class="text-lg font-medium mb-2">No tickets found</h3>
<p class="text-base-content/70">
<%= if @status_filter || @search_query do %>
Try adjusting your filters or search query.
<% else %>
Create your first ticket to get started.
<% end %>
</p>
</div>
<% else %>
<%!-- Desktop Table --%>
<div class="hidden lg:block bg-base-100 mb-6">
<.table_default variant="zebra" class="w-full table-fixed">
<.table_default_header>
<.table_default_row>
<.table_default_header_cell class="w-[38%] lg:w-[28%]">
Ticket
</.table_default_header_cell>
<.table_default_header_cell class="w-[26%] lg:w-[20%]">
Customer
</.table_default_header_cell>
<.table_default_header_cell class="hidden lg:table-cell lg:w-[17%]">
Assigned To
</.table_default_header_cell>
<.table_default_header_cell class="w-[14%]">
Status
</.table_default_header_cell>
<.table_default_header_cell class="w-[12%]">
Created
</.table_default_header_cell>
<.table_default_header_cell class="w-[10%] lg:w-[9%]">
Actions
</.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<%= for ticket <- @tickets do %>
<.table_default_row>
<.table_default_cell>
<div class="font-medium truncate">{ticket.title}</div>
<div class="text-xs text-base-content/60 line-clamp-2">
{ticket.description}
</div>
</.table_default_cell>
<.table_default_cell>
<%= if ticket.user do %>
<div class="flex items-center gap-2 min-w-0">
<.user_avatar user={ticket.user} size="sm" />
<span class="text-sm truncate min-w-0 flex-1">{ticket.user.email}</span>
</div>
<% else %>
<span class="text-base-content/50">Unknown</span>
<% end %>
</.table_default_cell>
<.table_default_cell class="hidden lg:table-cell">
<%= if ticket.assigned_to do %>
<span class="text-sm truncate block">{ticket.assigned_to.email}</span>
<% else %>
<span class="badge badge-ghost">Unassigned</span>
<% end %>
</.table_default_cell>
<.table_default_cell>
<%= case ticket.status do %>
<% "open" -> %>
<span class="badge badge-info whitespace-nowrap">Open</span>
<% "in_progress" -> %>
<span class="badge badge-warning whitespace-nowrap">In Progress</span>
<% "resolved" -> %>
<span class="badge badge-success whitespace-nowrap">Resolved</span>
<% "closed" -> %>
<span class="badge whitespace-nowrap">Closed</span>
<% end %>
</.table_default_cell>
<.table_default_cell class="text-sm text-base-content/70">
{Calendar.strftime(ticket.inserted_at, "%b %d, %Y")}
</.table_default_cell>
<.table_default_cell>
<div class="flex items-center gap-1">
<.link
navigate={
PhoenixKit.Utils.Routes.path(
"/admin/customer-service/tickets/#{ticket.uuid}"
)
}
class="btn btn-xs btn-outline btn-info tooltip tooltip-bottom"
data-tip={gettext("View details")}
>
<.icon name="hero-eye" class="w-4 h-4" />
</.link>
<.link
navigate={
PhoenixKit.Utils.Routes.path(
"/admin/customer-service/tickets/#{ticket.uuid}/edit"
)
}
class="btn btn-xs btn-outline tooltip tooltip-bottom"
data-tip={gettext("Edit ticket")}
>
<.icon name="hero-pencil" class="w-4 h-4" />
</.link>
</div>
</.table_default_cell>
</.table_default_row>
<% end %>
</.table_default_body>
</.table_default>
</div>
<%!-- Mobile Cards --%>
<div class="lg:hidden mb-6">
<div class="space-y-3">
<%= for ticket <- @tickets do %>
<div class="card card-border card-sm bg-base-100">
<div class="card-body gap-3">
<%!-- Header: title + status --%>
<div class="flex items-start justify-between gap-2">
<div class="font-medium text-sm leading-snug">{ticket.title}</div>
<%= case ticket.status do %>
<% "open" -> %>
<span class="badge badge-info whitespace-nowrap shrink-0">Open</span>
<% "in_progress" -> %>
<span class="badge badge-warning whitespace-nowrap shrink-0">
In Progress
</span>
<% "resolved" -> %>
<span class="badge badge-success whitespace-nowrap shrink-0">
Resolved
</span>
<% "closed" -> %>
<span class="badge whitespace-nowrap shrink-0">Closed</span>
<% end %>
</div>
<%!-- Body: key/value grid --%>
<div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
<div class="text-base-content/60">Customer</div>
<div class="truncate">
<%= if ticket.user do %>
{ticket.user.email}
<% else %>
<span class="text-base-content/40">Unknown</span>
<% end %>
</div>
<div class="text-base-content/60">Assigned</div>
<div>
<%= if ticket.assigned_to do %>
<span class="truncate">{ticket.assigned_to.email}</span>
<% else %>
<span class="text-base-content/40">Unassigned</span>
<% end %>
</div>
<div class="text-base-content/60">Created</div>
<div>{Calendar.strftime(ticket.inserted_at, "%b %d, %Y")}</div>
</div>
<%!-- Footer: actions --%>
<div class="card-actions justify-end pt-1 border-t border-base-200">
<.link
navigate={
PhoenixKit.Utils.Routes.path(
"/admin/customer-service/tickets/#{ticket.uuid}"
)
}
class="btn btn-xs btn-outline btn-info"
>
<.icon name="hero-eye" class="w-3 h-3" /> {gettext("View")}
</.link>
<.link
navigate={
PhoenixKit.Utils.Routes.path(
"/admin/customer-service/tickets/#{ticket.uuid}/edit"
)
}
class="btn btn-xs btn-outline"
>
<.icon name="hero-pencil" class="w-3 h-3" /> {gettext("Edit")}
</.link>
</div>
</div>
</div>
<% end %>
</div>
</div>
<%!-- Pagination --%>
<%= if @total_pages > 1 do %>
<div class="flex flex-col sm:flex-row justify-between items-center gap-4">
<div class="text-sm text-base-content/70">
Showing {(@page - 1) * @per_page + 1} to {min(@page * @per_page, @total_count)} of {@total_count} tickets
</div>
<div class="join">
<%= if @page > 1 do %>
<button
phx-click="change_page"
phx-value-page={@page - 1}
class="join-item btn btn-outline"
>
« Previous
</button>
<% end %>
<%= for page_num <- max(1, @page - 2)..min(@total_pages, @page + 2) do %>
<button
phx-click="change_page"
phx-value-page={page_num}
class={"join-item btn #{if page_num == @page, do: "btn-primary", else: "btn-outline"}"}
>
{page_num}
</button>
<% end %>
<%= if @page < @total_pages do %>
<button
phx-click="change_page"
phx-value-page={@page + 1}
class="join-item btn btn-outline"
>
Next »
</button>
<% end %>
</div>
</div>
<% end %>
<% end %>
<% end %>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>