Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions services/platform/apps/tickets/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from django.db.models import Q
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _

Expand All @@ -34,6 +35,24 @@
# Module-level default for file size limit (must match tickets.security)
_DEFAULT_MAX_FILE_SIZE_BYTES = 2097152 # 2MB

TICKET_STATUS_TABS = [
{"value": "", "label": _("All"), "border_class": "border-blue-500", "text_class": "text-blue-400"},
{"value": "open", "label": _("Open"), "border_class": "border-blue-500", "text_class": "text-blue-400"},
{
"value": "in_progress",
"label": _("In Progress"),
"border_class": "border-purple-500",
"text_class": "text-purple-400",
},
{
"value": "waiting_on_customer",
"label": _("Waiting on Customer"),
"border_class": "border-yellow-500",
"text_class": "text-yellow-400",
},
{"value": "closed", "label": _("Closed"), "border_class": "border-red-500", "text_class": "text-red-400"},
]


@dataclass
class TicketReplyData:
Expand Down Expand Up @@ -93,14 +112,36 @@ def ticket_list(request: HttpRequest) -> HttpResponse:
url_params.append(f"status={status_filter}")
url_params_str = "&".join(url_params)

open_count = tickets.filter(status__in=["open", "in_progress"]).count()
waiting_count = tickets.filter(status="waiting_on_customer").count()
total_count = tickets.count()

context = {
"tickets": tickets_page,
"open_count": tickets.filter(status__in=["open", "in_progress"]).count(),
"waiting_count": tickets.filter(status="waiting_on_customer").count(),
"total_count": tickets.count(),
"open_count": open_count,
"waiting_count": waiting_count,
"total_count": total_count,
"search_query": search_query,
"status_filter": status_filter,
"url_params": url_params_str,
# Shared component: header
"list_icon_bg": "bg-blue-600",
"list_icon_name": "chat",
"list_title": _("Support Tickets"),
"list_subtitle": _("Manage customer support requests"),
"list_stats": [
{"value": str(open_count), "label": _("Open"), "color": "text-amber-400"},
{"value": str(waiting_count), "label": _("Waiting"), "color": "text-yellow-400"},
{"value": str(total_count), "label": _("Total"), "color": "text-slate-400"},
],
# Shared component: filters
"filter_search_url": reverse("tickets:search_htmx"),
"filter_content_id": "tickets-content",
"filter_skeleton_id": "tickets-skeleton",
"filter_tabs": TICKET_STATUS_TABS,
"filter_active_tab": status_filter,
"filter_search_value": search_query,
"filter_search_placeholder": _("Search tickets…"),
}

return render(request, "tickets/list.html", context)
Expand Down
265 changes: 8 additions & 257 deletions services/platform/templates/tickets/list.html
Original file line number Diff line number Diff line change
@@ -1,274 +1,25 @@
{% extends 'base.html' %}
{% load i18n %}
{% load ui_components %}

{% block title %}{% trans 'Support Tickets' %} - PRAHO Platform{% endblock %}

{% block content %}
<div class="space-y-6">
<!-- Modern Header Card -->
<div class="bg-slate-800 rounded-lg border border-slate-700 mb-6">
<!-- Desktop Layout -->
<div class="hidden md:block p-6">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 bg-blue-600 rounded-lg flex items-center justify-center">
{% icon "tickets" size="lg" css_class="text-white" %}
</div>
<div>
<h1 class="text-2xl font-bold text-white">
{% if user.is_staff %}
{% trans "Support Tickets" %}
{% else %}
{% trans "My Support Tickets" %}
{% endif %}
</h1>
<p class="text-slate-400">
{% if user.is_staff %}
{% trans "Manage customer support requests" %}
{% else %}
{% trans "Get help with your hosting services" %}
{% endif %}
</p>
</div>
</div>
<div class="flex items-center space-x-6">
<div class="grid grid-cols-2 gap-6">
<div class="text-center">
<p class="text-2xl font-bold text-amber-400">{{ open_count|default:0 }}</p>
<p class="text-xs text-slate-400">{% trans 'Open Tickets' %}</p>
</div>
<div class="text-center">
<p class="text-2xl font-bold text-white">{{ total_count|default:0 }}</p>
<p class="text-xs text-slate-400">{% trans 'Total Tickets' %}</p>
</div>
</div>
<!-- New Ticket Button -->
<div class="htmx-stable">
{% url 'tickets:create' as create_ticket_url %}
{% trans "New Ticket" as bt1 %}{% button bt1 variant="primary" href=create_ticket_url responsive_text="New" size="sm" class="min-w-[80px] whitespace-nowrap" %}
</div>
</div>
</div>
</div>

<!-- Mobile Layout -->
<div class="md:hidden p-4">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
{% icon "tickets" css_class="text-white" %}
</div>
<div>
<h1 class="text-lg font-bold text-white">
{% if user.is_staff %}
{% trans "Tickets" %}
{% else %}
{% trans "Support Tickets" %}
{% endif %}
</h1>
</div>
</div>
<!-- New Ticket Button -->
<div class="htmx-stable">
{% trans "New Ticket" as bt2 %}{% button bt2 variant="primary" href=create_ticket_url responsive_text="New" size="sm" class="min-w-[60px] flex-shrink-0 whitespace-nowrap" %}
</div>
</div>

<!-- Mobile Stats -->
<div class="grid grid-cols-2 gap-3">
<div class="text-center py-2">
<p class="text-lg font-bold text-amber-400">{{ open_count|default:0 }}</p>
<p class="text-xs text-slate-400">{% trans 'Open' %}</p>
</div>
<div class="text-center py-2">
<p class="text-lg font-bold text-white">{{ total_count|default:0 }}</p>
<p class="text-xs text-slate-400">{% trans 'Total' %}</p>
</div>
</div>
</div>
</div>

<!-- Messages -->
{% if messages %}
<div class="space-y-2">
{% for message in messages %}
<div class="bg-green-500/10 border border-green-500/20 text-green-300 px-4 py-3 rounded-lg text-sm">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}

<!-- Filters -->
<div class="bg-slate-800/50 border border-slate-700 rounded-lg p-4">
<form hx-get="{% url 'tickets:search_htmx' %}"
hx-target="#tickets-container"
hx-trigger="keyup changed delay:600ms from:#search, change from:select[name='status']"
hx-sync="this:replace"
hx-params="q,status"
hx-indicator="#loading-indicator"
class="flex items-center space-x-4">
<div class="flex-1">
<label for="search" class="sr-only">{% trans 'Search' %}</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
{% icon "search" css_class="text-slate-400" %}
</div>
<input type="text" id="search" name="q" value="{{ search_query|default:'' }}" autocomplete="off"
class="block w-full pl-10 pr-3 py-2 border border-slate-600 rounded-lg bg-slate-700 text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="{% if user.is_staff %}{% trans 'Search by number, title or customer...' %}{% else %}{% trans 'Search' %}{% endif %}">
</div>
</div>
<select name="status"
class="px-3 py-2 border border-slate-600 rounded-lg bg-slate-700 text-white focus:outline-none focus:ring-2 focus:ring-primary">
<option value="">{% trans 'All statuses' %}</option>
<option value="open"{% if status_filter == 'open' %} selected{% endif %}>{% trans 'Open' %}</option>
<option value="in_progress"{% if status_filter == 'in_progress' %} selected{% endif %}>{% trans 'In Progress' %}</option>
<option value="waiting_on_customer"{% if status_filter == 'waiting_on_customer' %} selected{% endif %}>{% trans 'Waiting on Customer' %}</option>
<option value="closed"{% if status_filter == 'closed' %} selected{% endif %}>{% trans 'Closed' %}</option>
</select>
</form>
</div>
{# Shared Header Card #}
{% include "components/list_page_header.html" with list_action_include="tickets/partials/header_action.html" %}

<!-- Loading Indicator with Skeleton -->
<div id="loading-indicator" class="htmx-indicator hidden">
<!-- Desktop Skeleton -->
<div class="hidden md:block bg-slate-800/50 border border-slate-700 rounded-lg overflow-hidden">
<div class="animate-pulse">
<div class="bg-slate-800 px-6 py-3">
<div class="grid {% if user.is_staff %}grid-cols-5{% else %}grid-cols-4{% endif %} gap-4">
<div class="h-4 bg-slate-600 rounded"></div>
<div class="h-4 bg-slate-600 rounded"></div>
{% if user.is_staff %}<div class="h-4 bg-slate-600 rounded"></div>{% endif %}
<div class="h-4 bg-slate-600 rounded"></div>
<div class="h-4 bg-slate-600 rounded"></div>
</div>
</div>
<div class="divide-y divide-slate-700">
{% for i in "123456" %}
<div class="px-6 py-4 flex items-center space-x-4">
<div class="flex-shrink-0">
<div class="h-10 w-10 bg-slate-600 rounded-full"></div>
</div>
<div class="flex-1 space-y-2">
<div class="h-4 bg-slate-600 rounded w-3/4"></div>
<div class="h-3 bg-slate-700 rounded w-1/2"></div>
</div>
<div class="h-6 w-16 bg-slate-600 rounded-full"></div>
<div class="h-6 w-16 bg-slate-600 rounded-full"></div>
<div class="h-4 w-20 bg-slate-700 rounded"></div>
</div>
{% endfor %}
</div>
</div>
</div>

<!-- Mobile Skeleton -->
<div class="sm:hidden divide-y divide-slate-700">
<div class="animate-pulse">
{% for i in "123456" %}
<div class="min-h-[72px] flex flex-col justify-center p-4">
<div class="flex items-center justify-between mb-2">
<div class="h-4 bg-slate-600 rounded w-24"></div>
<div class="h-6 w-16 bg-slate-600 rounded-full"></div>
</div>
<div class="mb-2">
<div class="h-4 bg-slate-600 rounded w-full mb-1"></div>
<div class="h-4 bg-slate-700 rounded w-3/4"></div>
</div>
<div class="flex items-center justify-between text-sm">
<div class="h-3 w-16 bg-slate-700 rounded"></div>
</div>
</div>
{% endfor %}
</div>
</div>

<!-- Loading Text -->
<div class="flex items-center justify-center p-4 bg-slate-800/30 rounded-b-lg">
{% spinner size="md" css_class="mr-2" %}
<span class="text-slate-400 text-sm">{% trans 'Loading tickets...' %}</span>
</div>
</div>
{# Shared Filters with Tabs + Search #}
{% include "components/list_page_filters.html" with filter_tab_param="status" %}

<!-- Tickets List Container with inline skeleton -->
{# Tickets List Container #}
<div id="tickets-container" class="relative">
<!-- Inline Loading Skeleton that replaces content during loading -->
<div id="tickets-skeleton" class="htmx-indicator absolute inset-0 z-10 bg-slate-900">
<!-- Desktop Skeleton -->
<div class="hidden md:block w-full h-full">
<div class="w-full bg-slate-800/50 border border-slate-700 rounded-lg overflow-hidden h-full">
<div class="animate-pulse h-full">
<div class="bg-slate-800 px-6 py-3">
<div class="grid {% if user.is_staff %}grid-cols-5{% else %}grid-cols-4{% endif %} gap-4">
<div class="h-4 bg-slate-600 rounded"></div>
<div class="h-4 bg-slate-600 rounded"></div>
{% if user.is_staff %}<div class="h-4 bg-slate-600 rounded"></div>{% endif %}
<div class="h-4 bg-slate-600 rounded"></div>
<div class="h-4 bg-slate-600 rounded"></div>
</div>
</div>
<div class="divide-y divide-slate-700">
{% for i in "123456" %}
<div class="px-6 py-4 flex items-center space-x-4">
<div class="flex-shrink-0">
<div class="h-10 w-10 bg-slate-600 rounded-full"></div>
</div>
<div class="flex-1 space-y-2">
<div class="h-4 bg-slate-600 rounded w-3/4"></div>
<div class="h-3 bg-slate-700 rounded w-1/2"></div>
</div>
<div class="h-6 w-16 bg-slate-600 rounded-full"></div>
<div class="h-6 w-16 bg-slate-600 rounded-full"></div>
<div class="h-4 w-20 bg-slate-700 rounded"></div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>

<!-- Mobile Skeleton -->
<div class="sm:hidden w-full h-full">
<div class="w-full bg-slate-800/50 border border-slate-700 rounded-lg overflow-hidden h-full">
<div class="animate-pulse divide-y divide-slate-700">
{% for i in "123456" %}
<div class="min-h-[72px] flex flex-col justify-center p-4">
<div class="flex items-center justify-between mb-2">
<div class="h-4 bg-slate-600 rounded w-24"></div>
<div class="h-6 w-16 bg-slate-600 rounded-full"></div>
</div>
<div class="mb-2">
<div class="h-4 bg-slate-600 rounded w-full mb-1"></div>
<div class="h-4 bg-slate-700 rounded w-3/4"></div>
</div>
<div class="flex items-center justify-between text-sm">
<div class="h-3 w-16 bg-slate-700 rounded"></div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{# Shared Skeleton Loader #}
{% include "components/list_page_skeleton.html" with skeleton_id="tickets-skeleton" skeleton_cols=5 skeleton_rows=6 skeleton_has_avatar=False %}

<!-- Content that will be swapped by HTMX -->
{# Content (swapped by HTMX) #}
<div id="tickets-content">
{% include 'tickets/partials/tickets_table.html' %}
</div>
</div>
</div>

<style>
/* HTMX Loading Indicator Styles */
.htmx-indicator {
display: none;
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
display: block;
}
</style>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% load i18n %}
{% url 'tickets:create' as create_ticket_url %}
<a href="{{ create_ticket_url }}" class="inline-flex items-center px-3 sm:px-4 py-2 text-sm font-medium rounded-md border border-transparent bg-blue-600 text-white shadow-sm hover:bg-blue-700 min-w-[80px] whitespace-nowrap">
{% trans "New Ticket" %}
</a>
Loading
Loading