feat(fusion_helpdesk): Critical flag, KPI cards, colored stage pills

Three coordinated changes on top of the section grouping:

1. **Mark as Critical** — a red chip on the New tab sets priority='3'
   when submitted. The central post-create hook auto-applies a "Critical"
   helpdesk.tag (shipped via fusion_helpdesk_central data XML, noupdate=1
   so support can recolor without losing it on upgrade), giving support
   a kanban-groupable signal that doesn't rely on remembering what
   priority='3' means. Scoped to in-app-channel tickets only, so a
   support agent manually setting Urgent on their own ticket isn't
   silently tagged.

2. **KPI cards above the sections** — Total / Open / Closed / Critical
   in a 4-up grid (auto-collapses to 2x2 under 540px). Each card uses
   its own saturated gradient so it reads on both light and dark mode —
   the dialog backdrop is irrelevant because the gradient brings its
   own background. Counts are computed in JS from state.tickets so they
   always match what's rendered below.

3. **Colored stage pills** — red Critical, green Solved, dark-yellow New,
   orange Cancelled, blue for In Progress / Testing / On Hold. Critical
   priority gets a *separate* red pill alongside the stage pill so you
   keep stage info even on escalated tickets. Stage matching is
   substring-based (lowercased) so a renamed "Resolved" or "Done" stage
   on central still maps to the green pill.

Tests cover the new is_critical=True → priority='3' wiring and the
default omission so SLA / stage defaults keep working for normal
tickets. Bumps fusion_helpdesk to 19.0.1.7.0 and
fusion_helpdesk_central to 19.0.1.2.0. End-to-end smoke test verified
live: priority=3 + x_fc_client_label triggers the Critical tag.
This commit is contained in:
gsinghpal
2026-05-27 11:21:11 -04:00
parent 3e5ced1655
commit d7ec91b0f1
10 changed files with 300 additions and 10 deletions

View File

@@ -39,6 +39,7 @@ export class FusionHelpdeskDialog extends Component {
errorCode: "",
replyEmail: user.login || "",
attachments: [],
isCritical: false,
capturing: false,
submitting: false,
error: "",
@@ -147,6 +148,38 @@ export class FusionHelpdeskDialog extends Component {
return groups.filter((g) => g.tickets.length);
}
// KPI stats — same source as the section list so counts always agree
// with what the user sees scrolling below. "Open" rolls Critical + Open
// groups together (anything not folded); "Closed" is the Solved bucket.
get ticketStats() {
const t = this.state.tickets;
const critical = t.filter((x) => x.group === "critical").length;
const open = t.filter((x) => x.group === "critical" || x.group === "open").length;
const closed = t.filter((x) => x.group === "solved").length;
return { total: t.length, open, closed, critical };
}
// Colour pill class per ticket. Critical priority shows as a dedicated red
// pill alongside the stage pill (rendered in the template), so this only
// needs to map the stage name to a hue. Match on substring (lowercased) so
// a renamed "Resolved" / "Done" stage on central still maps to green.
pillClass(t) {
const s = (t.stage || "").toLowerCase();
if (s.includes("cancel")) return "o_fhd_pill o_fhd_pill_cancelled";
if (s.includes("solv") || s.includes("done") || s.includes("resolv")) {
return "o_fhd_pill o_fhd_pill_solved";
}
if (s === "new") return "o_fhd_pill o_fhd_pill_new";
if (s.includes("progress") || s.includes("test") || s.includes("hold")) {
return "o_fhd_pill o_fhd_pill_progress";
}
return "o_fhd_pill"; // unknown stage falls back to neutral chip
}
isCriticalRow(t) {
return t.priority === "2" || t.priority === "3";
}
// ==================================================================
// My Tickets — thread
// ==================================================================
@@ -369,6 +402,7 @@ export class FusionHelpdeskDialog extends Component {
description: this.state.description || "",
error_code: this.state.kind === "bug" ? this.state.errorCode || "" : "",
reply_email: (this.state.replyEmail || "").trim(),
is_critical: !!this.state.isCritical,
attachments: this.state.attachments.map((a) => ({
name: a.name,
mimetype: a.mimetype,
@@ -390,6 +424,7 @@ export class FusionHelpdeskDialog extends Component {
this.state.description = "";
this.state.errorCode = "";
this.state.attachments = [];
this.state.isCritical = false;
}
} catch (err) {
console.error("fusion_helpdesk: submit failed", err);

View File

@@ -337,6 +337,139 @@ $fhd-accent: var(--fhd-accent, $_fhd-accent-hex);
color: $fhd-muted;
}
// Colour pills — readable on both light and dark mode because each pill
// carries its own background (saturated brand colour) + white text. The
// 1px border darkens to keep the edge crisp on both backdrops.
.o_fhd_pill {
flex: 0 0 auto;
font-size: 0.75rem;
font-weight: 600;
padding: 0.15rem 0.55rem;
border-radius: 10px;
background-color: $fhd-hover;
border: 1px solid $fhd-border;
color: $fhd-muted;
display: inline-flex;
align-items: center;
line-height: 1.2;
}
.o_fhd_pill_critical {
background: linear-gradient(135deg, #e85d68 0%, #dc3545 100%);
border-color: #b02a37;
color: #fff;
}
.o_fhd_pill_solved {
background: linear-gradient(135deg, #5cc66f 0%, #28a745 100%);
border-color: #1e7e34;
color: #fff;
}
.o_fhd_pill_new {
// "Dark Yellow" — closer to amber/mustard so it sits well next to red/green.
background: linear-gradient(135deg, #e9b949 0%, #c79100 100%);
border-color: #a07700;
color: #fff;
}
.o_fhd_pill_cancelled {
background: linear-gradient(135deg, #ff9a3c 0%, #fd7e14 100%);
border-color: #c85a00;
color: #fff;
}
.o_fhd_pill_progress {
background: linear-gradient(135deg, #4ea3ff 0%, #2c89e9 100%);
border-color: #1e6bbf;
color: #fff;
}
// KPI cards — Total / Open / Closed / Critical. Each card uses its own
// saturated gradient so they read on both light and dark mode (the card
// brings its own background, the dialog backdrop doesn't matter).
.o_fhd_kpi_grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.6rem;
margin-bottom: 1rem;
@media (max-width: 540px) {
grid-template-columns: repeat(2, 1fr);
}
}
.o_fhd_kpi_card {
padding: 0.7rem 0.85rem;
border-radius: 10px;
color: #fff;
display: flex;
flex-direction: column;
gap: 0.15rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
position: relative;
overflow: hidden;
// Subtle inner highlight on top so the gradient reads as raised, not flat.
&::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.10) 0%, rgba(255, 255, 255, 0) 50%);
pointer-events: none;
}
}
.o_fhd_kpi_label {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.9;
position: relative;
z-index: 1;
}
.o_fhd_kpi_value {
font-size: 1.7rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1.05;
position: relative;
z-index: 1;
}
.o_fhd_kpi_total { background: linear-gradient(135deg, #6fb8ff 0%, #2c89e9 60%, #1e6bbf 100%); }
.o_fhd_kpi_open { background: linear-gradient(135deg, #f2c75c 0%, #d4a017 60%, #a07700 100%); }
.o_fhd_kpi_closed { background: linear-gradient(135deg, #6cd17f 0%, #28a745 60%, #1e7e34 100%); }
.o_fhd_kpi_critical { background: linear-gradient(135deg, #ff7681 0%, #dc3545 60%, #9b1f2b 100%); }
// "Mark as Critical" chip in the New tab — dormant by default, glows red
// once active so the user knows the flag is set before they submit.
.o_fhd_critical_row {
display: flex;
align-items: center;
gap: 0.6rem;
flex-wrap: wrap;
}
.o_fhd_critical_chip {
display: inline-flex;
align-items: center;
padding: 0.4rem 0.85rem;
border-radius: 999px;
background-color: $fhd-bg;
border: 1px solid $fhd-border;
color: $fhd-text;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
&:hover { background-color: $fhd-hover; }
&.o_fhd_critical_active {
background: linear-gradient(135deg, #e85d68 0%, #dc3545 100%);
border-color: #b02a37;
color: #fff;
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.18);
}
}
// Thread
.o_fhd_thread_head {
display: flex;

View File

@@ -72,6 +72,23 @@
placeholder="e.g. TypeError: Cannot read property 'foo' of undefined …"/>
</div>
<div class="o_fhd_field">
<label>Severity</label>
<div class="o_fhd_critical_row">
<button type="button"
class="o_fhd_critical_chip"
t-att-class="{ 'o_fhd_critical_active': state.isCritical }"
t-on-click="() => state.isCritical = !state.isCritical">
<i class="fa fa-exclamation-triangle me-1"/>
<t t-if="state.isCritical">Marked Critical</t>
<t t-else="">Mark as Critical</t>
</button>
<span class="o_fhd_hint">
Flag this if it's blocking production work — support gets paged.
</span>
</div>
</div>
<div class="o_fhd_field">
<label>Attachments</label>
<div class="o_fhd_actions_row">
@@ -128,6 +145,32 @@
t-on-click="() => this.setScope('all')">All (deployment)</button>
</div>
<!--
KPI cards — Total / Open / Closed / Critical. Counts come from
the same `state.tickets` the sections render, so the numbers
always match what's on screen. The grid auto-wraps to two
columns on narrow dialogs.
-->
<div t-if="!state.loadingList and !state.listError and state.tickets.length"
class="o_fhd_kpi_grid">
<div class="o_fhd_kpi_card o_fhd_kpi_total">
<div class="o_fhd_kpi_label">Total</div>
<div class="o_fhd_kpi_value" t-esc="ticketStats.total"/>
</div>
<div class="o_fhd_kpi_card o_fhd_kpi_open">
<div class="o_fhd_kpi_label">Open</div>
<div class="o_fhd_kpi_value" t-esc="ticketStats.open"/>
</div>
<div class="o_fhd_kpi_card o_fhd_kpi_closed">
<div class="o_fhd_kpi_label">Closed</div>
<div class="o_fhd_kpi_value" t-esc="ticketStats.closed"/>
</div>
<div class="o_fhd_kpi_card o_fhd_kpi_critical">
<div class="o_fhd_kpi_label">Critical</div>
<div class="o_fhd_kpi_value" t-esc="ticketStats.critical"/>
</div>
</div>
<div t-if="state.loadingList" class="o_fhd_muted text-center p-3">
<i class="fa fa-spinner fa-spin me-1"/> Loading your tickets…
</div>
@@ -153,13 +196,10 @@
<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 t-if="isCriticalRow(t)" class="o_fhd_pill o_fhd_pill_critical" title="Critical priority">
<i class="fa fa-exclamation-triangle me-1"/>Critical
</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"/>
<span t-att-class="pillClass(t)" t-esc="t.stage"/>
</div>
</div>
</div>