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
phoenix_kit lib modules tickets web user_details.ex
Raw

lib/modules/tickets/web/user_details.ex

defmodule PhoenixKit.Modules.Tickets.Web.UserDetails do
@moduledoc """
LiveView for displaying ticket details to the ticket owner.
Users can view their own tickets, see public comments (not internal notes),
and add new comments with optional attachments.
Security:
- Users can only view their own tickets (user_id check)
- Internal notes (is_internal: true) are hidden from users
- Users cannot change ticket status
"""
use PhoenixKitWeb, :live_view
require Logger
alias PhoenixKit.Modules.Storage
alias PhoenixKit.Modules.Tickets
alias PhoenixKit.Settings
alias PhoenixKit.Users.Auth
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]
case Tickets.get_ticket(id, preload: [:user, :assigned_to]) do
nil ->
{:ok,
socket
|> put_flash(:error, gettext("Ticket not found"))
|> push_navigate(to: Routes.path("/dashboard/tickets"))}
ticket ->
# Security: Verify user owns this ticket
if ticket.user_id != current_user.id do
{:ok,
socket
|> put_flash(:error, gettext("Access denied"))
|> push_navigate(to: Routes.path("/dashboard/tickets"))}
else
attachments_enabled =
Settings.get_boolean_setting("tickets_attachments_enabled", true)
socket =
socket
|> assign(:page_title, ticket.title)
|> assign(:ticket, ticket)
|> assign(:current_user, current_user)
|> assign(:comment_content, "")
|> assign(:attachments_enabled, attachments_enabled)
|> assign(:pending_comment_file_ids, [])
|> assign(:pending_comment_files, [])
|> load_public_comments()
|> load_attachments()
|> maybe_allow_comment_upload(attachments_enabled)
{:ok, socket}
end
end
else
{:ok,
socket
|> put_flash(:error, gettext("Tickets module is not enabled"))
|> push_navigate(to: Routes.path("/dashboard"))}
end
end
defp maybe_allow_comment_upload(socket, true) do
allow_upload(socket, :comment_attachments,
accept: ~w(.jpg .jpeg .png .gif .webp .pdf .doc .docx .txt),
max_entries: 3,
max_file_size: 10_000_000,
auto_upload: true
)
end
defp maybe_allow_comment_upload(socket, false), do: socket
@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()
if content == "" do
{:noreply, put_flash(socket, :error, gettext("Comment cannot be empty"))}
else
# Process any pending uploads first
socket = process_comment_uploads(socket)
pending_file_ids = socket.assigns.pending_comment_file_ids
# Users can only add public comments (is_internal is always false)
case Tickets.create_comment(ticket.id, current_user.id, %{content: content}) do
{:ok, comment} ->
# Attach any pending files to the comment
Enum.each(pending_file_ids, fn file_id ->
Tickets.add_attachment_to_comment(comment.id, file_id)
end)
{:noreply,
socket
|> put_flash(:info, gettext("Comment added"))
|> assign(:comment_content, "")
|> assign(:pending_comment_file_ids, [])
|> assign(:pending_comment_files, [])
|> reload_ticket()
|> load_public_comments()}
{:error, _} ->
{:noreply, put_flash(socket, :error, gettext("Failed to add comment"))}
end
end
end
@impl true
def handle_event("validate", _params, socket) do
# Handle file upload validation
{:noreply, socket}
end
@impl true
def handle_event("cancel_upload", %{"ref" => ref}, socket) do
{:noreply, cancel_upload(socket, :comment_attachments, ref)}
end
@impl true
def handle_event("remove_pending_comment_file", %{"id" => file_id}, socket) do
pending_file_ids = Enum.reject(socket.assigns.pending_comment_file_ids, &(&1 == file_id))
pending_files = Enum.reject(socket.assigns.pending_comment_files, &(&1.id == file_id))
{:noreply,
socket
|> assign(:pending_comment_file_ids, pending_file_ids)
|> assign(:pending_comment_files, pending_files)}
end
# Private functions
defp load_public_comments(socket) do
ticket = socket.assigns.ticket
# Only load public comments - exclude internal notes
comments = Tickets.list_public_comments(ticket.id, preload: [:user])
assign(socket, :comments, comments)
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 reload_ticket(socket) do
ticket = Tickets.get_ticket!(socket.assigns.ticket.id, preload: [:user, :assigned_to])
assign(socket, :ticket, ticket)
end
defp process_comment_uploads(socket) do
if socket.assigns.attachments_enabled and
Map.has_key?(socket.assigns, :uploads) and
socket.assigns.uploads.comment_attachments.entries != [] do
do_process_comment_uploads(socket)
else
socket
end
end
defp do_process_comment_uploads(socket) do
current_user = socket.assigns.current_user
uploaded_files =
consume_uploaded_entries(socket, :comment_attachments, fn %{path: path}, entry ->
ext = Path.extname(entry.client_name) |> String.replace_leading(".", "")
user_id = current_user.id
{:ok, _stat} = File.stat(path)
file_hash = Auth.calculate_file_hash(path)
case Storage.store_file_in_buckets(
path,
"document",
user_id,
file_hash,
ext,
entry.client_name
) do
{:ok, file, :duplicate} ->
Logger.info("Comment attachment is duplicate with ID: #{file.id}")
{:ok, file}
{:ok, file} ->
Logger.info("Comment attachment stored with ID: #{file.id}")
{:ok, file}
{:error, reason} ->
Logger.error("Storage Error: #{inspect(reason)}")
{:error, reason}
end
end)
# Extract successful uploads
new_files =
uploaded_files
|> Enum.filter(&match?({:ok, _}, &1))
|> Enum.map(fn {:ok, file} -> file end)
new_file_ids = Enum.map(new_files, & &1.id)
socket
|> assign(:pending_comment_file_ids, socket.assigns.pending_comment_file_ids ++ new_file_ids)
|> assign(:pending_comment_files, socket.assigns.pending_comment_files ++ new_files)
end
end