diff --git a/awesome_dashboard/__init__.py b/awesome_dashboard/__init__.py
index b0f26a9a602..aa4d0fd63a9 100644
--- a/awesome_dashboard/__init__.py
+++ b/awesome_dashboard/__init__.py
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import controllers
+from . import models
diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py
index a1cd72893d7..602c63b67a6 100644
--- a/awesome_dashboard/__manifest__.py
+++ b/awesome_dashboard/__manifest__.py
@@ -16,7 +16,7 @@
'version': '0.1',
'application': True,
'installable': True,
- 'depends': ['base', 'web', 'mail', 'crm'],
+ 'depends': ['base', 'web', 'mail', 'crm', 'sale'],
'data': [
'views/views.xml',
@@ -24,6 +24,10 @@
'assets': {
'web.assets_backend': [
'awesome_dashboard/static/src/**/*',
+ ('remove', 'awesome_dashboard/static/src/dashboard/**/*'),
+ ],
+ 'awesome_dashboard.dashboard': [
+ 'awesome_dashboard/static/src/dashboard/**/*',
],
},
'license': 'AGPL-3'
diff --git a/awesome_dashboard/models/__init__.py b/awesome_dashboard/models/__init__.py
new file mode 100644
index 00000000000..b6d3b8ca69b
--- /dev/null
+++ b/awesome_dashboard/models/__init__.py
@@ -0,0 +1,2 @@
+from . import res_users
+from . import sale_order
diff --git a/awesome_dashboard/models/res_users.py b/awesome_dashboard/models/res_users.py
new file mode 100644
index 00000000000..bb3610905d7
--- /dev/null
+++ b/awesome_dashboard/models/res_users.py
@@ -0,0 +1,15 @@
+from odoo import fields, models
+
+
+class ResUsers(models.Model):
+ _inherit = 'res.users'
+
+ dashboard_disabled_items = fields.Char(default='[]')
+
+ @property
+ def SELF_READABLE_FIELDS(self):
+ return super().SELF_READABLE_FIELDS + ['dashboard_disabled_items']
+
+ @property
+ def SELF_WRITEABLE_FIELDS(self):
+ return super().SELF_WRITEABLE_FIELDS + ['dashboard_disabled_items']
diff --git a/awesome_dashboard/models/sale_order.py b/awesome_dashboard/models/sale_order.py
new file mode 100644
index 00000000000..af837ff9258
--- /dev/null
+++ b/awesome_dashboard/models/sale_order.py
@@ -0,0 +1,10 @@
+from odoo import fields, models
+
+
+class SaleOrder(models.Model):
+ _inherit = 'sale.order'
+
+ size = fields.Selection(
+ [('s', 'S'), ('m', 'M'), ('xl', 'XL')],
+ string="T-shirt size",
+ )
diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js
deleted file mode 100644
index c4fb245621b..00000000000
--- a/awesome_dashboard/static/src/dashboard.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Component } from "@odoo/owl";
-import { registry } from "@web/core/registry";
-
-class AwesomeDashboard extends Component {
- static template = "awesome_dashboard.AwesomeDashboard";
-}
-
-registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard);
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js
new file mode 100644
index 00000000000..9d9e12f72f7
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.js
@@ -0,0 +1,69 @@
+import { Component, xml, useState, onWillStart } from "@odoo/owl";
+
+import { registry } from "@web/core/registry";
+import { Layout } from "@web/search/layout";
+import { useService } from "@web/core/utils/hooks";
+import { _t } from "@web/core/l10n/translation";
+
+import { DashboardItem } from "./dashboard_item";
+import { SettingsDialog } from "./settings_dialog";
+import "./dashboard_items";
+import "./disabled_items_service";
+
+class AwesomeDashboard extends Component {
+ static template = xml`
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ static components = { Layout, DashboardItem }
+
+ setup() {
+ this.action = useService("action")
+ this.dialog = useService("dialog")
+ this.statistics = useService("awesome_dashboard.statistics")
+ this.disabledItems = useService("awesome_dashboard.disabled_items")
+ this.items = useState(registry.category("awesome_dashboard").getAll().map(
+ (item) => ({ ...item, disabled: false })
+ ))
+ onWillStart(async () => {
+ const disabled = await this.disabledItems.load()
+ for (const item of this.items) {
+ item.disabled = disabled.includes(item.id)
+ }
+ })
+ }
+
+ openSettings() {
+ this.dialog.add(SettingsDialog, { items: this.items })
+ }
+
+ gotoCustomers() {
+ this.action.doAction("base.action_partner_form")
+ }
+
+ gotoLeads() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: _t("Leads"),
+ res_model: "crm.lead",
+ views: [[false, 'list'], [false, 'form']],
+ })
+ }
+
+}
+
+registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard);
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss
new file mode 100644
index 00000000000..32220f725f4
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.scss
@@ -0,0 +1,11 @@
+.o_dashboard {
+ background-color: gray;
+}
+
+@media (max-width: 768px) {
+ .o_dashboard .card {
+ display: block !important;
+ // override the inline width set by DashboardItem
+ width: auto !important;
+ }
+}
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml
similarity index 100%
rename from awesome_dashboard/static/src/dashboard.xml
rename to awesome_dashboard/static/src/dashboard/dashboard.xml
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item.js
new file mode 100644
index 00000000000..544e56b6fb9
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_item.js
@@ -0,0 +1,18 @@
+import { Component, xml } from "@odoo/owl"
+
+export class DashboardItem extends Component {
+ static props = {
+ slots: { type: Object, optional: true },
+ size: { type: Number, optional: true },
+ }
+
+ static defaultProps = { size: 1 }
+
+ static template = xml`
+
+ `
+}
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js
new file mode 100644
index 00000000000..a0525a3fca3
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js
@@ -0,0 +1,72 @@
+import { registry } from "@web/core/registry"
+import { _t } from "@web/core/l10n/translation"
+
+import { NumberCard } from "./number_card"
+import { PieChartCard } from "./pie_chart_card"
+
+const items = [
+ {
+ id: "average_quantity",
+ description: _t("Average amount of t-shirt"),
+ Component: NumberCard,
+ size: 1.5,
+ 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,
+ size: 2,
+ 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("New orders this month"),
+ Component: NumberCard,
+ props: (data) => ({
+ title: _t("Number of new orders this month"),
+ value: data.nb_new_orders,
+ }),
+ },
+ {
+ id: "nb_cancelled_orders",
+ description: _t("Cancelled orders this month"),
+ Component: NumberCard,
+ size: 1.5,
+ 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 this month"),
+ Component: NumberCard,
+ size: 1.5,
+ props: (data) => ({
+ title: _t("Total amount of new orders this month"),
+ value: data.total_amount,
+ }),
+ },
+ {
+ id: "orders_by_size",
+ description: _t("Shirt orders by size"),
+ Component: PieChartCard,
+ size: 2,
+ props: (data) => ({
+ title: _t("Shirt orders by size"),
+ values: data.orders_by_size,
+ }),
+ },
+]
+
+const itemsRegistry = registry.category("awesome_dashboard")
+for (const item of items) {
+ itemsRegistry.add(item.id, item)
+}
diff --git a/awesome_dashboard/static/src/dashboard/disabled_items_service.js b/awesome_dashboard/static/src/dashboard/disabled_items_service.js
new file mode 100644
index 00000000000..cc2bfc506f5
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/disabled_items_service.js
@@ -0,0 +1,22 @@
+import { registry } from "@web/core/registry";
+import { user } from "@web/core/user";
+
+const disabledItemsService = {
+ dependencies: ["orm"],
+
+ start(env, { orm }) {
+ return {
+ async load() {
+ const [data] = await orm.read("res.users", [user.userId], ["dashboard_disabled_items"]);
+ return JSON.parse(data.dashboard_disabled_items || "[]");
+ },
+ save(ids) {
+ return orm.write("res.users", [user.userId], {
+ dashboard_disabled_items: JSON.stringify(ids),
+ });
+ },
+ };
+ },
+};
+
+registry.category("services").add("awesome_dashboard.disabled_items", disabledItemsService);
diff --git a/awesome_dashboard/static/src/dashboard/number_card.js b/awesome_dashboard/static/src/dashboard/number_card.js
new file mode 100644
index 00000000000..3b3b381c683
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/number_card.js
@@ -0,0 +1,17 @@
+import { Component, xml } from "@odoo/owl"
+
+export class NumberCard extends Component {
+ static props = {
+ title: { type: String },
+ value: { type: [Number, String] },
+ }
+
+ static template = xml`
+
+
+
+
+
+
+ `
+}
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card.js
new file mode 100644
index 00000000000..2af32cfd6bb
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie_chart_card.js
@@ -0,0 +1,19 @@
+import { Component, xml } from "@odoo/owl"
+
+import { PieChart } from "../piechart/piechart"
+
+export class PieChartCard extends Component {
+ static props = {
+ title: { type: String },
+ values: { type: Object },
+ }
+
+ static components = { PieChart }
+
+ static template = xml`
+
+
+
+
+ `
+}
diff --git a/awesome_dashboard/static/src/dashboard/settings_dialog.js b/awesome_dashboard/static/src/dashboard/settings_dialog.js
new file mode 100644
index 00000000000..c6ae04ab68f
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/settings_dialog.js
@@ -0,0 +1,47 @@
+import { Component, xml, useState } from "@odoo/owl"
+
+import { Dialog } from "@web/core/dialog/dialog"
+import { CheckBox } from "@web/core/checkbox/checkbox"
+import { useService } from "@web/core/utils/hooks"
+import { _t } from "@web/core/l10n/translation"
+
+export class SettingsDialog extends Component {
+ static props = {
+ items: { type: Array },
+ close: { type: Function },
+ }
+
+ static components = { Dialog, CheckBox }
+
+ static template = xml`
+
+ `
+
+ setup() {
+ this.title = _t("Dashboard items configuration")
+ this.disabledItems = useService("awesome_dashboard.disabled_items")
+ this.disabled = useState({})
+ for (const item of this.props.items) {
+ this.disabled[item.id] = item.disabled
+ }
+ }
+
+ onApply() {
+ for (const item of this.props.items) {
+ item.disabled = this.disabled[item.id]
+ }
+ const ids = this.props.items.filter((item) => item.disabled).map((item) => item.id)
+ this.disabledItems.save(ids)
+ this.props.close()
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/statistics_service.js b/awesome_dashboard/static/src/dashboard/statistics_service.js
new file mode 100644
index 00000000000..4224ab5b76b
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/statistics_service.js
@@ -0,0 +1,22 @@
+import { reactive } from "@odoo/owl";
+
+import { registry } from "@web/core/registry";
+import { rpc } from "@web/core/network/rpc";
+
+const statisticsService = {
+ async _loadData(statistics) {
+ const updates = await rpc("/awesome_dashboard/statistics");
+ Object.assign(statistics, updates, { isReady: true });
+ },
+
+ start() {
+ const statistics = reactive({ isReady: false });
+
+ setInterval(() => this._loadData(statistics), 10_000);
+ this._loadData(statistics);
+
+ return statistics;
+ },
+};
+
+registry.category("services").add("awesome_dashboard.statistics", statisticsService);
diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js
new file mode 100644
index 00000000000..7b75ec1b3e4
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_action.js
@@ -0,0 +1,13 @@
+import { Component, xml } from "@odoo/owl";
+
+import { registry } from "@web/core/registry";
+import { LazyComponent } from "@web/core/assets";
+
+class AwesomeDashboardLoader extends Component {
+ static components = { LazyComponent };
+ static template = xml`
+
+ `;
+}
+
+registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader);
diff --git a/awesome_dashboard/static/src/piechart/piechart.js b/awesome_dashboard/static/src/piechart/piechart.js
new file mode 100644
index 00000000000..f9fcc9e042a
--- /dev/null
+++ b/awesome_dashboard/static/src/piechart/piechart.js
@@ -0,0 +1,55 @@
+import { Component, xml, onMounted, onWillStart, onWillUnmount, useRef } from "@odoo/owl";
+
+import { loadJS } from "@web/core/assets";
+import { useService } from "@web/core/utils/hooks";
+import { _t } from "@web/core/l10n/translation";
+
+export class PieChart extends Component {
+ static props = {
+ data: { type: Object },
+ };
+
+ static template = xml`
+
+
+
+ `;
+
+ setup() {
+ this.canvasRef = useRef("canvas");
+ this.action = useService("action");
+
+ onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js"));
+
+ onMounted(() => {
+ this.chart = new Chart(this.canvasRef.el, {
+ type: "pie",
+ data: {
+ labels: Object.keys(this.props.data),
+ datasets: [{ data: Object.values(this.props.data) }],
+ },
+ options: {
+ onClick: (ev, elements) => this.onClick(elements),
+ },
+ });
+ });
+
+ onWillUnmount(() => {
+ this.chart.destroy();
+ });
+ }
+
+ onClick([element]) {
+ if (!element) {
+ return;
+ }
+ const size = this.chart.data.labels[element.index];
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: _t("Orders of size %s", size),
+ res_model: "sale.order",
+ domain: [["size", "=", size]],
+ views: [[false, "list"], [false, "form"]],
+ });
+ }
+}
diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js
new file mode 100644
index 00000000000..017e940fe9a
--- /dev/null
+++ b/awesome_owl/static/src/card/card.js
@@ -0,0 +1,33 @@
+import { Component, xml, useState } from "@odoo/owl"
+
+export class Card extends Component {
+ static props = {
+ title: String,
+ slots: { type: Object, optional: true },
+ }
+
+ setup() {
+ this.state = useState({isOpen: false});
+ }
+
+ handleToggle() {{
+ this.state.isOpen = !this.state.isOpen;
+ }}
+
+
+ static template = xml`
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+}
\ No newline at end of file
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js
new file mode 100644
index 00000000000..da4de483b50
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.js
@@ -0,0 +1,25 @@
+import { Component, useState, xml } from "@odoo/owl";
+
+export class Counter extends Component {
+ static props = { callback: Function }
+
+ setup() {
+ this.state = useState({ value: 0 })
+ }
+
+ increment() {
+ this.state.value++
+ this.props.callback()
+ }
+
+ static template = xml`
+
+ `
+}
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js
index 4ac769b0aa5..3e3f9baea2b 100644
--- a/awesome_owl/static/src/playground.js
+++ b/awesome_owl/static/src/playground.js
@@ -1,5 +1,99 @@
-import { Component } from "@odoo/owl";
+import { Component, useState, xml, markup } from "@odoo/owl";
+
+import { Counter } from './counter/counter';
+import { Card } from './card/card';
+import { TodoList } from './todo/todo_list';
export class Playground extends Component {
- static template = "awesome_owl.playground";
+ setup() {
+ this.state = useState({
+ a: 0,
+ b: 0,
+ c: 0,
+ content: markup(`hello
`),
+ task_id: 1,
+ })
+ this.todos = useState([]);
+ }
+
+ increment_a() {
+ this.state.a++
+ }
+
+ increment_b() {
+ this.state.b++
+ }
+
+ increment_c() {
+ this.state.c++
+ }
+
+ handleKeyup(ev) {
+ const key = ev.key;
+
+ if (key === "Enter" && ev.target.value) {
+ this.todos.push({
+ id: this.state.task_id,
+ description: ev.target.value,
+ isCompleted: false,
+ })
+
+ this.state.task_id++;
+ ev.target.value = "";
+
+ return;
+ }
+ }
+
+ toggleTodo(id) {
+ const idx = this.todos.findIndex(t => t.id === id);
+
+ if (idx !== -1) {
+ const old = this.todos[idx].isCompleted;
+ this.todos[idx].isCompleted = !old;
+ }
+ }
+
+ removeTodo(id) {
+ const idx = this.todos.findIndex(t => t.id === id);
+
+
+ if (idx !== -1) this.todos.splice(idx, 1);
+ }
+
+ static template = xml`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sum:
+
+
+
+
+
+
+
+
+
+
+ `
+
+ static components = { Counter, Card, TodoList }
}
diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml
index 4fb905d59f9..9a392c6f276 100644
--- a/awesome_owl/static/src/playground.xml
+++ b/awesome_owl/static/src/playground.xml
@@ -1,10 +1,6 @@
-
-
- hello world
-
-
+
diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js
new file mode 100644
index 00000000000..5dc7b8cb663
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_item.js
@@ -0,0 +1,31 @@
+import { Component, xml } from "@odoo/owl";
+
+
+export class TodoItem extends Component {
+ static props = {
+ id: Number,
+ description: String,
+ isCompleted: Boolean,
+ toggleTodo: Function,
+ removeTodo: Function
+ }
+
+ static template = xml`
+
+ `
+}
diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js
new file mode 100644
index 00000000000..4b96aaf45d8
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_list.js
@@ -0,0 +1,23 @@
+import { Component, xml } from "@odoo/owl";
+
+import { TodoItem } from "./todo_item";
+
+export class TodoList extends Component {
+ static props = { list: Array, toggleTodo: Function, removeTodo: Function }
+
+ static template = xml`
+
+
+
+
+
+ `
+
+ static components = { TodoItem }
+}