diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py
index a1cd72893d7..ec653ae92dc 100644
--- a/awesome_dashboard/__manifest__.py
+++ b/awesome_dashboard/__manifest__.py
@@ -23,7 +23,10 @@
],
'assets': {
'web.assets_backend': [
- 'awesome_dashboard/static/src/**/*',
+ 'awesome_dashboard/static/src/dashboard_action.js',
+ ],
+ 'awesome_dashboard.dashboard_bundle': [
+ 'awesome_dashboard/static/src/dashboard/**/*'
],
},
'license': 'AGPL-3'
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.xml b/awesome_dashboard/static/src/dashboard.xml
deleted file mode 100644
index 1a2ac9a2fed..00000000000
--- a/awesome_dashboard/static/src/dashboard.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- hello dashboard
-
-
-
diff --git a/awesome_dashboard/static/src/dashboard/core/awesome_dashboard/statistics_service.js b/awesome_dashboard/static/src/dashboard/core/awesome_dashboard/statistics_service.js
new file mode 100644
index 00000000000..f26143de128
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/core/awesome_dashboard/statistics_service.js
@@ -0,0 +1,22 @@
+import { reactive } from "@odoo/owl";
+import { rpc } from "@web/core/network/rpc";
+import { registry } from "@web/core/registry";
+
+
+const statisticsService = {
+ start() {
+ const stats = reactive({});
+
+ async function loadStats() {
+ const stats_data = await rpc("/awesome_dashboard/statistics");
+ Object.assign(stats, stats_data);
+ }
+
+ loadStats();
+ setInterval(loadStats, 10000*60);
+
+ return stats;
+ },
+};
+
+registry.category("services").add("awesome_dashboard.statistics", statisticsService);
diff --git a/awesome_dashboard/static/src/dashboard/dashboard-item/dashboard-item.js b/awesome_dashboard/static/src/dashboard/dashboard-item/dashboard-item.js
new file mode 100644
index 00000000000..f190aaa953a
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard-item/dashboard-item.js
@@ -0,0 +1,12 @@
+import { Component } from "@odoo/owl";
+
+export class DashboardItem extends Component {
+ static template = "awesome_dashboard.dashboard_item";
+ static props = {
+ slots: { type: Object, optional: true },
+ size: { type: Number, optional: true },
+ };
+ static defaultProps = {
+ size: 1,
+ };
+}
diff --git a/awesome_dashboard/static/src/dashboard/dashboard-item/dashboard-item.xml b/awesome_dashboard/static/src/dashboard/dashboard-item/dashboard-item.xml
new file mode 100644
index 00000000000..ee08ad926bd
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard-item/dashboard-item.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js
new file mode 100644
index 00000000000..241b9a99964
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.js
@@ -0,0 +1,55 @@
+import { Component, useState } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+import { Layout } from "@web/search/layout";
+import { DashboardItem } from "./dashboard-item/dashboard-item";
+import { SettingsDialog } from "./settings-dialog/settings-dialog";
+
+class AwesomeDashboard extends Component {
+ static template = "awesome_dashboard.AwesomeDashboard";
+ static components = { Layout, DashboardItem };
+ static props = [];
+
+ setup() {
+ this.action = useService("action");
+ const statisticsService = useService("awesome_dashboard.statistics");
+ this.dialog = useService('dialog');
+
+ this.statistics = useState(statisticsService);
+
+ this.state = useState({ items: this.getDisplayedItems() });
+
+ }
+
+ getDisplayedItems() {
+ const filtered_item_ids = localStorage.getItem("awesome_dashboard.displayed_items") ?? [];
+ return registry
+ .category("awesome_dashboard")
+ .getAll()
+ .filter((item) => !filtered_item_ids.includes(item.id));
+ }
+
+ reloadDashboard() {
+ this.state.items = this.getDisplayedItems();
+ }
+
+ openPartnerForm() {
+ this.action.doAction("base.action_partner_form");
+ }
+
+ openLeads() {
+ this.action.doAction({
+ type: 'ir.actions.act_window',
+ name: 'Leads',
+ res_model: 'crm.lead',
+ views: [[false, 'list'], [false, 'form']],
+ });
+ }
+
+ openSettingsDialog() {
+ this.dialog.add(SettingsDialog, {}, { onClose: () => this.reloadDashboard() });
+ }
+
+}
+
+registry.category("lazy_components").add("awesome_dashboard.dashboard", 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..2508cc740ad
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.scss
@@ -0,0 +1,3 @@
+.o_dashboard {
+ background-color: palegoldenrod;
+}
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml
new file mode 100644
index 00000000000..fcae74453a7
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..cd33aec087f
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js
@@ -0,0 +1,64 @@
+import { NumberCard } from "./number-card/number-card";
+import { PieChartCard } from "./pie-chart-card/pie-chart-card";
+import { registry } from "@web/core/registry";
+
+const items = [
+ {
+ id: "nb_new_orders",
+ description: "Number of new orders this month",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Number of new orders this month",
+ value: data.nb_new_orders,
+ }),
+ },
+ {
+ id: "total_amount",
+ description: "Total amount of new orders this month",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Total amount of new orders this month",
+ value: data.total_amount,
+ }),
+ },
+ {
+ id: "average_quantity",
+ description: "Average amount of t-shirt by order this month",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Average amount of t-shirt by order this month",
+ value: data.average_quantity,
+ }),
+ },
+ {
+ id: "nb_cancelled_orders",
+ description: "Number of cancelled orders this month",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Number of cancelled orders this month",
+ value: data.nb_cancelled_orders,
+ }),
+ },
+ {
+ id: "average_time",
+ description: "Average time for an order to go from 'new' to 'sent' or 'cancelled'",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'",
+ value: data.average_time,
+ }),
+ },
+ {
+ id: "orders_by_size",
+ description: "Orders by size",
+ Component: PieChartCard,
+ props: (data) => ({
+ title: "Orders by size",
+ data: data.orders_by_size,
+ }),
+ },
+];
+
+for (const item of items) {
+ registry.category("awesome_dashboard").add("awesome_dashboard." + item.id, item);
+}
diff --git a/awesome_dashboard/static/src/dashboard/number-card/number-card.js b/awesome_dashboard/static/src/dashboard/number-card/number-card.js
new file mode 100644
index 00000000000..f586334528e
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/number-card/number-card.js
@@ -0,0 +1,8 @@
+import { Component } from "@odoo/owl";
+import { DashboardItem } from "../dashboard-item/dashboard-item";
+
+export class NumberCard extends Component {
+ static template = "awesome_dashboard.number_card";
+ static props = ['title', 'value'];
+ static components = { DashboardItem };
+}
diff --git a/awesome_dashboard/static/src/dashboard/number-card/number-card.xml b/awesome_dashboard/static/src/dashboard/number-card/number-card.xml
new file mode 100644
index 00000000000..0efb0a51475
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/number-card/number-card.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie-chart-card/pie-chart-card.js b/awesome_dashboard/static/src/dashboard/pie-chart-card/pie-chart-card.js
new file mode 100644
index 00000000000..fe2ae29ff8c
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie-chart-card/pie-chart-card.js
@@ -0,0 +1,9 @@
+import { Component } from "@odoo/owl";
+import { DashboardItem } from "../dashboard-item/dashboard-item";
+import { PieChart } from "../pie-chart/pie-chart";
+
+export class PieChartCard extends Component {
+ static template = "awesome_dashboard.pie_chart_card";
+ static props = ['title', 'data'];
+ static components = { DashboardItem, PieChart };
+}
diff --git a/awesome_dashboard/static/src/dashboard/pie-chart-card/pie-chart-card.xml b/awesome_dashboard/static/src/dashboard/pie-chart-card/pie-chart-card.xml
new file mode 100644
index 00000000000..f56f1d0f746
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie-chart-card/pie-chart-card.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie-chart/pie-chart.js b/awesome_dashboard/static/src/dashboard/pie-chart/pie-chart.js
new file mode 100644
index 00000000000..a85684e831c
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie-chart/pie-chart.js
@@ -0,0 +1,55 @@
+import { Component, onWillStart, useRef, useEffect } from "@odoo/owl";
+import { loadJS } from "@web/core/assets";
+
+export class PieChart extends Component {
+ static template = "awesome_dashboard.pie_chart"
+ static props = ['data']
+
+ setup() {
+ this.chart = null;
+ this.canvasRef = useRef("canvas");
+ this.title = "Shirt orders by size"
+ onWillStart(async () => loadJS('/web/static/lib/Chart/Chart.js'));
+
+ useEffect(() => {
+ this.renderChart();
+ return () => {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ };
+ });
+ }
+
+ renderChart() {
+ const labels = Object.keys(this.props.data);
+ const data = Object.values(this.props.data);
+ const config = {
+ type: "doughnut",
+ data: {
+ datasets: [
+ {
+ data: data,
+ backgroundColor: ["#1f77b4", "#dddddd"],
+ },
+ ],
+ labels: labels,
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ layout: {
+ padding: 5,
+ },
+ plugins: {
+ title: {
+ display: true,
+ text: this.title,
+ padding: 4,
+ },
+ },
+ },
+ };
+ this.chart = new Chart(this.canvasRef.el, config);
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/pie-chart/pie-chart.xml b/awesome_dashboard/static/src/dashboard/pie-chart/pie-chart.xml
new file mode 100644
index 00000000000..a3db9bc373d
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie-chart/pie-chart.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+ Chart
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/settings-dialog/settings-dialog.js b/awesome_dashboard/static/src/dashboard/settings-dialog/settings-dialog.js
new file mode 100644
index 00000000000..f5d78d25628
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/settings-dialog/settings-dialog.js
@@ -0,0 +1,38 @@
+import { Component, useState } from "@odoo/owl";
+import { Dialog } from "@web/core/dialog/dialog"
+import { registry } from "@web/core/registry";
+
+
+export class SettingsDialog extends Component {
+ static template = "awesome_dashboard.settings_dialog";
+ static props = ['close'];
+ static components = { Dialog };
+
+ setup() {
+ this.state = useState({ items: {} });
+
+ const items = registry.category("awesome_dashboard").getAll();
+
+ const filtered_ids = localStorage.getItem("awesome_dashboard.displayed_items") ?? [];
+
+ for (var i of items) {
+ this.state.items[i.id] = { display: !filtered_ids.includes(i.id), description: i.description };
+ }
+ this.toggleDisplay = this.toggleDisplay.bind(this);
+ this.onSave = this.onSave.bind(this);
+ }
+
+ toggleDisplay(id) {
+ this.state.items[id].display = !this.state.items[id].display;
+ }
+
+ onSave() {
+ const filtered_ids = Object.entries(this.state.items)
+ .filter(([id, item]) => !item.display)
+ .map(([id]) => id);
+
+ localStorage.setItem("awesome_dashboard.displayed_items", filtered_ids)
+ this.props.close();
+ }
+
+}
diff --git a/awesome_dashboard/static/src/dashboard/settings-dialog/settings-dialog.xml b/awesome_dashboard/static/src/dashboard/settings-dialog/settings-dialog.xml
new file mode 100644
index 00000000000..738377acfd6
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/settings-dialog/settings-dialog.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js
new file mode 100644
index 00000000000..3d7d553d247
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_action.js
@@ -0,0 +1,13 @@
+import { Component, xml } from "@odoo/owl";
+import { LazyComponent } from "@web/core/assets";
+import { registry } from "@web/core/registry";
+
+class AwesomeDashboardLazy extends Component {
+ static components = { LazyComponent };
+ static template = xml`
+
+ `;
+ static props = ['*'];
+}
+
+registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLazy);
diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js
new file mode 100644
index 00000000000..be545f485e8
--- /dev/null
+++ b/awesome_owl/static/src/card/card.js
@@ -0,0 +1,10 @@
+import { Component, useState } from "@odoo/owl";
+
+export class Card extends Component {
+ static template = "awesome_owl.card";
+ static props = ['title?', 'slots']
+
+ setup() {
+ this.state = useState({ open: true });
+ }
+}
diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml
new file mode 100644
index 00000000000..e34b162fb66
--- /dev/null
+++ b/awesome_owl/static/src/card/card.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js
new file mode 100644
index 00000000000..5e0ceb2183b
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.js
@@ -0,0 +1,13 @@
+import { Component, useState } from "@odoo/owl";
+
+export class Counter extends Component {
+ static template = "awesome_owl.counter";
+ static props = ['onChange?']
+
+ state = useState({ value: 0 });
+
+ increment() {
+ this.state.value++;
+ this.props.onChange?.();
+ }
+}
diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml
new file mode 100644
index 00000000000..1f1cd5d6e54
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Counter:
+
+
+
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js
index 4ac769b0aa5..2f890565dd5 100644
--- a/awesome_owl/static/src/playground.js
+++ b/awesome_owl/static/src/playground.js
@@ -1,5 +1,18 @@
-import { Component } from "@odoo/owl";
+import { Component, markup, useState } 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";
+ static template = "awesome_owl.playground";
+ static components = { Counter, Card, TodoList };
+
+ state = useState({ value: 0 });
+
+ card1Content = '
some content 1
';
+ card2Content = markup('some content 2
');
+
+ incrementSum() {
+ this.state.value++;
+ }
}
diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml
index 4fb905d59f9..9b25b9145f2 100644
--- a/awesome_owl/static/src/playground.xml
+++ b/awesome_owl/static/src/playground.xml
@@ -4,6 +4,16 @@
hello world
+
+
+
Sum:
+
+
+
+
+ Hey !
+
+
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..df56ad30626
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_item.js
@@ -0,0 +1,6 @@
+import { Component, useState } from "@odoo/owl";
+
+export class TodoItem extends Component {
+ static template = "awesome_owl.todo_item";
+ static props = ['id', 'description', 'isCompleted?', 'toggleState', 'removeTodo']
+}
diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml
new file mode 100644
index 00000000000..3870e998924
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_item.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ .
+
+
+
+
+
+
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..d1fc401ecbf
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_list.js
@@ -0,0 +1,35 @@
+import { Component, useState } from "@odoo/owl";
+import { TodoItem } from "./todo_item";
+import { useAutoFocus } from "../utils"
+
+export class TodoList extends Component {
+ static template = "awesome_owl.todo_list";
+ static components = { TodoItem };
+
+
+ setup() {
+ this.state = useState({ items: [], count: 0 });
+ useAutoFocus("todo-input");
+ this.toggleItemState = this.toggleItemState.bind(this);
+ this.removeTodo = this.removeTodo.bind(this);
+ }
+
+ addTodo(ev) {
+ if (ev.key === "Enter" && ev.target.value.trim() !== ""){
+ this.state.items.push({ 'id': this.state.count++, 'description': ev.target.value, 'isCompleted': false });
+ ev.target.value = "";
+ }
+ }
+
+ toggleItemState(id) {
+ const item = this.state.items.find(item => item.id == id);
+ item['isCompleted'] = !item['isCompleted'];
+ }
+
+ removeTodo(id) {
+ const item_index = this.state.items.findIndex(item => item.id == id);
+ this.state.items.splice(item_index, 1);
+ }
+
+
+}
diff --git a/awesome_owl/static/src/todo/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml
new file mode 100644
index 00000000000..8d6cefa7af7
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_list.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js
new file mode 100644
index 00000000000..dd255757856
--- /dev/null
+++ b/awesome_owl/static/src/utils.js
@@ -0,0 +1,9 @@
+import { useRef, useEffect } from "@odoo/owl";
+
+export function useAutoFocus(name) {
+ let ref = useRef(name);
+ useEffect(
+ (el) => el && el.focus(),
+ () => [ref.el]
+ );
+}