Packages
phoenix_kit
1.7.21
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/modules/tickets/web/details.ex
defmodule PhoenixKit.Modules.Tickets.Web.Details do
@moduledoc """
LiveView for displaying ticket details with comments and status management.
Provides comprehensive ticket detail view including:
- Full ticket information
- Status change buttons
- Public comment thread
- Internal notes section (for staff)
- Attachment gallery
- Status history timeline
"""
use PhoenixKitWeb, :live_view
require Logger
alias PhoenixKit.Modules.Tickets
alias PhoenixKit.Settings
alias PhoenixKit.Users.Roles
alias PhoenixKit.Utils.Routes
@impl true
def mount(%{"id" => id}, _session, socket) do
if Tickets.enabled?() do
current_user = socket.assigns[:phoenix_kit_current_user]
if can_access_tickets?(current_user) do
case Tickets.get_ticket(id, preload: [:user, :assigned_to]) do
nil ->
{:ok,
socket
|> put_flash(:error, "Ticket not found")
|> push_navigate(to: Routes.path("/admin/tickets"))}
ticket ->
project_title = Settings.get_setting("project_title", "PhoenixKit")
socket =
socket
|> assign(:page_title, "Ticket: #{ticket.title}")
|> assign(:project_title, project_title)
|> assign(:ticket, ticket)
|> assign(:current_user, current_user)
|> assign(:can_view_internal, can_access_tickets?(current_user))
|> assign(
:internal_notes_enabled,
Settings.get_boolean_setting("tickets_internal_notes_enabled", true)
)
|> assign(:comment_form, %{"content" => "", "is_internal" => false})
|> assign(:show_internal_form, false)
|> assign(:show_media_selector, false)
|> assign(
:attachments_enabled,
Settings.get_boolean_setting("tickets_attachments_enabled", true)
)
|> load_comments()
|> load_attachments()
|> load_status_history()
{:ok, socket}
end
else
{:ok,
socket
|> put_flash(:error, "Access denied")
|> push_navigate(to: Routes.path("/admin"))}
end
else
{:ok,
socket
|> put_flash(:error, "Tickets module is not enabled")
|> push_navigate(to: Routes.path("/admin"))}
end
end
@impl true
def handle_params(_params, uri, socket) do
{:noreply, assign(socket, :url_path, URI.parse(uri).path)}
end
@impl true
def handle_event("add_comment", %{"comment" => params}, socket) do
ticket = socket.assigns.ticket
current_user = socket.assigns.current_user
content = Map.get(params, "content", "") |> String.trim()
is_internal = Map.get(params, "is_internal", "false") == "true"
if content == "" do
{:noreply, put_flash(socket, :error, "Comment cannot be empty")}
else
result =
if is_internal do
Tickets.create_internal_note(ticket.id, current_user.id, %{content: content})
else
Tickets.create_comment(ticket.id, current_user.id, %{content: content})
end
case result do
{:ok, _comment} ->
{:noreply,
socket
|> put_flash(:info, if(is_internal, do: "Internal note added", else: "Comment added"))
|> assign(:comment_form, %{"content" => "", "is_internal" => false})
|> reload_ticket()
|> load_comments()}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to add comment")}
end
end
end
@impl true
def handle_event("toggle_internal_form", _params, socket) do
{:noreply, assign(socket, :show_internal_form, !socket.assigns.show_internal_form)}
end
@impl true
def handle_event("change_status", %{"status" => new_status}, socket) do
ticket = socket.assigns.ticket
current_user = socket.assigns.current_user
result =
case new_status do
"in_progress" -> Tickets.start_progress(ticket, current_user)
"resolved" -> Tickets.resolve_ticket(ticket, current_user)
"closed" -> Tickets.close_ticket(ticket, current_user)
"open" -> Tickets.reopen_ticket(ticket, current_user)
_ -> {:error, :invalid_status}
end
case result do
{:ok, updated_ticket} ->
{:noreply,
socket
|> put_flash(:info, "Status updated to #{new_status}")
|> assign(
:ticket,
Tickets.get_ticket!(updated_ticket.id, preload: [:user, :assigned_to])
)
|> load_status_history()}
{:error, :invalid_transition} ->
{:noreply, put_flash(socket, :error, "Invalid status transition")}
{:error, :reopen_not_allowed} ->
{:noreply, put_flash(socket, :error, "Reopening tickets is not allowed")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update status")}
end
end
@impl true
def handle_event("assign_to_me", _params, socket) do
ticket = socket.assigns.ticket
current_user = socket.assigns.current_user
case Tickets.assign_ticket(ticket, current_user.id, current_user) do
{:ok, updated_ticket} ->
{:noreply,
socket
|> put_flash(:info, "Ticket assigned to you")
|> assign(
:ticket,
Tickets.get_ticket!(updated_ticket.id, preload: [:user, :assigned_to])
)
|> load_status_history()}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to assign ticket")}
end
end
@impl true
def handle_event("delete_comment", %{"id" => comment_id}, socket) do
case Tickets.get_comment!(comment_id) do
nil ->
{:noreply, put_flash(socket, :error, "Comment not found")}
comment ->
case Tickets.delete_comment(comment) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "Comment deleted")
|> reload_ticket()
|> load_comments()}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to delete comment")}
end
end
end
@impl true
def handle_event("open_media_selector", _params, socket) do
{:noreply, assign(socket, :show_media_selector, true)}
end
@impl true
def handle_event("close_media_selector", _params, socket) do
{:noreply, assign(socket, :show_media_selector, false)}
end
@impl true
def handle_event("remove_attachment", %{"id" => attachment_id}, socket) do
case Tickets.remove_attachment(attachment_id) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "Attachment removed")
|> load_attachments()}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to remove attachment")}
end
end
@impl true
def handle_info({:media_selected, file_ids}, socket) do
ticket = socket.assigns.ticket
Enum.each(file_ids, fn file_id ->
Tickets.add_attachment_to_ticket(ticket.id, file_id)
end)
{:noreply,
socket
|> assign(:show_media_selector, false)
|> put_flash(:info, "#{length(file_ids)} file(s) attached")
|> load_attachments()}
end
# Private functions
defp can_access_tickets?(nil), do: false
defp can_access_tickets?(user) do
Roles.user_has_role_owner?(user) or
Roles.user_has_role_admin?(user) or
Roles.user_has_role?(user, "SupportAgent")
end
defp load_comments(socket) do
ticket = socket.assigns.ticket
comments =
if socket.assigns.can_view_internal do
Tickets.list_all_comments(ticket.id, preload: [:user])
else
Tickets.list_public_comments(ticket.id, preload: [:user])
end
public_comments = Enum.filter(comments, &(!&1.is_internal))
internal_notes = Enum.filter(comments, & &1.is_internal)
socket
|> assign(:public_comments, public_comments)
|> assign(:internal_notes, internal_notes)
end
defp load_attachments(socket) do
ticket = socket.assigns.ticket
attachments = Tickets.list_ticket_attachments(ticket.id, preload: [:file])
assign(socket, :attachments, attachments)
end
defp load_status_history(socket) do
ticket = socket.assigns.ticket
history = Tickets.get_status_history(ticket.id, preload: [:changed_by])
assign(socket, :status_history, history)
end
defp reload_ticket(socket) do
ticket = Tickets.get_ticket!(socket.assigns.ticket.id, preload: [:user, :assigned_to])
assign(socket, :ticket, ticket)
end
end