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/tickets.ex
defmodule PhoenixKit.Modules.Tickets do
@moduledoc """
Context for managing support tickets, comments, and attachments.
Provides complete API for the customer support ticketing system including
CRUD operations, status workflow, comment threading with internal notes,
file attachments, and audit trail.
## Features
- **Ticket Management**: Create, update, delete tickets
- **Status Workflow**: open → in_progress → resolved → closed
- **Assignment**: Assign tickets to support staff
- **Comment System**: Public comments and internal notes
- **File Attachments**: Multiple files per ticket/comment
- **Audit Trail**: Complete status change history
## Status Flow
- `open` - New ticket, awaiting assignment or response
- `in_progress` - Being worked on by support staff
- `resolved` - Issue resolved, awaiting confirmation
- `closed` - Ticket closed (resolved or abandoned)
## Examples
# Create a ticket
{:ok, ticket} = Tickets.create_ticket(user_id, %{
title: "Cannot login",
description: "I get an error when trying to login..."
})
# Assign to support staff
{:ok, ticket} = Tickets.assign_ticket(ticket, staff_user_id, current_user)
# Start working on it
{:ok, ticket} = Tickets.start_progress(ticket, current_user)
# Add a public comment
{:ok, comment} = Tickets.create_comment(ticket.id, staff_user_id, %{
content: "We're looking into this issue."
})
# Add an internal note (hidden from customer)
{:ok, note} = Tickets.create_internal_note(ticket.id, staff_user_id, %{
content: "Customer seems frustrated. Need to escalate."
})
# Resolve the ticket
{:ok, ticket} = Tickets.resolve_ticket(ticket, current_user, "Fixed in v2.0.1")
"""
import Ecto.Query, warn: false
alias PhoenixKit.Settings
alias PhoenixKit.Modules.Tickets.{
Ticket,
TicketAttachment,
TicketComment,
TicketStatusHistory
}
# ============================================================================
# Module Status
# ============================================================================
@doc """
Checks if the Tickets module is enabled.
## Examples
iex> enabled?()
false
"""
def enabled? do
Settings.get_boolean_setting("tickets_enabled", false)
end
@doc """
Enables the Tickets module.
## Examples
iex> enable_system()
{:ok, %Setting{}}
"""
def enable_system do
Settings.update_boolean_setting_with_module("tickets_enabled", true, "tickets")
end
@doc """
Disables the Tickets module.
## Examples
iex> disable_system()
{:ok, %Setting{}}
"""
def disable_system do
Settings.update_boolean_setting_with_module("tickets_enabled", false, "tickets")
end
@doc """
Gets the current Tickets module configuration and stats.
## Examples
iex> get_config()
%{enabled: false, total_tickets: 0, open_tickets: 0, ...}
"""
def get_config do
%{
enabled: enabled?(),
total_tickets: count_tickets(),
open_tickets: count_tickets_by_status("open"),
in_progress_tickets: count_tickets_by_status("in_progress"),
resolved_tickets: count_tickets_by_status("resolved"),
closed_tickets: count_tickets_by_status("closed"),
comments_enabled: Settings.get_boolean_setting("tickets_comments_enabled", true),
internal_notes_enabled:
Settings.get_boolean_setting("tickets_internal_notes_enabled", true),
attachments_enabled: Settings.get_boolean_setting("tickets_attachments_enabled", true),
allow_reopen: Settings.get_boolean_setting("tickets_allow_reopen", true)
}
end
defp count_tickets do
repo().aggregate(Ticket, :count, :id)
rescue
_ -> 0
end
defp count_tickets_by_status(status) do
from(t in Ticket, where: t.status == ^status)
|> repo().aggregate(:count, :id)
rescue
_ -> 0
end
# ============================================================================
# Ticket CRUD Operations
# ============================================================================
@doc """
Creates a new ticket.
## Parameters
- `user_id` - Customer who created the ticket
- `attrs` - Ticket attributes (title, description)
## Examples
iex> create_ticket(42, %{title: "Bug report", description: "Something is wrong"})
{:ok, %Ticket{}}
iex> create_ticket(42, %{title: ""})
{:error, %Ecto.Changeset{}}
"""
def create_ticket(user_id, attrs) when is_integer(user_id) do
attrs =
attrs
|> Map.put("user_id", user_id)
|> Map.put("status", "open")
repo().transaction(fn ->
case %Ticket{}
|> Ticket.changeset(attrs)
|> repo().insert() do
{:ok, ticket} ->
# Record initial status in history
create_status_history(ticket.id, user_id, nil, "open", nil)
ticket
{:error, changeset} ->
repo().rollback(changeset)
end
end)
end
@doc """
Updates an existing ticket.
## Parameters
- `ticket` - Ticket struct to update
- `attrs` - Attributes to update
## Examples
iex> update_ticket(ticket, %{title: "Updated Title"})
{:ok, %Ticket{}}
"""
def update_ticket(%Ticket{} = ticket, attrs) do
ticket
|> Ticket.changeset(attrs)
|> repo().update()
end
@doc """
Deletes a ticket and all related data.
## Parameters
- `ticket` - Ticket struct to delete
## Examples
iex> delete_ticket(ticket)
{:ok, %Ticket{}}
"""
def delete_ticket(%Ticket{} = ticket) do
repo().delete(ticket)
end
@doc """
Gets a single ticket by ID with optional preloads.
Raises `Ecto.NoResultsError` if ticket not found.
## Parameters
- `id` - Ticket ID (UUIDv7)
- `opts` - Options
- `:preload` - List of associations to preload
## Examples
iex> get_ticket!("018e3c4a-...")
%Ticket{}
iex> get_ticket!("018e3c4a-...", preload: [:user, :assigned_to, :comments])
%Ticket{user: %User{}, assigned_to: %User{}, comments: [...]}
"""
def get_ticket!(id, opts \\ []) do
preloads = Keyword.get(opts, :preload, [])
Ticket
|> repo().get!(id)
|> repo().preload(preloads)
end
@doc """
Gets a single ticket by ID. Returns nil if not found.
"""
def get_ticket(id, opts \\ []) do
preloads = Keyword.get(opts, :preload, [])
case repo().get(Ticket, id) do
nil -> nil
ticket -> repo().preload(ticket, preloads)
end
end
@doc """
Gets a single ticket by slug.
## Parameters
- `slug` - Ticket slug
- `opts` - Options
- `:preload` - List of associations to preload
## Examples
iex> get_ticket_by_slug("cannot-login-123456")
%Ticket{}
"""
def get_ticket_by_slug(slug, opts \\ []) when is_binary(slug) do
preloads = Keyword.get(opts, :preload, [])
Ticket
|> where([t], t.slug == ^slug)
|> repo().one()
|> case do
nil -> nil
ticket -> repo().preload(ticket, preloads)
end
end
@doc """
Lists tickets with optional filtering and pagination.
## Parameters
- `opts` - Options
- `:user_id` - Filter by customer (ticket creator)
- `:assigned_to_id` - Filter by assigned handler
- `:status` - Filter by status (open/in_progress/resolved/closed)
- `:search` - Search in title and description
- `:page` - Page number (default: 1)
- `:per_page` - Items per page (default: 20)
- `:preload` - Associations to preload
## Examples
iex> list_tickets()
[%Ticket{}, ...]
iex> list_tickets(status: "open", assigned_to_id: nil)
[%Ticket{}, ...]
"""
def list_tickets(opts \\ []) do
user_id = Keyword.get(opts, :user_id)
assigned_to_id = Keyword.get(opts, :assigned_to_id)
status = Keyword.get(opts, :status)
search = Keyword.get(opts, :search)
preloads = Keyword.get(opts, :preload, [])
page = Keyword.get(opts, :page, 1)
per_page = Keyword.get(opts, :per_page, 20)
Ticket
|> maybe_filter_by_user(user_id)
|> maybe_filter_by_assigned_to(assigned_to_id)
|> maybe_filter_by_status(status)
|> maybe_search_tickets(search)
|> order_by([t], desc: t.inserted_at)
|> paginate(page, per_page)
|> repo().all()
|> repo().preload(preloads)
end
@doc """
Lists unassigned tickets (where assigned_to_id is nil).
"""
def list_unassigned_tickets(opts \\ []) do
opts = Keyword.put(opts, :assigned_to_id, nil)
list_tickets(opts)
end
@doc """
Lists tickets assigned to a specific handler.
"""
def list_tickets_assigned_to(handler_id, opts \\ []) when is_integer(handler_id) do
opts = Keyword.put(opts, :assigned_to_id, handler_id)
list_tickets(opts)
end
@doc """
Lists tickets created by a specific user.
"""
def list_user_tickets(user_id, opts \\ []) when is_integer(user_id) do
opts = Keyword.put(opts, :user_id, user_id)
list_tickets(opts)
end
# ============================================================================
# Status Transitions
# ============================================================================
@doc """
Assigns a ticket to a support staff member.
If the ticket is open, it will be moved to in_progress.
## Parameters
- `ticket` - Ticket to assign
- `handler_id` - User ID of the support staff
- `changed_by` - User making the change
## Examples
iex> assign_ticket(ticket, 5, current_user)
{:ok, %Ticket{assigned_to_id: 5}}
"""
def assign_ticket(%Ticket{} = ticket, handler_id, changed_by) when is_integer(handler_id) do
changed_by_id = get_user_id(changed_by)
repo().transaction(fn ->
attrs = %{assigned_to_id: handler_id}
# If ticket is open, move to in_progress
{attrs, new_status} =
if ticket.status == "open" do
{Map.put(attrs, :status, "in_progress"), "in_progress"}
else
{attrs, nil}
end
case update_ticket(ticket, attrs) do
{:ok, updated_ticket} ->
if new_status do
create_status_history(
ticket.id,
changed_by_id,
ticket.status,
new_status,
"Assigned to handler"
)
end
updated_ticket
{:error, changeset} ->
repo().rollback(changeset)
end
end)
end
@doc """
Moves ticket to in_progress status.
## Parameters
- `ticket` - Ticket to update
- `changed_by` - User making the change
## Examples
iex> start_progress(ticket, current_user)
{:ok, %Ticket{status: "in_progress"}}
"""
def start_progress(%Ticket{} = ticket, changed_by) do
transition_status(ticket, "in_progress", changed_by)
end
@doc """
Resolves a ticket.
## Parameters
- `ticket` - Ticket to resolve
- `changed_by` - User making the change
- `reason` - Optional resolution reason
## Examples
iex> resolve_ticket(ticket, current_user, "Fixed in version 2.0.1")
{:ok, %Ticket{status: "resolved"}}
"""
def resolve_ticket(%Ticket{} = ticket, changed_by, reason \\ nil) do
transition_status(ticket, "resolved", changed_by, reason)
end
@doc """
Closes a ticket.
## Parameters
- `ticket` - Ticket to close
- `changed_by` - User making the change
- `reason` - Optional close reason
## Examples
iex> close_ticket(ticket, current_user, "No response from customer")
{:ok, %Ticket{status: "closed"}}
"""
def close_ticket(%Ticket{} = ticket, changed_by, reason \\ nil) do
transition_status(ticket, "closed", changed_by, reason)
end
@doc """
Reopens a closed or resolved ticket.
## Parameters
- `ticket` - Ticket to reopen
- `changed_by` - User making the change
- `reason` - Optional reopen reason
## Examples
iex> reopen_ticket(ticket, current_user, "Issue still occurring")
{:ok, %Ticket{status: "open"}}
"""
def reopen_ticket(%Ticket{} = ticket, changed_by, reason \\ nil) do
if Settings.get_boolean_setting("tickets_allow_reopen", true) do
transition_status(ticket, "open", changed_by, reason)
else
{:error, :reopen_not_allowed}
end
end
defp transition_status(%Ticket{} = ticket, new_status, changed_by, reason \\ nil) do
changed_by_id = get_user_id(changed_by)
if Ticket.valid_transition?(ticket.status, new_status) do
repo().transaction(fn ->
attrs = %{status: new_status}
# Set timestamps based on new status
attrs =
case new_status do
"resolved" -> Map.put(attrs, :resolved_at, DateTime.utc_now())
"closed" -> Map.put(attrs, :closed_at, DateTime.utc_now())
"open" -> Map.merge(attrs, %{resolved_at: nil, closed_at: nil})
_ -> attrs
end
case update_ticket(ticket, attrs) do
{:ok, updated_ticket} ->
create_status_history(ticket.id, changed_by_id, ticket.status, new_status, reason)
updated_ticket
{:error, changeset} ->
repo().rollback(changeset)
end
end)
else
{:error, :invalid_transition}
end
end
defp create_status_history(ticket_id, changed_by_id, from_status, to_status, reason) do
%TicketStatusHistory{}
|> TicketStatusHistory.changeset(%{
ticket_id: ticket_id,
changed_by_id: changed_by_id,
from_status: from_status,
to_status: to_status,
reason: reason
})
|> repo().insert()
end
# ============================================================================
# Comments
# ============================================================================
@doc """
Creates a public comment on a ticket.
## Parameters
- `ticket_id` - ID of the ticket
- `user_id` - ID of the commenter
- `attrs` - Comment attributes (content, optional parent_id)
## Examples
iex> create_comment(ticket.id, user_id, %{content: "Thanks for looking into this!"})
{:ok, %TicketComment{}}
"""
def create_comment(ticket_id, user_id, attrs) when is_integer(user_id) do
attrs =
attrs
|> ensure_string_keys()
|> Map.put("ticket_id", ticket_id)
|> Map.put("user_id", user_id)
|> Map.put("is_internal", false)
attrs = maybe_calculate_depth(attrs)
repo().transaction(fn ->
case %TicketComment{}
|> TicketComment.changeset(attrs)
|> repo().insert() do
{:ok, comment} ->
increment_comment_count(ticket_id)
comment
{:error, changeset} ->
repo().rollback(changeset)
end
end)
end
@doc """
Creates an internal note on a ticket (visible only to support staff).
## Parameters
- `ticket_id` - ID of the ticket
- `user_id` - ID of the staff member
- `attrs` - Note attributes (content)
## Examples
iex> create_internal_note(ticket.id, staff_id, %{content: "Customer seems frustrated"})
{:ok, %TicketComment{is_internal: true}}
"""
def create_internal_note(ticket_id, user_id, attrs) when is_integer(user_id) do
attrs =
attrs
|> ensure_string_keys()
|> Map.put("ticket_id", ticket_id)
|> Map.put("user_id", user_id)
|> Map.put("is_internal", true)
attrs = maybe_calculate_depth(attrs)
%TicketComment{}
|> TicketComment.changeset(attrs)
|> repo().insert()
end
@doc """
Updates a comment.
"""
def update_comment(%TicketComment{} = comment, attrs) do
comment
|> TicketComment.changeset(attrs)
|> repo().update()
end
@doc """
Deletes a comment.
"""
def delete_comment(%TicketComment{} = comment) do
repo().transaction(fn ->
case repo().delete(comment) do
{:ok, deleted} ->
unless comment.is_internal do
decrement_comment_count(comment.ticket_id)
end
deleted
{:error, changeset} ->
repo().rollback(changeset)
end
end)
end
@doc """
Gets a comment by ID.
"""
def get_comment!(id, opts \\ []) do
preloads = Keyword.get(opts, :preload, [])
TicketComment
|> repo().get!(id)
|> repo().preload(preloads)
end
@doc """
Lists public comments for a ticket (excludes internal notes).
"""
def list_public_comments(ticket_id, opts \\ []) do
preloads = Keyword.get(opts, :preload, [:user])
from(c in TicketComment,
where: c.ticket_id == ^ticket_id and c.is_internal == false,
order_by: [asc: c.inserted_at]
)
|> repo().all()
|> repo().preload(preloads)
end
@doc """
Lists all comments for a ticket (includes internal notes).
For staff use only.
"""
def list_all_comments(ticket_id, opts \\ []) do
preloads = Keyword.get(opts, :preload, [:user])
from(c in TicketComment,
where: c.ticket_id == ^ticket_id,
order_by: [asc: c.inserted_at]
)
|> repo().all()
|> repo().preload(preloads)
end
@doc """
Lists only internal notes for a ticket.
"""
def list_internal_notes(ticket_id, opts \\ []) do
preloads = Keyword.get(opts, :preload, [:user])
from(c in TicketComment,
where: c.ticket_id == ^ticket_id and c.is_internal == true,
order_by: [asc: c.inserted_at]
)
|> repo().all()
|> repo().preload(preloads)
end
defp maybe_calculate_depth(attrs) do
parent_id = Map.get(attrs, "parent_id") || Map.get(attrs, :parent_id)
if parent_id do
parent = repo().get!(TicketComment, parent_id)
Map.put(attrs, "depth", parent.depth + 1)
else
Map.put(attrs, "depth", 0)
end
end
defp increment_comment_count(ticket_id) do
from(t in Ticket, where: t.id == ^ticket_id)
|> repo().update_all(inc: [comment_count: 1])
end
defp decrement_comment_count(ticket_id) do
from(t in Ticket, where: t.id == ^ticket_id)
|> repo().update_all(inc: [comment_count: -1])
end
defp ensure_string_keys(map) when is_map(map) do
Map.new(map, fn
{k, v} when is_atom(k) -> {Atom.to_string(k), v}
{k, v} -> {k, v}
end)
end
# ============================================================================
# Attachments
# ============================================================================
@doc """
Attaches a file to a ticket.
## Parameters
- `ticket_id` - ID of the ticket
- `file_id` - ID of the uploaded file
- `opts` - Options
- `:position` - Display order (default: auto-calculated)
- `:caption` - Optional caption
## Examples
iex> add_attachment_to_ticket(ticket.id, file.id, caption: "Error screenshot")
{:ok, %TicketAttachment{}}
"""
def add_attachment_to_ticket(ticket_id, file_id, opts \\ []) do
position = Keyword.get(opts, :position) || next_ticket_attachment_position(ticket_id)
caption = Keyword.get(opts, :caption)
%TicketAttachment{}
|> TicketAttachment.changeset(%{
ticket_id: ticket_id,
file_id: file_id,
position: position,
caption: caption
})
|> repo().insert()
end
@doc """
Attaches a file to a comment.
"""
def add_attachment_to_comment(comment_id, file_id, opts \\ []) do
position = Keyword.get(opts, :position) || next_comment_attachment_position(comment_id)
caption = Keyword.get(opts, :caption)
%TicketAttachment{}
|> TicketAttachment.changeset(%{
comment_id: comment_id,
file_id: file_id,
position: position,
caption: caption
})
|> repo().insert()
end
@doc """
Removes an attachment.
"""
def remove_attachment(attachment_id) do
attachment = repo().get!(TicketAttachment, attachment_id)
repo().delete(attachment)
end
@doc """
Lists attachments for a ticket.
"""
def list_ticket_attachments(ticket_id, opts \\ []) do
preloads = Keyword.get(opts, :preload, [:file])
from(a in TicketAttachment,
where: a.ticket_id == ^ticket_id and is_nil(a.comment_id),
order_by: [asc: a.position]
)
|> repo().all()
|> repo().preload(preloads)
end
@doc """
Lists attachments for a comment.
"""
def list_comment_attachments(comment_id, opts \\ []) do
preloads = Keyword.get(opts, :preload, [:file])
from(a in TicketAttachment,
where: a.comment_id == ^comment_id,
order_by: [asc: a.position]
)
|> repo().all()
|> repo().preload(preloads)
end
defp next_ticket_attachment_position(ticket_id) do
from(a in TicketAttachment,
where: a.ticket_id == ^ticket_id and is_nil(a.comment_id),
select: coalesce(max(a.position), 0)
)
|> repo().one()
|> Kernel.+(1)
end
defp next_comment_attachment_position(comment_id) do
from(a in TicketAttachment,
where: a.comment_id == ^comment_id,
select: coalesce(max(a.position), 0)
)
|> repo().one()
|> Kernel.+(1)
end
# ============================================================================
# Status History
# ============================================================================
@doc """
Gets the status history for a ticket.
"""
def get_status_history(ticket_id, opts \\ []) do
preloads = Keyword.get(opts, :preload, [:changed_by])
from(h in TicketStatusHistory,
where: h.ticket_id == ^ticket_id,
order_by: [asc: h.inserted_at]
)
|> repo().all()
|> repo().preload(preloads)
end
# ============================================================================
# Statistics
# ============================================================================
@doc """
Gets ticket statistics.
"""
def get_stats do
%{
total: count_tickets(),
open: count_tickets_by_status("open"),
in_progress: count_tickets_by_status("in_progress"),
resolved: count_tickets_by_status("resolved"),
closed: count_tickets_by_status("closed"),
unassigned: count_unassigned_tickets()
}
end
defp count_unassigned_tickets do
from(t in Ticket, where: is_nil(t.assigned_to_id) and t.status in ["open", "in_progress"])
|> repo().aggregate(:count, :id)
rescue
_ -> 0
end
# ============================================================================
# Private Helpers
# ============================================================================
defp maybe_filter_by_user(query, nil), do: query
defp maybe_filter_by_user(query, user_id) do
where(query, [t], t.user_id == ^user_id)
end
defp maybe_filter_by_assigned_to(query, nil), do: query
defp maybe_filter_by_assigned_to(query, :unassigned) do
where(query, [t], is_nil(t.assigned_to_id))
end
defp maybe_filter_by_assigned_to(query, assigned_to_id) do
where(query, [t], t.assigned_to_id == ^assigned_to_id)
end
defp maybe_filter_by_status(query, nil), do: query
defp maybe_filter_by_status(query, status) when is_binary(status) do
where(query, [t], t.status == ^status)
end
defp maybe_filter_by_status(query, statuses) when is_list(statuses) do
where(query, [t], t.status in ^statuses)
end
defp maybe_search_tickets(query, nil), do: query
defp maybe_search_tickets(query, ""), do: query
defp maybe_search_tickets(query, search) do
search_pattern = "%#{search}%"
where(query, [t], ilike(t.title, ^search_pattern) or ilike(t.description, ^search_pattern))
end
defp paginate(query, page, per_page) do
offset = (page - 1) * per_page
query
|> limit(^per_page)
|> offset(^offset)
end
defp get_user_id(%{id: id}), do: id
defp get_user_id(id) when is_integer(id), do: id
defp repo do
PhoenixKit.RepoHelper.repo()
end
end