diff --git a/fusion_helpdesk/__manifest__.py b/fusion_helpdesk/__manifest__.py index 8f8c575b..6c7882c1 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.6.0', + 'version': '19.0.1.7.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 4a7b98d8..857bf99a 100644 --- a/fusion_helpdesk/controllers/main.py +++ b/fusion_helpdesk/controllers/main.py @@ -44,7 +44,8 @@ class FusionHelpdeskController(http.Controller): ) def submit(self, kind, subject, description, error_code=None, attachments=None, - page_url=None, user_agent=None, reply_email=None): + page_url=None, user_agent=None, reply_email=None, + is_critical=False): """Forward a bug report or feature request to the central Odoo Helpdesk and return {ok, ticket_id, ticket_url, error}. @@ -98,6 +99,7 @@ class FusionHelpdeskController(http.Controller): team_id=cfg['team_id'], client_label=cfg['client_label'], reporter_name=user.name, reporter_email=reporter_email, company_name=request.env.company.name, + is_critical=bool(is_critical), ) # ---- Talk to remote Odoo -------------------------------------- diff --git a/fusion_helpdesk/static/src/js/fusion_helpdesk_dialog.js b/fusion_helpdesk/static/src/js/fusion_helpdesk_dialog.js index 490cff1c..13d6b6ea 100644 --- a/fusion_helpdesk/static/src/js/fusion_helpdesk_dialog.js +++ b/fusion_helpdesk/static/src/js/fusion_helpdesk_dialog.js @@ -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); diff --git a/fusion_helpdesk/static/src/scss/fusion_helpdesk.scss b/fusion_helpdesk/static/src/scss/fusion_helpdesk.scss index 5659b9eb..0f76c91e 100644 --- a/fusion_helpdesk/static/src/scss/fusion_helpdesk.scss +++ b/fusion_helpdesk/static/src/scss/fusion_helpdesk.scss @@ -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; diff --git a/fusion_helpdesk/static/src/xml/fusion_helpdesk_dialog.xml b/fusion_helpdesk/static/src/xml/fusion_helpdesk_dialog.xml index 18c1bb8f..ef108b34 100644 --- a/fusion_helpdesk/static/src/xml/fusion_helpdesk_dialog.xml +++ b/fusion_helpdesk/static/src/xml/fusion_helpdesk_dialog.xml @@ -72,6 +72,23 @@ placeholder="e.g. TypeError: Cannot read property 'foo' of undefined …"/> +
b
', team_id=1, + client_label='ENTECH', reporter_name='X', reporter_email='x@x.com', + company_name='X Inc', is_critical=True, + ) + self.assertEqual(vals.get('priority'), '3') + + def test_is_critical_default_omits_priority(self): + # Default path must NOT set priority so the central stage / SLA default + # keeps working — only the explicit toggle escalates. + vals = build_ticket_vals( + kind='bug', subject='X', body_html='b
', team_id=1, + client_label='ENTECH', reporter_name='X', reporter_email='x@x.com', + company_name='X Inc', + ) + self.assertNotIn('priority', vals) + @tagged('post_install', '-at_install', 'fusion_helpdesk') class TestScopeDomain(TransactionCase): diff --git a/fusion_helpdesk/utils.py b/fusion_helpdesk/utils.py index fd3accc0..68451fec 100644 --- a/fusion_helpdesk/utils.py +++ b/fusion_helpdesk/utils.py @@ -49,13 +49,19 @@ def _norm_email(*candidates): def build_ticket_vals(kind, subject, body_html, team_id, client_label, - reporter_name, reporter_email, company_name): + reporter_name, reporter_email, company_name, + is_critical=False): """Construct the `helpdesk.ticket` create vals for a forwarded report. The identity fields (`partner_email`, `partner_name`, `partner_company_name`) drive native helpdesk find-or-create of the customer partner + follower subscription on the central Odoo, and `x_fc_client_label` tags the deployment for the scoped inbox. + + When `is_critical` is set, we pass `priority='3'` (Urgent) — the central + post-create hook then auto-applies the Critical helpdesk.tag so support + can filter by it. Priority drives the inbox's Critical section too + (see `bucket_ticket`), so the same toggle is load-bearing on both ends. """ kind_label = 'Bug Report' if kind == 'bug' else 'Feature Request' prefix = ('[%s] ' % client_label) if client_label else '' @@ -72,6 +78,8 @@ def build_ticket_vals(kind, subject, body_html, team_id, client_label, vals['partner_company_name'] = company_name if client_label: vals['x_fc_client_label'] = client_label + if is_critical: + vals['priority'] = '3' return vals diff --git a/fusion_helpdesk_central/__manifest__.py b/fusion_helpdesk_central/__manifest__.py index 8f2f61e7..537a849d 100644 --- a/fusion_helpdesk_central/__manifest__.py +++ b/fusion_helpdesk_central/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 { 'name': 'Fusion Helpdesk Central — Client API Keys', - 'version': '19.0.1.1.0', + 'version': '19.0.1.2.0', 'category': 'Productivity', 'summary': 'Admin UI on the central Odoo for issuing per-client API ' 'keys used by fusion_helpdesk client deployments.', @@ -29,6 +29,7 @@ Depends only on `helpdesk`. No client-side install needed. 'security/ir.model.access.csv', 'data/ir_config_parameter_data.xml', 'data/mail_template_ack.xml', + 'data/helpdesk_tag_critical.xml', 'views/fusion_helpdesk_client_key_views.xml', 'views/helpdesk_ticket_views.xml', ], diff --git a/fusion_helpdesk_central/data/helpdesk_tag_critical.xml b/fusion_helpdesk_central/data/helpdesk_tag_critical.xml new file mode 100644 index 00000000..c2e774c6 --- /dev/null +++ b/fusion_helpdesk_central/data/helpdesk_tag_critical.xml @@ -0,0 +1,24 @@ + + +