{% set page_title = 'Tickets | HoneyBee' %}
{% include '@Application/inc/central_header.html.twig' %}
{% include '@CompanyGroup/pages/admin/_sidebar.html.twig' %}
<style>
/* ── Design tokens ─────────────────────────────────────────────────────────── */
:root {
--n-cream: #F7F5F0;
--n-cream-2: #F0EDE5;
--n-white: #FFFFFF;
--n-dark: #1A1D2E;
--n-dark-2: #252840;
--n-amber: #C07D2A;
--n-amber-lt: #D4954A;
--n-amber-dim: rgba(192,125,42,.10);
--n-sage: #3D6B52;
--n-sage-dim: rgba(61,107,82,.09);
--n-muted: #6B6E7F;
--n-muted-2: #9395A5;
--n-border: rgba(26,29,46,.07);
--n-border-md: rgba(26,29,46,.12);
--n-shadow-sm: 0 2px 12px rgba(26,29,46,.07);
--n-shadow-md: 0 8px 32px rgba(26,29,46,.09);
--n-radius: 12px;
--n-radius-sm: 8px;
--n-font: 'DM Sans', 'Poppins', system-ui, sans-serif;
}
*, *::before, *::after { box-sizing: border-box; }
body { background: var(--n-cream); font-family: var(--n-font); color: var(--n-dark); text-align: left; }
/* ── Layout ────────────────────────────────────────────────────────────────── */
.t-wrap { max-width: 1100px; margin: 0 auto; padding: 0 28px; }
.t-page { padding: 40px 0 80px; }
/* ── Page header ───────────────────────────────────────────────────────────── */
.t-page-header {
display: flex; align-items: flex-end; justify-content: space-between;
flex-wrap: wrap; gap: 16px; margin-bottom: 28px;
padding-bottom: 24px; border-bottom: 1px solid var(--n-border-md);
}
.t-eyebrow {
display: inline-flex; align-items: center; gap: 8px;
font-size: 11px; font-weight: 700; letter-spacing: .18em; text-transform: uppercase;
color: var(--n-amber); margin-bottom: 8px;
}
.t-eyebrow::before { content:''; width:18px; height:1.5px; background:currentColor; border-radius:2px; }
.t-page-title {
font-family: 'Montserrat', sans-serif; font-size: clamp(1.4rem,2.5vw,2rem);
font-weight: 800; color: var(--n-dark); letter-spacing: -.02em; margin: 0 0 4px;
}
.t-page-sub { font-size: .88rem; color: var(--n-muted); margin: 0; }
/* ── Summary chips ─────────────────────────────────────────────────────────── */
.t-summary-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px; }
.t-chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 5px 14px; border-radius: 100px;
font-size: .75rem; font-weight: 700; letter-spacing: .03em;
border: 1px solid transparent;
}
.t-chip-open { background: rgba(192,125,42,.10); color: var(--n-amber); border-color: rgba(192,125,42,.2); }
.t-chip-progress { background: rgba(26,29,46,.06); color: var(--n-dark); border-color: var(--n-border-md); }
.t-chip-resolved { background: var(--n-sage-dim); color: var(--n-sage); border-color: rgba(61,107,82,.2); }
.t-chip-closed { background: rgba(26,29,46,.04); color: var(--n-muted); border-color: var(--n-border); }
.t-chip .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
/* ── Filter bar ────────────────────────────────────────────────────────────── */
.t-filter-bar {
background: var(--n-white); border: 1px solid var(--n-border-md);
border-radius: var(--n-radius); padding: 18px 20px; margin-bottom: 20px;
box-shadow: var(--n-shadow-sm);
}
.t-filter-bar .form-select,
.t-filter-bar .form-control {
border-radius: var(--n-radius-sm); font-size: .875rem;
border: 1.5px solid var(--n-border-md); background: var(--n-cream);
color: var(--n-dark);
}
.t-filter-bar .form-select:focus,
.t-filter-bar .form-control:focus {
border-color: var(--n-amber);
box-shadow: 0 0 0 3px var(--n-amber-dim);
}
.t-filter-bar label { font-size: .78rem; font-weight: 600; color: var(--n-muted); margin-bottom: 4px; display: block; }
/* ── Ticket card ───────────────────────────────────────────────────────────── */
.t-card {
background: var(--n-white); border: 1px solid var(--n-border-md);
border-radius: var(--n-radius); padding: 16px 20px;
margin-bottom: 10px; display: flex; align-items: flex-start; gap: 16px;
transition: box-shadow .2s, border-color .2s;
}
.t-card:hover { box-shadow: var(--n-shadow-md); border-color: rgba(192,125,42,.22); }
.t-card-icon { padding-top: 3px; flex-shrink: 0; font-size: 1.1rem; }
.t-card-icon.emergency { color: #c0392b; }
.t-card-icon.important { color: var(--n-amber); }
.t-card-icon.normal { color: var(--n-muted-2); }
.t-card-body { flex: 1; min-width: 0; }
.t-card-pills { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; }
.t-pill {
font-family: monospace; font-size: .65rem; font-weight: 700;
letter-spacing: .06em; text-transform: uppercase;
padding: 3px 9px; border-radius: 100px; display: inline-block;
}
.t-pill-num { background: var(--n-amber-dim); color: var(--n-amber); }
.t-pill-open { background: var(--n-amber-dim); color: var(--n-amber); }
.t-pill-progress { background: rgba(26,29,46,.07); color: var(--n-dark); }
.t-pill-resolved { background: var(--n-sage-dim); color: var(--n-sage); }
.t-pill-closed { background: rgba(26,29,46,.04); color: var(--n-muted-2); }
.t-pill-emerg { background: rgba(192,57,43,.09); color: #c0392b; }
.t-pill-import { background: rgba(192,125,42,.10);color: var(--n-amber); }
.t-pill-normal { background: rgba(26,29,46,.05); color: var(--n-muted); }
.t-pill-system { background: rgba(111,66,193,.09);color: #6f42c1; }
.t-pill-manual { background: var(--n-sage-dim); color: var(--n-sage); }
.t-pill-api { background: rgba(26,29,46,.06); color: var(--n-muted-2); }
.t-count-chip {
display: inline-flex; align-items: center; gap: 4px;
font-size: .72rem; color: var(--n-amber); background: var(--n-amber-dim);
border-radius: 50px; padding: 2px 9px; font-weight: 700; font-family: monospace;
}
.t-card-title { font-size: .95rem; font-weight: 600; color: var(--n-dark); margin-bottom: 4px; }
.t-card-meta { font-size: .8rem; color: var(--n-muted-2); display: flex; gap: 16px; flex-wrap: wrap; }
.t-card-meta .unassigned { color: #c0392b; }
.t-card-actions { flex-shrink: 0; display: flex; flex-direction: column; gap: 4px; text-align: right; }
.t-card-actions a {
font-size: .78rem; font-weight: 600; color: var(--n-muted);
text-decoration: none; padding: 4px 10px; border-radius: 6px;
border: 1px solid var(--n-border-md); background: var(--n-cream);
white-space: nowrap; transition: all .15s;
}
.t-card-actions a:hover { border-color: var(--n-amber); color: var(--n-amber); background: var(--n-amber-dim); }
/* ── Buttons ───────────────────────────────────────────────────────────────── */
.t-btn-primary {
display: inline-flex; align-items: center; gap: 7px;
padding: 10px 22px; background: var(--n-dark); color: #fff;
border: none; border-radius: var(--n-radius-sm);
font-family: var(--n-font); font-size: .88rem; font-weight: 700;
cursor: pointer; text-decoration: none; transition: background .18s;
}
.t-btn-primary:hover { background: var(--n-amber); color: #fff; }
.t-btn-ghost {
display: inline-flex; align-items: center; gap: 6px;
padding: 10px 16px; background: transparent; color: var(--n-muted);
border: 1.5px solid var(--n-border-md); border-radius: var(--n-radius-sm);
font-family: var(--n-font); font-size: .88rem; font-weight: 600;
cursor: pointer; text-decoration: none; transition: all .18s;
}
.t-btn-ghost:hover { border-color: var(--n-dark); color: var(--n-dark); }
/* ── Empty state ───────────────────────────────────────────────────────────── */
.t-empty {
text-align: center; padding: 72px 0; color: var(--n-muted-2);
}
.t-empty-icon { font-size: 2.4rem; margin-bottom: 16px; opacity: .4; }
.t-empty p { font-size: .95rem; margin-bottom: 20px; }
/* ── Pagination ────────────────────────────────────────────────────────────── */
.t-pagination { display: flex; justify-content: center; gap: 4px; margin-top: 28px; flex-wrap: wrap; }
.t-pagination a, .t-pagination span {
display: inline-flex; align-items: center; justify-content: center;
width: 36px; height: 36px; border-radius: var(--n-radius-sm);
font-size: .85rem; font-weight: 600; text-decoration: none;
border: 1px solid var(--n-border-md); color: var(--n-muted);
background: var(--n-white); transition: all .15s;
}
.t-pagination a:hover { border-color: var(--n-amber); color: var(--n-amber); }
.t-pagination .active { background: var(--n-dark); color: #fff; border-color: var(--n-dark); }
</style>
<div class="hb-admin-layout">
<div class="t-page">
<div class="t-wrap">
{# ── Page header ── #}
<div class="t-page-header">
<div>
<div class="t-eyebrow">Support</div>
<h1 class="t-page-title">Tickets</h1>
<p class="t-page-sub">Manage and track all support requests across your workspace</p>
</div>
<a href="{{ path('ticket_create') }}" class="t-btn-primary">
<i class="fa fa-plus" style="font-size:.75rem"></i> New Ticket
</a>
</div>
{# ── Summary chips ── #}
<div class="t-summary-row">
<span class="t-chip t-chip-open">
<span class="dot"></span> Open <b>{{ summary.open ?? 0 }}</b>
</span>
<span class="t-chip t-chip-progress">
<span class="dot"></span> In Progress <b>{{ summary.inProgress ?? 0 }}</b>
</span>
<span class="t-chip t-chip-resolved">
<span class="dot"></span> Resolved <b>{{ summary.resolved ?? 0 }}</b>
</span>
<span class="t-chip t-chip-closed">
<span class="dot"></span> Closed <b>{{ summary.closed ?? 0 }}</b>
</span>
</div>
{# ── Filter bar ── #}
<form method="GET" action="{{ path('ticket_list') }}" class="t-filter-bar">
<div class="row g-2 align-items-end">
<div class="col-12 col-md-3">
<label>Status</label>
<select name="status" class="form-select form-select-sm">
<option value="">All Statuses</option>
<option value="1" {{ filters.status == 1 ? 'selected' }}>Open</option>
<option value="2" {{ filters.status == 2 ? 'selected' }}>In Progress</option>
<option value="3" {{ filters.status == 3 ? 'selected' }}>Resolved</option>
<option value="4" {{ filters.status == 4 ? 'selected' }}>Closed</option>
</select>
</div>
<div class="col-12 col-md-2">
<label>Priority</label>
<select name="priority" class="form-select form-select-sm">
<option value="">All Priorities</option>
<option value="1" {{ filters.priority == 1 ? 'selected' }}>Emergency</option>
<option value="2" {{ filters.priority == 2 ? 'selected' }}>Important</option>
<option value="3" {{ filters.priority == 3 ? 'selected' }}>Normal</option>
</select>
</div>
<div class="col-12 col-md-2">
<label>Source</label>
<select name="source" class="form-select form-select-sm">
<option value="">All Sources</option>
<option value="1" {{ filters.source == 1 ? 'selected' }}>Manual</option>
<option value="2" {{ filters.source == 2 ? 'selected' }}>System</option>
<option value="3" {{ filters.source == 3 ? 'selected' }}>API</option>
</select>
</div>
<div class="col-12 col-md-3">
<label>Search</label>
<input type="text" name="q" class="form-control form-control-sm"
placeholder="Ticket number or keyword…"
value="{{ filters.q ?? '' }}">
</div>
<div class="col-12 col-md-2 d-flex gap-2">
<button type="submit" class="t-btn-primary w-100" style="padding:8px 12px;justify-content:center;">
<i class="fa fa-filter" style="font-size:.75rem"></i> Filter
</button>
<a href="{{ path('ticket_list') }}" class="t-btn-ghost" style="padding:8px 12px;">
<i class="fa fa-times" style="font-size:.75rem"></i>
</a>
</div>
</div>
</form>
{# ── Ticket list ── #}
{% if tickets is defined and tickets|length > 0 %}
{% for ticket in tickets %}
{% set statusLabel = {1: 'Open', 2: 'In Progress', 3: 'Resolved', 4: 'Closed'} %}
{% set statusPill = {1: 't-pill-open', 2: 't-pill-progress', 3: 't-pill-resolved', 4: 't-pill-closed'} %}
{% set priorityLabel = {1: 'Emergency', 2: 'Important', 3: 'Normal'} %}
{% set priorityPill = {1: 't-pill-emerg', 2: 't-pill-import', 3: 't-pill-normal'} %}
{% set iconClass = {1: 'emergency', 2: 'important', 3: 'normal'} %}
<div class="t-card">
{# Priority icon #}
<div class="t-card-icon {{ iconClass[ticket.urgency] ?? 'normal' }}">
{% if ticket.urgency == 1 %}
<i class="fa fa-exclamation-circle"></i>
{% elseif ticket.urgency == 2 %}
<i class="fa fa-exclamation-triangle"></i>
{% else %}
<i class="fa fa-ticket-alt"></i>
{% endif %}
</div>
{# Main body #}
<div class="t-card-body">
<div class="t-card-pills">
<span class="t-pill t-pill-num">{{ ticket.ticketNumber }}</span>
<span class="t-pill {{ statusPill[ticket.status] ?? '' }}">{{ statusLabel[ticket.status] ?? 'Unknown' }}</span>
<span class="t-pill {{ priorityPill[ticket.urgency] ?? 't-pill-normal' }}">{{ priorityLabel[ticket.urgency] ?? '' }}</span>
{% if ticket.systemCreated %}
<span class="t-pill t-pill-system"><i class="fa fa-robot fa-xs"></i> System</span>
{% else %}
<span class="t-pill t-pill-manual"><i class="fa fa-user fa-xs"></i> Manual</span>
{% endif %}
{% if ticket.reportCount > 1 %}
<span class="t-count-chip">
<i class="fa fa-redo fa-xs"></i> {{ ticket.reportCount }}× reported
</span>
{% endif %}
</div>
<div class="t-card-title">{{ ticket.title ?? '(No title)' }}</div>
<div class="t-card-meta">
<span><i class="fa fa-calendar-alt fa-xs" style="margin-right:4px"></i>{{ ticket.createdAt ? ticket.createdAt|date('d M Y') : '—' }}</span>
{% if ticket.deadlineDate %}
<span><i class="fa fa-clock fa-xs" style="margin-right:4px"></i>Due: {{ ticket.deadlineDate|date('d M Y') }}</span>
{% endif %}
{% if ticket.assignedToUserId %}
<span><i class="fa fa-user-check fa-xs" style="margin-right:4px"></i>Assigned: #{{ ticket.assignedToUserId }}</span>
{% else %}
<span class="unassigned"><i class="fa fa-user-times fa-xs" style="margin-right:4px"></i>Unassigned</span>
{% endif %}
{% if ticket.serverIdentifier %}
<span><i class="fa fa-server fa-xs" style="margin-right:4px"></i>{{ ticket.serverIdentifier }}</span>
{% endif %}
</div>
</div>
{# Actions #}
<div class="t-card-actions">
<a href="{{ path('ticket_view', {id: ticket.id}) }}">
<i class="fa fa-eye fa-xs"></i> View
</a>
<a href="{{ path('ticket_edit', {id: ticket.id}) }}">
<i class="fa fa-edit-alt fa-xs"></i> Edit
</a>
</div>
</div>
{% endfor %}
{% else %}
<div class="t-empty">
<div class="t-empty-icon"><i class="fa fa-inbox"></i></div>
<p>No tickets found matching your filters.</p>
<a href="{{ path('ticket_create') }}" class="t-btn-primary">
<i class="fa fa-plus" style="font-size:.75rem"></i> Create First Ticket
</a>
</div>
{% endif %}
{# ── Pagination ── #}
{% if totalPages is defined and totalPages > 1 %}
<div class="t-pagination">
{% for p in 1..totalPages %}
{% if p == currentPage %}
<span class="active">{{ p }}</span>
{% else %}
<a href="{{ path('ticket_list', filters|merge({page: p})) }}">{{ p }}</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>{# /hb-admin-layout #}
{% include '@Application/footer/central_footer.html.twig' %}