From 3e5ced16554286f5d2fb550ff4c130e31d0df7db Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 27 May 2026 11:04:31 -0400 Subject: [PATCH] feat(fusion_helpdesk): group My Tickets into Critical/New/Solved sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- fusion_helpdesk/__manifest__.py | 2 +- fusion_helpdesk/controllers/main.py | 23 +++++++- .../static/src/js/fusion_helpdesk_dialog.js | 23 ++++++++ .../static/src/scss/fusion_helpdesk.scss | 56 +++++++++++++++++++ .../static/src/xml/fusion_helpdesk_dialog.xml | 32 ++++++++--- fusion_helpdesk/tests/test_utils.py | 30 ++++++++++ fusion_helpdesk/utils.py | 16 ++++++ 7 files changed, 172 insertions(+), 10 deletions(-) diff --git a/fusion_helpdesk/__manifest__.py b/fusion_helpdesk/__manifest__.py index c05106a1..8f8c575b 100644 --- a/fusion_helpdesk/__manifest__.py +++ b/fusion_helpdesk/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Helpdesk Reporter', - 'version': '19.0.1.5.0', + 'version': '19.0.1.6.0', 'category': 'Productivity', 'summary': 'One-click in-app bug reporting & feature requesting — ' 'auto-creates a helpdesk.ticket on a central Odoo Helpdesk.', diff --git a/fusion_helpdesk/controllers/main.py b/fusion_helpdesk/controllers/main.py index 4e92c90a..4a7b98d8 100644 --- a/fusion_helpdesk/controllers/main.py +++ b/fusion_helpdesk/controllers/main.py @@ -26,6 +26,7 @@ from odoo.http import request from odoo.addons.fusion_helpdesk.utils import ( build_ticket_vals, build_scope_domain, + bucket_ticket, is_public_message, compute_unread_count, escape_like, @@ -418,6 +419,21 @@ class FusionHelpdeskController(http.Controller): 'again in a moment.') % {'code': e.errcode}, ) + def _stage_fold_map(self, cfg, tickets): + """{stage_id: fold_bool} for the distinct stages on these tickets. + + `helpdesk.stage.fold` is the central support team's signal for + "kanban-folded = closed" — Solved + Cancelled by default. We use it + (not the stage name) to bucket tickets into the Solved section, so + renaming a stage on central doesn't quietly mis-categorise rows. + One extra RPC per list call, batched across all distinct stages.""" + ids = sorted({t['stage_id'][0] for t in tickets if t.get('stage_id')}) + if not ids: + return {} + rows = self._rpc(cfg, 'helpdesk.stage', 'read', + [ids], {'fields': ['fold']}) + return {r['id']: r.get('fold', False) for r in rows} + def _internal_subtype_map(self, cfg, subtype_ids): """{subtype_id: internal_bool} so internal notes can be hidden.""" ids = [s for s in set(subtype_ids) if s] @@ -562,9 +578,10 @@ class FusionHelpdeskController(http.Controller): tickets = self._rpc( cfg, 'helpdesk.ticket', 'search_read', [domain], {'fields': ['id', 'name', 'stage_id', 'partner_id', - 'write_date', 'ticket_ref'], + 'write_date', 'ticket_ref', 'priority'], 'order': 'write_date desc', 'limit': 100}) msgs = self._ticket_messages(cfg, [t['id'] for t in tickets]) + stage_fold = self._stage_fold_map(cfg, tickets) except (_RemoteError, xmlrpc.client.Fault, OSError, ssl.SSLError) as e: return self._remote_failure(cfg, e) @@ -575,6 +592,7 @@ class FusionHelpdeskController(http.Controller): for t in tickets: rid = t['id'] ls = last_support.get(rid, 0) + sid = t['stage_id'][0] if t['stage_id'] else None rows.append({ 'id': rid, 'ref': t.get('ticket_ref') or str(rid), @@ -583,6 +601,9 @@ class FusionHelpdeskController(http.Controller): 'last_update': t['write_date'], 'last_support_msg_id': ls, 'has_unread': ls > (seen.get(rid, 0) or 0), + 'priority': t.get('priority') or '0', + 'group': bucket_ticket(stage_fold.get(sid, False), + t.get('priority')), }) return {'ok': True, 'tickets': rows, 'is_admin': ident['is_admin'], 'unread': compute_unread_count(rows, seen)} diff --git a/fusion_helpdesk/static/src/js/fusion_helpdesk_dialog.js b/fusion_helpdesk/static/src/js/fusion_helpdesk_dialog.js index fecd5cc1..490cff1c 100644 --- a/fusion_helpdesk/static/src/js/fusion_helpdesk_dialog.js +++ b/fusion_helpdesk/static/src/js/fusion_helpdesk_dialog.js @@ -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 // ================================================================== diff --git a/fusion_helpdesk/static/src/scss/fusion_helpdesk.scss b/fusion_helpdesk/static/src/scss/fusion_helpdesk.scss index 45f1036d..5659b9eb 100644 --- a/fusion_helpdesk/static/src/scss/fusion_helpdesk.scss +++ b/fusion_helpdesk/static/src/scss/fusion_helpdesk.scss @@ -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; diff --git a/fusion_helpdesk/static/src/xml/fusion_helpdesk_dialog.xml b/fusion_helpdesk/static/src/xml/fusion_helpdesk_dialog.xml index 5a45fec2..18c1bb8f 100644 --- a/fusion_helpdesk/static/src/xml/fusion_helpdesk_dialog.xml +++ b/fusion_helpdesk/static/src/xml/fusion_helpdesk_dialog.xml @@ -138,14 +138,30 @@ No tickets yet. Use the New tab to report a bug or request a feature. -
-
- - - - - +
+
+
+ + + +
+
+
+ + + + + + + + + + + +
+
diff --git a/fusion_helpdesk/tests/test_utils.py b/fusion_helpdesk/tests/test_utils.py index 3943b68e..610ec43f 100644 --- a/fusion_helpdesk/tests/test_utils.py +++ b/fusion_helpdesk/tests/test_utils.py @@ -12,6 +12,7 @@ from odoo.tests import TransactionCase, tagged from odoo.addons.fusion_helpdesk.utils import ( build_ticket_vals, build_scope_domain, + bucket_ticket, is_public_message, compute_unread_count, _norm_email, @@ -129,3 +130,32 @@ class TestNormEmail(TransactionCase): # every inbox endpoint. Guard that the name is resolvable there. from odoo.addons.fusion_helpdesk.controllers import main self.assertTrue(hasattr(main, '_norm_email')) + + +@tagged('post_install', '-at_install', 'fusion_helpdesk') +class TestBucketTicket(TransactionCase): + """Bucketing pins how the My Tickets inbox sections work — getting this + wrong used to scatter Solved tickets in with New ones, defeating the + whole point of the grouped view.""" + + def test_folded_stage_goes_to_solved(self): + # Folded wins even if priority is high — closed tickets aren't + # actionable so they belong in Solved regardless. + self.assertEqual(bucket_ticket(True, '0'), 'solved') + self.assertEqual(bucket_ticket(True, '3'), 'solved') + + def test_open_high_or_urgent_promotes_to_critical(self): + self.assertEqual(bucket_ticket(False, '2'), 'critical') + self.assertEqual(bucket_ticket(False, '3'), 'critical') + + def test_open_low_or_normal_is_just_open(self): + self.assertEqual(bucket_ticket(False, '0'), 'open') + self.assertEqual(bucket_ticket(False, '1'), 'open') + self.assertEqual(bucket_ticket(False, None), 'open') # missing -> normal + self.assertEqual(bucket_ticket(False, ''), 'open') + + def test_controller_namespace_resolves_bucket_ticket(self): + # Regression: the inbox controller imports bucket_ticket — if the + # name disappears from utils.py, every list call would 500. + from odoo.addons.fusion_helpdesk.controllers import main + self.assertTrue(hasattr(main, 'bucket_ticket')) diff --git a/fusion_helpdesk/utils.py b/fusion_helpdesk/utils.py index 1c0bcc02..fd3accc0 100644 --- a/fusion_helpdesk/utils.py +++ b/fusion_helpdesk/utils.py @@ -101,6 +101,22 @@ def is_public_message(msg): return not msg.get('subtype_is_internal', False) +def bucket_ticket(stage_is_folded, priority): + """Bucket key for the "My Tickets" inbox: 'critical' | 'solved' | 'open'. + + Folded stages (Solved, Cancelled — whatever the central support team has + marked as kanban-folded) collapse into the 'solved' bucket regardless of + priority, because a closed ticket is not actionable. For everything still + open, High and Urgent (priority '2'/'3') promote to 'critical' so the + reporter can find blockers without scrolling. Anything else is 'open'. + """ + if stage_is_folded: + return 'solved' + if (priority or '0') in ('2', '3'): + return 'critical' + return 'open' + + def compute_unread_count(tickets, seen_by_id): """Number of tickets with a support reply the user hasn't seen.