Files
Odoo-Modules/fusion_repairs/static/src/components/dashboard/dashboard.js
gsinghpal 38a79a4b04 feat(fusion_repairs): OWL dashboard - quick actions, KPIs, portal share
A real landing dashboard for the Fusion Repairs app so users see at a
glance what is open, what is urgent, and where to click. Built as an
OWL client action, theme-aware (light AND dark) at SCSS compile time,
zero hardcoded user-facing colours.

What's on it
- Hero banner with gradient accent
- 4 quick-action tiles (New Service Call, Service Calls, Maintenance
  Contracts, Repair Warranties)
- 6 KPI stat tiles (Open / Urgent+Safety / Awaiting Dispatch /
  Needs Re-Quote / New This Month / Maintenance Due 30d) - each is
  clickable and lands in the right filtered list
- Self-service portal cards with copy-to-clipboard for the public
  client portal URL and the sales rep portal URL (so office can
  share them on voicemail / printed materials / training)
- Recent Service Calls list (last 5) - click jumps to repair form
- Upcoming Maintenance list (next 5 due) - red pill when <=7 days out
- Configuration tiles (Equipment Categories / Intake Templates /
  Service Catalogue)
- Refresh button

Architecture
- fusion.repair.dashboard AbstractModel exposes get_dashboard_data():
  returns stats + urgency_breakdown + source_breakdown + recent[5] +
  upcoming[5] + portals (URLs resolved via web.base.url +
  fusion_repairs.client_portal_url)
- FusionRepairsDashboard OWL component (registry actions
  'fusion_repairs.dashboard') uses standalone rpc() per project rule
  #3, useService('action') for navigation, useService('notification')
  for copy feedback. static props = ['*'] to accept the client-action
  props envelope.
- _fr_tokens.scss registered FIRST in web.assets_backend so its
  variables are in scope when dashboard.scss compiles. NO @import (per
  project rule). Branches on $o-webclient-color-scheme at compile time
  so the dark bundle (web.assets_web_dark) gets dark hex values
  automatically - per project CLAUDE.md rule on dark mode.
- All visible colours come from CSS-variable-wrapped SCSS tokens
  (--fr-page-bg, --fr-card-bg, --fr-border, --fr-accent, ...) which
  fall back to the SCSS hex value. Three-layer contrast: page (grayest)
  -> card (mid) -> elevated (brightest).
- New ir.actions.client action_fusion_repairs_home_dashboard with
  tag='fusion_repairs.dashboard'.
- Top-level menu now lands on this dashboard. 'Dashboard' added as
  the first sub-menu; 'Service Calls' (the kanban) is still right
  below it.

Verified on local westin-v19:
  STATS: open=15, urgent=4, new_this_month=13, awaiting_dispatch=9,
         requires_requote=1, maintenance_due_30d=1, active_total=2
  PORTALS: client=http://192.168.139.165:8069/repair
           sales_rep=http://192.168.139.165:8069/my/repair/new
  RECENT count: 5
  UPCOMING count: 2
  SOURCE breakdown: backend_wizard 9, client_portal 3, manual 2, sales_rep_portal 1
  Web /web/login: 200, no SCSS compile errors in logs.

Bumped to 19.0.1.0.5 so the asset bundle hash refreshes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 22:58:06 -04:00

130 lines
3.8 KiB
JavaScript

/** @odoo-module **/
// Fusion Repairs dashboard - OWL client action.
// Uses standalone rpc() from @web/core/network/rpc per project rule #3
// and useService("action") to navigate to backend act_window actions.
import { Component, useState, onWillStart } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";
import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
export class FusionRepairsDashboard extends Component {
static template = "fusion_repairs.Dashboard";
static props = ["*"];
setup() {
this.action = useService("action");
this.notification = useService("notification");
this.state = useState({
loading: true,
stats: {},
urgency_breakdown: [],
source_breakdown: [],
recent: [],
upcoming: [],
portals: {},
});
onWillStart(async () => {
await this._loadData();
});
}
async _loadData() {
try {
const data = await rpc("/web/dataset/call_kw", {
model: "fusion.repair.dashboard",
method: "get_dashboard_data",
args: [],
kwargs: {},
});
this.state.stats = data.stats || {};
this.state.urgency_breakdown = data.urgency_breakdown || [];
this.state.source_breakdown = data.source_breakdown || [];
this.state.recent = data.recent || [];
this.state.upcoming = data.upcoming || [];
this.state.portals = data.portals || {};
} catch (e) {
this.notification.add(_t("Could not load dashboard data."), {
type: "danger",
});
} finally {
this.state.loading = false;
}
}
async refresh() {
this.state.loading = true;
await this._loadData();
}
openAction(xmlId, extraContext) {
return this.action.doAction(xmlId, {
additionalContext: extraContext || {},
});
}
openWizard() {
return this.action.doAction("fusion_repairs.action_open_repair_intake_wizard");
}
openRepair(repairId) {
return this.action.doAction({
type: "ir.actions.act_window",
res_model: "repair.order",
res_id: repairId,
views: [[false, "form"]],
target: "current",
});
}
openContract(contractId) {
return this.action.doAction({
type: "ir.actions.act_window",
res_model: "fusion.repair.maintenance.contract",
res_id: contractId,
views: [[false, "form"]],
target: "current",
});
}
openUrl(url) {
if (url) {
window.open(url, "_blank", "noopener");
}
}
async copyUrl(url) {
if (!url) return;
try {
await navigator.clipboard.writeText(url);
this.notification.add(_t("Copied to clipboard."), { type: "success" });
} catch (e) {
this.notification.add(_t("Could not copy URL. Select and copy manually."), {
type: "warning",
});
}
}
formatDate(value) {
if (!value) return "";
return value.slice(0, 10);
}
urgencyPillClass(urgency) {
if (urgency === "safety") return "fr-pill fr-pill-safety";
if (urgency === "urgent") return "fr-pill fr-pill-urgent";
return "fr-pill fr-pill-normal";
}
urgencyLabel(urgency) {
const map = { safety: "Safety", urgent: "Urgent", normal: "Normal" };
return map[urgency] || "Normal";
}
}
registry
.category("actions")
.add("fusion_repairs.dashboard", FusionRepairsDashboard);