Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7f460d7
copy changes from other branch
averykatko Sep 22, 2025
981e046
[FIX] awesome_owl: fix manifest
averykatko Sep 22, 2025
465f136
[ADD] awesome_dashboard: dashboard-item
averykatko Sep 23, 2025
d74534d
[ADD] awesome_dashboard: statistics in dashboard
averykatko Sep 23, 2025
dabc866
[FIX] awesome_dashboard: file naming convention
averykatko Sep 24, 2025
b6826af
[ADD] awesome_dashboard: statistics service
averykatko Sep 24, 2025
74e79a8
[ADD] awesome_dashboard: shirt orders by size pie chart
averykatko Sep 24, 2025
eab21f4
[ADD] awesome_dashboard: periodically update statistics
averykatko Sep 24, 2025
22c573b
[ADD] awesome_dashboard: lazy load dashboard
averykatko Sep 24, 2025
662c8da
[ADD] awesome_dashboard: generic dashboard
averykatko Sep 24, 2025
f741891
[ADD] awesome_dashboard: dashboard item registry
averykatko Sep 24, 2025
dd9cf1c
[ADD] awesome_dashboard: settings to include/exclude dashboard items
averykatko Sep 25, 2025
6c5926d
[CLN] awesome_dashboard: clean up logs
averykatko Sep 29, 2025
2227ad4
[CLN] awesome_owl: restore comma
averykatko Sep 29, 2025
d4b7c1f
[FIX] awesome_owl: required: false -> optional: true
averykatko Sep 29, 2025
72155fa
[CLN] awesome_owl: use ?. instead of if condition call
averykatko Sep 29, 2025
3c9dd0b
[CLN] awesome_dashboard: self-closing <button/> instead of empty tag …
averykatko Sep 29, 2025
55834d8
[CLN] awesome_owl: simplify slots prop spec
averykatko Sep 29, 2025
1771dde
[CLN] awesome_dashboard: simply DashboardItem slots prop spec
averykatko Sep 29, 2025
da104d4
[ADD] awesome_dashboard: DashboardItemsDialog items prop type
averykatko Sep 29, 2025
f578df7
[CLN] awesome_dashboard: clearer names/structure for DashboardItemsDi…
averykatko Sep 29, 2025
d007b07
[ADD] awesome_dashboard: make dashboard item descriptions translatable
averykatko Sep 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# vscode
.vscode/
Comment on lines +130 to +132

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't push your changes on this file

9 changes: 7 additions & 2 deletions awesome_dashboard/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

'author': "Odoo",
'website': "https://www.odoo.com/",
'category': 'Tutorials/AwesomeDashboard',
'category': 'Tutorials',
'version': '0.1',
'application': True,
'installable': True,
Expand All @@ -22,8 +22,13 @@
'views/views.xml',
],
'assets': {
'awesome_dashboard.dashboard': [
'awesome_dashboard/static/src/dashboard/*',
'awesome_dashboard/static/src/dashboard/**/*',
],
'web.assets_backend': [
'awesome_dashboard/static/src/**/*',
'awesome_dashboard/static/src/dashboard_action.js',
'awesome_dashboard/static/src/statistics_service.js',
],
},
'license': 'AGPL-3'
Expand Down
10 changes: 0 additions & 10 deletions awesome_dashboard/static/src/dashboard.js

This file was deleted.

65 changes: 65 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Component, reactive, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useBus, useService } from "@web/core/utils/hooks";
import { Layout } from "@web/search/layout";
import { DashboardItem } from "./dashboard_item/dashboard_item";
import { NumberCard } from "./number_card/number_card";
import { PieChartCard } from "./pie_chart_card/pie_chart_card";
import { DashboardItemsDialog } from "./dashboard_items_dialog/dashboard_items_dialog";

const EXCLUDED_DASHBOARD_ITEMS_LS_KEY = 'excluded_dashboard_items';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice touch 🐐


class AwesomeDashboard extends Component {
static template = "awesome_dashboard.AwesomeDashboard";
static components = { Layout, DashboardItem, DashboardItemsDialog, NumberCard, PieChartCard };

setup() {
this.action = useService("action");
this.dialog = useService("dialog");

this.statisticsService = useService("statistics");
this.state = useState(this.statisticsService);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you probably don't need the middle man here:
useState(useService("statistics"));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also again state is a very general name. You can go with one of these 2:
this.statistics = useState(....)
this.state = useState({statistics: ...});


this.items = registry.category('dashboard_items').getAll();
useBus(registry.category('dashboard_items'), 'UPDATE', this.handleDashboardItemsUpdate.bind(this));

const initExcludedItems = JSON.parse(localStorage.getItem(EXCLUDED_DASHBOARD_ITEMS_LS_KEY)) ?? [];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know the difference between using ?? and || here? Might be a nice thing to look for :)

const storedStateObj = { excludedItems: initExcludedItems };
const store = obj => localStorage.setItem(EXCLUDED_DASHBOARD_ITEMS_LS_KEY, JSON.stringify(obj.excludedItems));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to store even if the obj doesn't have excludedItems?

const reactiveStoredState = reactive(storedStateObj, () => store(reactiveStoredState));
store(reactiveStoredState);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it not call the function initially when you give it to reactive? Did you test with removing this line?

this.storedState = useState(storedStateObj);
}

handleDashboardItemsUpdate(event) {
this.items = registry.category('dashboard_item').getAll();
}

openCustomers() {
this.action.doAction("base.action_partner_form");
}

openLeads() {
this.action.doAction({
type: 'ir.actions.act_window',
name: "Leads",
target: 'current',
res_model: 'crm.lead',
views: [[false, 'form'], [false, 'list']],
});
}

handleDashboardItemsConfigChange(excludedItems) {
this.storedState.excludedItems = excludedItems;
}

openSettings() {
this.removeSettingsDialog = this.dialog.add(DashboardItemsDialog, {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ummm actually using this dialog they can also add items so the name is not entirely correct 😎

items: this.items,
excludedItems: this.storedState.excludedItems,
onApply: this.handleDashboardItemsConfigChange.bind(this),
});
}
}

registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard);
14 changes: 14 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.o_dashboard {
background-color: gray;
}

.stats-name {
font-size: medium;
color: black;
}

.stats-number {
font-size: larger;
font-weight: bold;
color: green;
}
21 changes: 21 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_dashboard.AwesomeDashboard">
<Layout className="'o_dashboard h-100'" display="{controlPanel: {}}">
<t t-set-slot="control-panel-create-button">
<button t-on-click="openCustomers" type="button" class="btn btn-primary">Customers</button>
<button t-on-click="openLeads" type="button" class="btn btn-primary">Leads</button>
<button t-on-click="openSettings" type="button" class="fa fa-gear"></button>
</t>

<t t-foreach="items" t-as="item" t-key="item.id">
<DashboardItem t-if="!storedState.excludedItems.includes(item.id)" size="item.size || 1">
<t t-set="itemProp" t-value="item.props ? item.props(state.statistics) : {'data': state.statistics}"/>
<t t-component="item.Component" t-props="itemProp"/>
</DashboardItem>
</t>
</Layout>
</t>

</templates>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Component } from "@odoo/owl";

export class DashboardItem extends Component {
static template = 'awesome_dashboard.DashboardItem';
static props = {
size: {
type: Number,
optional: true,
},
slots: Object,
};
static defaultProps = {
size: 1,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.dashboard-item {
background-color: white;
display: inline-block;
margin: 1rem;
padding: 1rem;
text-align: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_dashboard.DashboardItem">
<div class="dashboard-item" t-att-style="'width: ' + 18*props.size + 'rem;'">
<t t-slot="default"/>
</div>
</t>
</templates>
68 changes: 68 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard_items.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { NumberCard } from "./number_card/number_card";
import { PieChartCard } from "./pie_chart_card/pie_chart_card";

export const dashboardItemRegistry = registry.category("dashboard_items");

export const items = [
{
id: 'average_quantity',
description: _t("Average amount of t-shirt"),
Component: NumberCard,
size: 3,
props: data => ({
title: _t("Average amount of t-shirt by order this month"),
value: data.average_quantity,
}),
},
{
id: 'average_time',
description: _t("Average time for an order"),
Component: NumberCard,
props: data => ({
title: _t("Average time for an order to go from 'new' to 'sent' or 'cancelled'"),
value: data.average_time,
}),
},
{
id: 'nb_new_orders',
description: _t("Number of new orders"),
Component: NumberCard,
props: data => ({
title: _t("Number of new orders this month"),
value: data.nb_new_orders,
}),
},
{
id: 'nb_cancelled_orders',
description: _t("Number of cancelled orders"),
Component: NumberCard,
props: data => ({
title: _t("Number of cancelled orders this month"),
value: data.nb_cancelled_orders,
}),
},
{
id: 'total_amount',
description: _t("Total amount of new orders"),
Component: NumberCard,
props: data => ({
title: _t("Total amount of new orders this month"),
value: data.total_amount,
}),
},
{
id: 'orders_by_size',
description: _t("Orders by size"),
Component: PieChartCard,
props: data => ({
title: _t("Shirt orders by size"),
value: data.orders_by_size,
}),
},
];

for (const item of items) {
dashboardItemRegistry.add(item.id, item);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Component, useState } from "@odoo/owl";
import { Dialog } from "@web/core/dialog/dialog";

export class DashboardItemsDialog extends Component {
static template = 'awesome_dashboard.DashboardItemsDialog';
static components = { Dialog };
static props = {
items: {
type: Array,
element: {
type: Object,
shape: {
id: String,
description: String,
"*": true,
},
},
},
excludedItems: {
type: Array,
element: String,
},
close: Function,
onApply: Function,
};

setup() {
this.state = useState({
items: this.props.items.map(
item => ({
...item,
checked: !this.props.excludedItems.includes(item.id),
}),
),
});
}

toggle(event) {
const index = this.state.items.findIndex(item => item.id === event.target.name);
if (index !== -1) {
this.state.items[index].checked = !this.state.items[index].checked;
}
}

apply() {
const newExcludedItems = this.state.items.filter(item => !item.checked).map(item => item.id);
this.props.onApply(newExcludedItems);
this.props.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_dashboard.DashboardItemsDialog">
<Dialog>
<t t-set-slot="header">
<h1 class="modal-title fs-5" id="dashboardItemsDialogTitle">Dashboard items configuration</h1>
<button type="button" class="btn-close" aria-label="Close" t-on-click="props.close"/>
</t>
<p>Which cards do you wish to see?</p>
<p t-foreach="state.items" t-as="item" t-key="item.id">
<input t-att-name="item.id" type="checkbox" t-on-change="toggle" t-att-checked="item.checked"/>
<label><t t-esc="item.description"/></label>
</p>
<t t-set-slot="footer">
<button type="button" class="btn btn-primary" t-on-click="apply">Apply</button>
</t>
</Dialog>
</t>
</templates>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Component } from "@odoo/owl";

export class NumberCard extends Component {
static template = 'awesome_dashboard.NumberCard';
static props = {
title: String,
value: Number,
};
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_dashboard.NumberCard">
<p class="stats-name"><t t-esc="props.title"/></p>
<p class="stats-number"><t t-esc="props.value"/></p>
</t>
</templates>
50 changes: 50 additions & 0 deletions awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Component, onWillStart, onMounted, onWillUnmount, useEffect, useRef } from "@odoo/owl";
import { loadJS } from "@web/core/assets";

export class PieChart extends Component {
static template = 'awesome_dashboard.PieChart';
static props = {
data: Object,
};

setup() {
const canvasRef = useRef('canvas');

onWillStart(async () => {
await loadJS('/web/static/lib/Chart/Chart.js');
});

onMounted(() => {
const canvas = canvasRef.el;
const ctx = canvas.getContext('2d');
this.chart = new Chart(ctx, {
type: 'pie',
data: {
labels: Object.keys(this.props.data),
datasets: [
{
data: Object.values(this.props.data),
},
],
},
options: {},
});
this.chart.update();
});

useEffect(
data => {
this.chart.data.labels = Object.keys(data);
this.chart.data.datasets[0].data = Object.values(data);
Comment on lines +37 to +38

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you did definitely works 👏
Maybe this would look better though:

this.chart.date = { labels: Object.keys(data), datasets: [ data: Object.values(data) ]}

this.chart.update();
},
() => [this.props.data],
);

onWillUnmount(() => {
if (this.chart) {
this.chart.destroy();
}
Comment on lines +45 to +47

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the coolest things in JS is the ?

You can do the same thing without the if clause:
this.chart?.destroy();

});
}
}
Loading