Skip to content

Commit 6941e0e

Browse files
committed
[ADD] awesome_dashboard: implement custom dashboard module
Developed a custom dashboard module using OWL and JavaScript to display key business metrics in a dynamic and user-friendly interface. The dashboard integrates with backend models to show real-time data using cards and charts.This module demonstrates how to build interactive dashboards in Odoo using client-side rendering and custom components.
1 parent d272742 commit 6941e0e

File tree

18 files changed

+533
-18
lines changed

18 files changed

+533
-18
lines changed

awesome_dashboard/static/src/dashboard.js

Lines changed: 0 additions & 10 deletions
This file was deleted.

awesome_dashboard/static/src/dashboard.xml

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/** @odoo-module **/
2+
3+
import { Component, onWillStart, useState } from "@odoo/owl";
4+
import { registry } from "@web/core/registry";
5+
import { Layout } from "@web/search/layout";
6+
import { useService } from "@web/core/utils/hooks";
7+
import { DashboardItem } from "./dashboardItem/dashboarditem";
8+
import { rpc } from "@web/core/network/rpc";
9+
import { DashboardSetting } from "./dashboardSetting/dashboardsetting";
10+
import { PieChart } from "./piechart/piechart";
11+
12+
13+
class AwesomeDashboard extends Component {
14+
static template = "awesome_dashboard.AwesomeDashboard";
15+
16+
static components = { Layout, DashboardItem, PieChart };
17+
18+
setup() {
19+
const dashboardItemsRegistry = registry.category("awesome_dashboard");
20+
this.items = dashboardItemsRegistry.getAll();
21+
this.dialogService = useService("dialog");
22+
23+
24+
this.action = useService("action");
25+
this.statisticsService = useService("awesome_dashboard.statistics");
26+
this.state = useState({ statistics: this.statisticsService.statistics });
27+
28+
29+
this.displayState = useState({
30+
disabledItems: [],
31+
isLoading: true,
32+
});
33+
onWillStart(async () => {
34+
try {
35+
const fetchedDisabledItems = await rpc("/web/dataset/call_kw/res.users/get_dashboard_settings", {
36+
model: 'res.users',
37+
method: 'get_dashboard_settings',
38+
args: [],
39+
kwargs: {},
40+
});
41+
this.displayState.disabledItems = fetchedDisabledItems;
42+
} catch (error) {
43+
console.error("Error loading initial dashboard settings from server:", error);
44+
this.displayState.disabledItems = [];
45+
} finally {
46+
this.displayState.isLoading = false;
47+
}
48+
});
49+
}
50+
51+
updateSettings(newUncheckedItems) {
52+
this.displayState.disabledItems.length = 0;
53+
this.displayState.disabledItems.push(...newUncheckedItems);
54+
}
55+
56+
openSettings() {
57+
this.dialogService.add(DashboardSetting, {
58+
items: this.items,
59+
initialDisabledItems: this.displayState.disabledItems,
60+
updateSettings: this.updateSettings.bind(this),
61+
});
62+
}
63+
64+
openCustomerView() {
65+
this.action.doAction("base.action_partner_form")
66+
}
67+
68+
openLeadsView() {
69+
this.action.doAction({
70+
type: 'ir.actions.act_window',
71+
target: 'current',
72+
res_model: 'crm.lead',
73+
views: [[false, 'list'], [false, 'form']],
74+
})
75+
}
76+
}
77+
78+
registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<templates xml:space="preserve">
3+
4+
<t t-name="awesome_dashboard.AwesomeDashboard">
5+
<Layout className="'o_dashboard h-100'" display="{controlPanel: {} }">
6+
<t t-set-slot="control-panel-create-button">
7+
<button type="button" class="btn btn-primary o-kanban-button-new" t-on-click="openCustomerView">Customer</button>
8+
<button type="button" class="btn btn-primary o-kanban-button-new" t-on-click="openLeadsView">Leads</button>
9+
</t>
10+
<t t-set-slot="control-panel-additional-actions">
11+
<button type="button" icon="fa-cog" class="btn btn-light"
12+
t-on-click="openSettings">
13+
<i class="fa fa-cog" />
14+
</button>
15+
</t>
16+
<div class="dashboard-items">
17+
<t t-foreach="this.items" t-as="item" t-key="item.id">
18+
<DashboardItem size="item.size || 1"
19+
t-if="!this.displayState.disabledItems.includes(item.id)">
20+
<t t-set="itemProp" t-value="item.props ? item.props(this.state.statistics) : {'data': this.state.statistics}"/>
21+
<t t-component="item.Component" t-props="itemProp" />
22+
</DashboardItem>
23+
</t>
24+
</div>
25+
</Layout>
26+
</t>
27+
28+
</templates>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/** @odoo-module **/
2+
3+
import { Component } from "@odoo/owl"
4+
5+
export class DashboardItem extends Component {
6+
static template = "awesome_dashboard.DashboardItem"
7+
8+
static props = {
9+
size: { type: Number, optional: true, default: 1 },
10+
}
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<templates xml:space="preserve">
3+
<t t-name="awesome_dashboard.DashboardItem">
4+
<t t-set="itemSize" t-value="props.size or 1"/>
5+
<div t-attf-class="o_dashboard_item"
6+
t-attf-style="width: #{18 * itemSize}rem;">
7+
<t t-slot="default"/>
8+
</div>
9+
</t>
10+
</templates>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/** @odoo-module **/
2+
3+
import { Component } from "@odoo/owl";
4+
import { Dialog } from "@web/core/dialog/dialog";
5+
import { rpc } from "@web/core/network/rpc";
6+
import { _t } from "@web/core/l10n/translation";
7+
8+
export class DashboardSetting extends Component {
9+
static template = "awesome_dashboard.setting";
10+
11+
static components = { Dialog };
12+
13+
static props = {
14+
close: { type: Function }
15+
};
16+
17+
setup() {
18+
const items = this.props.items || {};
19+
20+
const initialDisabledItems = this.props.initialDisabledItems || [];
21+
22+
this.settingDisplayItems = Object.values(items).map((item) => ({
23+
...item,
24+
checked: !initialDisabledItems.includes(item.id),
25+
}))
26+
}
27+
28+
_t(...args) {
29+
return _t(...args);
30+
}
31+
32+
onChange(checked, itemInDialog) {
33+
const targetItem = this.settingDisplayItems.find(i => i.id === itemInDialog.id);
34+
if (targetItem) {
35+
targetItem.checked = checked;
36+
}
37+
}
38+
39+
async confirmDone() {
40+
const newDisableItems = this.settingDisplayItems.filter((item) => !item.checked).map((item) => item.id);
41+
42+
await rpc("/web/dataset/call_kw/res.users/set_dashboard_settings", {
43+
model: 'res.users',
44+
method: 'set_dashboard_settings',
45+
args: [newDisableItems],
46+
kwargs: {},
47+
});
48+
49+
if (this.props.updateSettings) {
50+
this.props.updateSettings(newDisableItems);
51+
}
52+
this.props.close();
53+
}
54+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<templates xml:space="preserve">
3+
<t t-name="awesome_dashboard.setting">
4+
<Dialog title="_t('Dashboard Items Configuration')">
5+
<div class="p-3">
6+
<h2>Select items to display on your dashboard:</h2>
7+
<div t-foreach="settingDisplayItems" t-as="item" t-key="item.id" class="form-check mb-2">
8+
<input
9+
type="checkbox"
10+
class="form-check-input"
11+
t-att-id="'settings_item_' + item.id"
12+
t-att-checked="item.checked"
13+
t-on-change="(ev) => this.onChange(ev.target.checked, item)"
14+
/>
15+
<label class="form-check-label" t-att-for="'settings_item_' + item.id">
16+
<t t-out="item.description"/>
17+
</label>
18+
</div>
19+
</div>
20+
<t t-set-slot="footer">
21+
<button class="btn btn-primary" t-on-click="confirmDone">Done</button>
22+
<button class="btn btn-secondary ms-2" t-on-click="props.close">Cancel</button>
23+
</t>
24+
</Dialog>
25+
</t>
26+
</templates>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.o_dashboard{
2+
background-color: gray;
3+
}
4+
.o_dashboard_stat_block {
5+
text-align: center;
6+
margin-bottom: 24px;
7+
}
8+
9+
.o_dashboard_stat_label {
10+
font-weight: normal;
11+
margin-bottom: 10px;
12+
display: block;
13+
}
14+
15+
.o_dashboard_stat_value {
16+
font-size: 48px;
17+
color: #228B22;
18+
font-weight: bold;
19+
}
20+
.o_dashboard_item {
21+
background: #fff;
22+
border-radius: 0.75rem;
23+
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
24+
padding: 1rem;
25+
margin: 1rem;
26+
display: inline-flex;
27+
justify-content: center;
28+
vertical-align: top;
29+
min-height: 3rem;
30+
}
31+
32+
@media (max-width: 426px) {
33+
.o_dashboard_item {
34+
width: 100% !important;
35+
display: flex;
36+
margin-left: 0.5rem;
37+
margin-right: 0.5rem;
38+
box-sizing: border-box;
39+
}
40+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { NumberCard } from "./numbercard/numbercard";
2+
import { PieChartCard } from "./piechartcard/piechartcard";
3+
import { registry } from "@web/core/registry";
4+
import { _t } from "@web/core/l10n/translation";
5+
6+
const items = [
7+
{
8+
id: "nb_new_orders",
9+
description: _t("The number of new orders, this month"),
10+
Component: NumberCard,
11+
size: 1,
12+
props: (data) => ({
13+
title: _t("New Orders This Month:"),
14+
value: data.data.nb_new_orders
15+
}),
16+
},
17+
{
18+
id: "total_amount",
19+
description: _t("The total amount of orders, this month"),
20+
Component: NumberCard,
21+
size: 2,
22+
props: (data) => ({
23+
title: "Total Amount This Month:",
24+
value: data.data.total_amount
25+
}),
26+
},
27+
{
28+
id: "average_quantity",
29+
description: _t("The average number of t-shirts by order"),
30+
Component: NumberCard,
31+
size: 1,
32+
props: (data) => ({
33+
title: _t("Avg. T-Shirts per Order:"),
34+
value: data.data.average_quantity
35+
}),
36+
},
37+
{
38+
id: "nb_cancelled_orders",
39+
description: _t("The number of cancelled orders, this month"),
40+
Component: NumberCard,
41+
size: 1,
42+
props: (data) => ({
43+
title: _t("Cancelled Orders:"),
44+
value: data.data.nb_cancelled_orders
45+
}),
46+
},
47+
{
48+
id: "average_time",
49+
description: _t("The average time (in hours) elapsed between the moment an order is created, and the moment is it sent"),
50+
Component: NumberCard,
51+
size: 1,
52+
props: (data) => ({
53+
title: _t("Avg. Time New → Sent/Cancelled:"),
54+
value: data.data.average_time
55+
}),
56+
},
57+
{
58+
id: "orders_by_size",
59+
description: _t("Number of shirts ordered based on size"),
60+
Component: PieChartCard,
61+
size: 3,
62+
props: (data) => ({
63+
title: _t("Shirt orders by size:"),
64+
value: data.data.orders_by_size
65+
}),
66+
}
67+
]
68+
items.forEach((item) => {
69+
registry.category("awesome_dashboard").add(item.id, item)
70+
});

0 commit comments

Comments
 (0)