feat(fusion_helpdesk): group My Tickets into Critical/New/Solved sections

The flat write_date-sorted list was hard to scan with 50+ tickets — solved
ones were intermixed with active ones, and there was no signal for
priority. Bucket each ticket server-side into 'critical' (open + priority
High/Urgent), 'solved' (stage marked fold=True on central) or 'open'
(everything else), and render three labelled sections in the dialog with
sticky headers, count badges, and per-group accent colours. Backend keeps
its write_date desc order so latest is always at top within each bucket.

Bucketing uses helpdesk.stage.fold (not the stage name) so renaming
"Solved" to "Done" on the central won't quietly mis-categorise rows.
Adds bucket_ticket() in utils.py with unit tests covering the
folded-wins-over-priority precedence and the missing-priority fallback.

Also surfaces a small Urgent (triangle) / High (arrow) icon on each row
so a critical ticket reads at a glance even after a user scrolls past
the section header.

Bumps fusion_helpdesk to 19.0.1.6.0.
This commit is contained in:
gsinghpal
2026-05-27 11:04:31 -04:00
parent aabfc1afe7
commit 3e5ced1655
7 changed files with 172 additions and 10 deletions

View File

@@ -124,6 +124,29 @@ export class FusionHelpdeskDialog extends Component {
await this.loadList();
}
// Bucket tickets into Critical / New & Open / Solved for the section view.
// Backend already sorts by write_date desc, so iterating preserves latest-
// first within each bucket. We only render non-empty sections so the
// dialog doesn't show "0 Critical" noise when the user has none.
get groupedTickets() {
const groups = [
{ key: "critical", title: _t("Critical"),
iconClass: "fa fa-exclamation-circle", className: "o_fhd_group_critical",
tickets: [] },
{ key: "open", title: _t("New & Open"),
iconClass: "fa fa-inbox", className: "o_fhd_group_open",
tickets: [] },
{ key: "solved", title: _t("Solved"),
iconClass: "fa fa-check-circle", className: "o_fhd_group_solved",
tickets: [] },
];
const byKey = Object.fromEntries(groups.map((g) => [g.key, g]));
for (const t of this.state.tickets) {
(byKey[t.group] || byKey.open).tickets.push(t);
}
return groups.filter((g) => g.tickets.length);
}
// ==================================================================
// My Tickets — thread
// ==================================================================

View File

@@ -226,6 +226,62 @@ $fhd-accent: var(--fhd-accent, $_fhd-accent-hex);
margin-bottom: 0.85rem;
}
// Ticket groups (Critical / New & Open / Solved) — render a stack of
// labelled sections; backend already sorts each by latest-first.
.o_fhd_ticket_groups {
display: flex;
flex-direction: column;
gap: 0.85rem;
max-height: 60vh;
overflow-y: auto;
padding-right: 4px; // breathing room beside the scrollbar
}
.o_fhd_group_header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.1rem;
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.04em;
position: sticky;
top: 0;
z-index: 1;
background-color: $fhd-bg;
border-bottom: 1px solid $fhd-border;
}
.o_fhd_group_count {
margin-left: auto;
font-size: 0.75rem;
font-weight: 600;
background-color: $fhd-hover;
border: 1px solid $fhd-border;
color: $fhd-muted;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-variant-numeric: tabular-nums;
}
// Per-group accent colours on the header — kept subtle so the list rows
// stay the visual focus. The Critical icon is the only saturated colour.
.o_fhd_group_critical .o_fhd_group_header { color: #d9534f; }
.o_fhd_group_critical .o_fhd_group_header > i { color: #d9534f; }
.o_fhd_group_open .o_fhd_group_header > i { color: $fhd-accent; }
.o_fhd_group_solved .o_fhd_group_header { color: $fhd-muted; }
.o_fhd_group_solved .o_fhd_group_header > i { color: #4CAF50; }
// Priority indicator inline in the row — Urgent + High only; Low/Normal
// get nothing so 90% of rows stay quiet.
.o_fhd_priority_urgent, .o_fhd_priority_high {
flex: 0 0 auto;
font-size: 0.85rem;
}
.o_fhd_priority_urgent { color: #d9534f; }
.o_fhd_priority_high { color: #f0ad4e; }
// Ticket list
.o_fhd_ticket_list {
display: flex;

View File

@@ -138,14 +138,30 @@
<i class="fa fa-inbox fa-2x d-block mb-2"/>
No tickets yet. Use the <b>New</b> tab to report a bug or request a feature.
</div>
<div t-else="" class="o_fhd_ticket_list">
<div t-foreach="state.tickets" t-as="t" t-key="t.id"
class="o_fhd_ticket_row" t-on-click="() => this.openTicket(t.id)">
<span t-if="t.has_unread" class="o_fhd_unread_dot" title="New reply"/>
<span t-else="" class="o_fhd_unread_spacer"/>
<span class="o_fhd_ticket_ref" t-esc="'#' + t.ref"/>
<span class="o_fhd_ticket_subject" t-esc="t.subject"/>
<span class="o_fhd_ticket_stage" t-esc="t.stage"/>
<div t-else="" class="o_fhd_ticket_groups">
<div t-foreach="groupedTickets" t-as="group" t-key="group.key"
t-attf-class="o_fhd_group {{ group.className }}">
<div class="o_fhd_group_header">
<i t-att-class="group.iconClass"/>
<span class="o_fhd_group_title" t-esc="group.title"/>
<span class="o_fhd_group_count" t-esc="group.tickets.length"/>
</div>
<div class="o_fhd_ticket_list">
<div t-foreach="group.tickets" t-as="t" t-key="t.id"
class="o_fhd_ticket_row" t-on-click="() => this.openTicket(t.id)">
<span t-if="t.has_unread" class="o_fhd_unread_dot" title="New reply"/>
<span t-else="" class="o_fhd_unread_spacer"/>
<span class="o_fhd_ticket_ref" t-esc="'#' + t.ref"/>
<span class="o_fhd_ticket_subject" t-esc="t.subject"/>
<span t-if="t.priority === '3'" class="o_fhd_priority_urgent" title="Urgent">
<i class="fa fa-exclamation-triangle"/>
</span>
<span t-elif="t.priority === '2'" class="o_fhd_priority_high" title="High">
<i class="fa fa-arrow-up"/>
</span>
<span class="o_fhd_ticket_stage" t-esc="t.stage"/>
</div>
</div>
</div>
</div>
</div>