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.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={"Ticket: #{@ticket.title}"}
current_path={@url_path}
project_title={@project_title}
>
<div class="container mx-auto px-4 py-6">
<%!-- Header with Back Button --%>
<div class="mb-6">
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/tickets")}
class="btn btn-ghost btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"
/>
</svg>
Back to Tickets
</.link>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<%!-- Main Content --%>
<div class="lg:col-span-2 space-y-6">
<%!-- Ticket Info Card --%>
<div class="card bg-base-100 shadow">
<div class="card-body">
<div class="flex items-start justify-between">
<div>
<h1 class="card-title text-2xl">{@ticket.title}</h1>
<div class="flex items-center gap-3 mt-2 text-sm text-base-content/70">
<span>#{String.slice(@ticket.id, 0, 8)}</span>
<span>•</span>
<span>Created {Calendar.strftime(@ticket.inserted_at, "%Y-%m-%d %H:%M")}</span>
</div>
</div>
<div class="flex gap-2">
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/tickets/#{@ticket.id}/edit")}
class="btn btn-outline btn-sm"
>
Edit
</.link>
</div>
</div>
<div class="divider"></div>
<%!-- Description --%>
<div class="prose max-w-none">
<p class="whitespace-pre-wrap">{@ticket.description}</p>
</div>
<%!-- Attachments Section --%>
<%= if @attachments_enabled do %>
<div class="divider"></div>
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold">Attachments ({length(@attachments)})</h3>
<button
type="button"
phx-click="open_media_selector"
class="btn btn-primary btn-sm"
>
<.icon name="hero-paper-clip" class="w-4 h-4" /> Add Files
</button>
</div>
<%= if Enum.any?(@attachments) do %>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<%= for attachment <- @attachments do %>
<div class="relative group border rounded-lg overflow-hidden">
<%= if attachment.file do %>
<%= if attachment.file.file_type == "image" do %>
<img
src={
PhoenixKit.Modules.Storage.get_public_url_by_variant(
attachment.file,
"thumbnail"
)
}
alt={attachment.caption || attachment.file.original_file_name}
class="w-full h-24 object-cover"
/>
<% else %>
<div class="w-full h-24 flex items-center justify-center bg-base-200">
<.icon name="hero-document" class="w-8 h-8 text-base-content/50" />
</div>
<% end %>
<div class="p-2">
<div class="text-xs truncate">{attachment.file.original_file_name}</div>
</div>
<button
type="button"
phx-click="remove_attachment"
phx-value-id={attachment.id}
class="absolute top-1 right-1 btn btn-circle btn-xs btn-error opacity-0 group-hover:opacity-100 transition-opacity"
data-confirm="Remove this attachment?"
>
<.icon name="hero-x-mark" class="w-3 h-3" />
</button>
<% end %>
</div>
<% end %>
</div>
<% else %>
<div class="text-center py-4 text-base-content/50">
No attachments yet. Click "Add Files" to attach files.
</div>
<% end %>
<% end %>
</div>
</div>
<%!-- Public Comments Section --%>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title">Comments ({length(@public_comments)})</h2>
<%!-- Comments List --%>
<div class="space-y-4 mt-4">
<%= if Enum.empty?(@public_comments) do %>
<div class="text-center py-8 text-base-content/50">
No comments yet. Be the first to respond.
</div>
<% else %>
<%= for comment <- @public_comments do %>
<div class="bg-base-200 rounded-lg p-4">
<div class="flex items-start justify-between">
<div class="flex items-center gap-2">
<.user_avatar user={comment.user} size="sm" />
<div>
<span class="font-medium">{comment.user.email}</span>
<span class="text-sm text-base-content/50 ml-2">
{Calendar.strftime(comment.inserted_at, "%Y-%m-%d %H:%M")}
</span>
</div>
</div>
<button
phx-click="delete_comment"
phx-value-id={comment.id}
class="btn btn-ghost btn-xs"
data-confirm="Are you sure you want to delete this comment?"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
<div class="mt-2 whitespace-pre-wrap">{comment.content}</div>
</div>
<% end %>
<% end %>
</div>
<%!-- Add Comment Form --%>
<div class="divider"></div>
<form phx-submit="add_comment" class="space-y-4">
<input type="hidden" name="comment[is_internal]" value="false" />
<textarea
name="comment[content]"
class="textarea textarea-bordered w-full"
rows="3"
placeholder="Write a public reply..."
></textarea>
<div class="flex justify-end">
<button type="submit" class="btn btn-primary">
Add Comment
</button>
</div>
</form>
</div>
</div>
<%!-- Internal Notes Section (Staff Only) --%>
<%= if @can_view_internal and @internal_notes_enabled do %>
<div class="card bg-warning/10 border-2 border-warning/30 shadow">
<div class="card-body">
<div class="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-warning"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
<h2 class="card-title text-warning">
Internal Notes ({length(@internal_notes)})
</h2>
<span class="badge badge-warning badge-sm">Staff Only</span>
</div>
<%!-- Internal Notes List --%>
<div class="space-y-4 mt-4">
<%= if Enum.empty?(@internal_notes) do %>
<div class="text-center py-4 text-base-content/50">
No internal notes yet.
</div>
<% else %>
<%= for note <- @internal_notes do %>
<div class="bg-warning/20 rounded-lg p-4">
<div class="flex items-start justify-between">
<div class="flex items-center gap-2">
<.user_avatar user={note.user} size="sm" />
<div>
<span class="font-medium">{note.user.email}</span>
<span class="text-sm text-base-content/50 ml-2">
{Calendar.strftime(note.inserted_at, "%Y-%m-%d %H:%M")}
</span>
</div>
</div>
<button
phx-click="delete_comment"
phx-value-id={note.id}
class="btn btn-ghost btn-xs"
data-confirm="Are you sure you want to delete this note?"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
<div class="mt-2 whitespace-pre-wrap">{note.content}</div>
</div>
<% end %>
<% end %>
</div>
<%!-- Add Internal Note Form --%>
<div class="divider"></div>
<form phx-submit="add_comment" class="space-y-4">
<input type="hidden" name="comment[is_internal]" value="true" />
<textarea
name="comment[content]"
class="textarea textarea-bordered textarea-warning w-full"
rows="3"
placeholder="Write an internal note (not visible to customer)..."
></textarea>
<div class="flex justify-end">
<button type="submit" class="btn btn-warning">
Add Internal Note
</button>
</div>
</form>
</div>
</div>
<% end %>
</div>
<%!-- Sidebar --%>
<div class="space-y-6">
<%!-- Status & Actions Card --%>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h3 class="font-semibold mb-4">Status & Actions</h3>
<%!-- Current Status --%>
<div class="mb-4">
<span class="text-sm text-base-content/70">Current Status</span>
<div class="mt-1">
<%= case @ticket.status do %>
<% "open" -> %>
<span class="badge badge-info badge-lg">Open</span>
<% "in_progress" -> %>
<span class="badge badge-warning badge-lg">In Progress</span>
<% "resolved" -> %>
<span class="badge badge-success badge-lg">Resolved</span>
<% "closed" -> %>
<span class="badge badge-lg">Closed</span>
<% end %>
</div>
</div>
<%!-- Status Actions --%>
<div class="space-y-2">
<%= if @ticket.status == "open" do %>
<button
phx-click="change_status"
phx-value-status="in_progress"
class="btn btn-warning btn-sm w-full"
>
Start Progress
</button>
<button
phx-click="change_status"
phx-value-status="resolved"
class="btn btn-success btn-sm w-full"
>
Mark Resolved
</button>
<% end %>
<%= if @ticket.status == "in_progress" do %>
<button
phx-click="change_status"
phx-value-status="resolved"
class="btn btn-success btn-sm w-full"
>
Mark Resolved
</button>
<button
phx-click="change_status"
phx-value-status="open"
class="btn btn-ghost btn-sm w-full"
>
Return to Open
</button>
<% end %>
<%= if @ticket.status == "resolved" do %>
<button
phx-click="change_status"
phx-value-status="closed"
class="btn btn-sm w-full"
>
Close Ticket
</button>
<button
phx-click="change_status"
phx-value-status="open"
class="btn btn-ghost btn-sm w-full"
>
Reopen
</button>
<% end %>
<%= if @ticket.status == "closed" do %>
<button
phx-click="change_status"
phx-value-status="open"
class="btn btn-info btn-sm w-full"
>
Reopen Ticket
</button>
<% end %>
</div>
</div>
</div>
<%!-- Details Card --%>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h3 class="font-semibold mb-4">Details</h3>
<%!-- Customer --%>
<div class="mb-4">
<span class="text-sm text-base-content/70">Customer</span>
<div class="mt-1 flex items-center gap-2">
<%= if @ticket.user do %>
<.user_avatar user={@ticket.user} size="xs" />
<span class="text-sm">{@ticket.user.email}</span>
<% else %>
<span class="text-sm text-base-content/50">Unknown</span>
<% end %>
</div>
</div>
<%!-- Assigned To --%>
<div class="mb-4">
<span class="text-sm text-base-content/70">Assigned To</span>
<div class="mt-1">
<%= if @ticket.assigned_to do %>
<span class="badge badge-outline">{@ticket.assigned_to.email}</span>
<% else %>
<div class="flex items-center gap-2">
<span class="badge badge-ghost">Unassigned</span>
<button phx-click="assign_to_me" class="btn btn-xs btn-primary">
Assign to me
</button>
</div>
<% end %>
</div>
</div>
<%!-- Comments Count --%>
<div class="mb-4">
<span class="text-sm text-base-content/70">Comments</span>
<div class="mt-1 font-medium">{@ticket.comment_count}</div>
</div>
<%!-- Timestamps --%>
<%= if @ticket.resolved_at do %>
<div class="mb-4">
<span class="text-sm text-base-content/70">Resolved At</span>
<div class="mt-1 text-sm">
{Calendar.strftime(@ticket.resolved_at, "%Y-%m-%d %H:%M")}
</div>
</div>
<% end %>
<%= if @ticket.closed_at do %>
<div class="mb-4">
<span class="text-sm text-base-content/70">Closed At</span>
<div class="mt-1 text-sm">
{Calendar.strftime(@ticket.closed_at, "%Y-%m-%d %H:%M")}
</div>
</div>
<% end %>
</div>
</div>
<%!-- Status History Card --%>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h3 class="font-semibold mb-4">Status History</h3>
<div class="space-y-3">
<%= for entry <- @status_history do %>
<div class="flex items-start gap-3 text-sm">
<div class="w-2 h-2 rounded-full bg-base-content/30 mt-1.5"></div>
<div>
<div>
<%= if entry.from_status do %>
<span class="badge badge-xs">{entry.from_status}</span> →
<% end %>
<span class="badge badge-xs badge-primary">{entry.to_status}</span>
</div>
<div class="text-base-content/50 mt-1">
by {(entry.changed_by && entry.changed_by.email) || "System"}
<br />
{Calendar.strftime(entry.inserted_at, "%Y-%m-%d %H:%M")}
</div>
<%= if entry.reason do %>
<div class="text-base-content/70 mt-1 italic">"{entry.reason}"</div>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>
<%!-- Media Selector Modal --%>
<%= if @attachments_enabled do %>
<.live_component
module={PhoenixKitWeb.Live.Components.MediaSelectorModal}
id="ticket-media-selector"
show={@show_media_selector}
mode={:multiple}
selected_ids={Enum.map(@attachments, & &1.file_id)}
phoenix_kit_current_user={@current_user}
/>
<% end %>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>