diff --git a/awesome_dashboard/static/src/dashboard-item/dashboard-item.js b/awesome_dashboard/static/src/dashboard-item/dashboard-item.js
new file mode 100644
index 00000000000..ab480f80323
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard-item/dashboard-item.js
@@ -0,0 +1,9 @@
+import { Component } from "@odoo/owl";
+
+export class DashboardItem extends Component {
+ static template = "awesome_dashboard.dashboard-item";
+ static props = {
+ size: Number,
+ slots: { type: Object, optional: true },
+ };
+}
diff --git a/awesome_dashboard/static/src/dashboard-item/dashboard-item.xml b/awesome_dashboard/static/src/dashboard-item/dashboard-item.xml
new file mode 100644
index 00000000000..f0c6d50d65c
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard-item/dashboard-item.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js
index c4fb245621b..91a99d78eb8 100644
--- a/awesome_dashboard/static/src/dashboard.js
+++ b/awesome_dashboard/static/src/dashboard.js
@@ -1,8 +1,91 @@
-import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+import { Layout } from "@web/search/layout";
+import { Component, useState } from "@odoo/owl";
+import { DashboardItem } from "./dashboard-item/dashboard-item"
+import { NumberCard } from "./number-card/number-card"
+import { PieChartCard } from "./pie-chart/pie-chart-card"
class AwesomeDashboard extends Component {
static template = "awesome_dashboard.AwesomeDashboard";
+ static components = { Layout, DashboardItem, NumberCard, PieChartCard };
+
+ setup() {
+ this.action = useService("action");
+ this.statistics = useState(useService("awesome_dashboard.statistics"));
+
+ // Dashboard items
+ this.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: "Ordered T-shirts by size",
+ component: PieChartCard,
+ size: 2,
+ props: (data) => ({
+ title: "Ordered T-shirts by size",
+ value: data.orders_by_size,
+ }),
+ },
+ ];
+ }
+
+ actionCustomers() {
+ this.action.doAction("base.action_partner_form", { viewType: "kanban" });
+ }
+
+ actionLeads() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: "All leads",
+ res_model: "crm.lead",
+ views: [[false, "list"], [false, "form"]],
+ });
+ }
}
registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard);
diff --git a/awesome_dashboard/static/src/dashboard.scss b/awesome_dashboard/static/src/dashboard.scss
new file mode 100644
index 00000000000..32862ec0d82
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard.scss
@@ -0,0 +1,3 @@
+.o_dashboard {
+ background-color: gray;
+}
diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml
index 1a2ac9a2fed..8f60c06385a 100644
--- a/awesome_dashboard/static/src/dashboard.xml
+++ b/awesome_dashboard/static/src/dashboard.xml
@@ -2,7 +2,22 @@
- hello dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/number-card/number-card.js b/awesome_dashboard/static/src/number-card/number-card.js
new file mode 100644
index 00000000000..173497ff13f
--- /dev/null
+++ b/awesome_dashboard/static/src/number-card/number-card.js
@@ -0,0 +1,9 @@
+import { Component } from "@odoo/owl";
+
+export class NumberCard extends Component {
+ static template = "awesome_dashboard.number-card";
+ static props = {
+ title: String,
+ value: Number,
+ };
+}
diff --git a/awesome_dashboard/static/src/number-card/number-card.scss b/awesome_dashboard/static/src/number-card/number-card.scss
new file mode 100644
index 00000000000..5321e9b2a13
--- /dev/null
+++ b/awesome_dashboard/static/src/number-card/number-card.scss
@@ -0,0 +1,5 @@
+.number-card-value {
+ color: green;
+ font-weight: bold;
+ text-align: center;
+}
diff --git a/awesome_dashboard/static/src/number-card/number-card.xml b/awesome_dashboard/static/src/number-card/number-card.xml
new file mode 100644
index 00000000000..b349bb6da35
--- /dev/null
+++ b/awesome_dashboard/static/src/number-card/number-card.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/pie-chart/pie-chart-card.js b/awesome_dashboard/static/src/pie-chart/pie-chart-card.js
new file mode 100644
index 00000000000..4e2e767c05d
--- /dev/null
+++ b/awesome_dashboard/static/src/pie-chart/pie-chart-card.js
@@ -0,0 +1,11 @@
+import { Component } from "@odoo/owl";
+import { PieChart } from "./pie-chart";
+
+export class PieChartCard extends Component {
+ static template = "awesome_dashboard.pie-chart-card";
+ static props = {
+ title: String,
+ value: { type: Object, values: Number },
+ };
+ static components = { PieChart };
+}
diff --git a/awesome_dashboard/static/src/pie-chart/pie-chart-card.xml b/awesome_dashboard/static/src/pie-chart/pie-chart-card.xml
new file mode 100644
index 00000000000..eff186e06bd
--- /dev/null
+++ b/awesome_dashboard/static/src/pie-chart/pie-chart-card.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/pie-chart/pie-chart.js b/awesome_dashboard/static/src/pie-chart/pie-chart.js
new file mode 100644
index 00000000000..46bc6ba3dfd
--- /dev/null
+++ b/awesome_dashboard/static/src/pie-chart/pie-chart.js
@@ -0,0 +1,50 @@
+import { Component, onWillStart, onWillUnmount, onMounted, useRef, onWillUpdateProps } from "@odoo/owl";
+import { loadJS } from "@web/core/assets";
+import { DashboardItem } from "../dashboard-item/dashboard-item";
+
+export class PieChart extends Component {
+ static template = "awesome_dashboard.pie-chart";
+ static components = { DashboardItem };
+ static props = {
+ data: { type: Object, values: Number },
+ }
+
+ setup() {
+ this.canvasRef = useRef("canvas");
+ this.chart = null;
+
+ onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js"));
+ onMounted(this.renderChart);
+ onWillUpdateProps(this.updateChart);
+ onWillUnmount(this.destroyChart);
+ }
+
+ renderChart() {
+ this.destroyChart();
+
+ const labels = Object.keys(this.props.data);
+ const datapoints = Object.values(this.props.data);
+ this.chart = new Chart(this.canvasRef.el, {
+ type: "doughnut",
+ data: {
+ labels: labels,
+ datasets: [{ data: datapoints }],
+ },
+ });
+ }
+
+ updateChart(newProps) {
+ if (this.chart) {
+ const datapoints = Object.values(newProps.data);
+ Object.assign(this.chart.data.datasets[0], { data: datapoints });
+ this.chart.update();
+ }
+ }
+
+ destroyChart() {
+ if (this.chart) {
+ this.chart.destroy();
+ this.chart = null;
+ }
+ }
+}
diff --git a/awesome_dashboard/static/src/pie-chart/pie-chart.xml b/awesome_dashboard/static/src/pie-chart/pie-chart.xml
new file mode 100644
index 00000000000..a01e05e71d6
--- /dev/null
+++ b/awesome_dashboard/static/src/pie-chart/pie-chart.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/statistics-service.js b/awesome_dashboard/static/src/statistics-service.js
new file mode 100644
index 00000000000..0c97dc57973
--- /dev/null
+++ b/awesome_dashboard/static/src/statistics-service.js
@@ -0,0 +1,23 @@
+import { registry } from "@web/core/registry";
+import { rpc } from "@web/core/network/rpc";
+import { reactive } from "@odoo/owl";
+
+export const statisticsService = {
+ start() {
+ let statistics = reactive({ isReady: false });
+ async function loadStatistics() {
+ Object.assign(statistics, await rpc("/awesome_dashboard/statistics"), { isReady: true });
+ }
+
+ // Refresh every 10 seconds for testing
+ //const interval = 10000;
+ // Refresh every 10 minutes for real
+ const interval = 600000;
+
+ setInterval(loadStatistics, interval);
+ loadStatistics();
+ return statistics;
+ },
+};
+
+registry.category("services").add("awesome_dashboard.statistics", statisticsService);
diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js
new file mode 100644
index 00000000000..940e461f25c
--- /dev/null
+++ b/awesome_owl/static/src/card/card.js
@@ -0,0 +1,17 @@
+import { useState, Component } from "@odoo/owl";
+
+export class Card extends Component {
+ static template = "awesome_owl.card";
+ static props = {
+ title: String,
+ slots: {type: Object, optional: true},
+ };
+
+ setup() {
+ this.state = useState({ visible: true });
+ }
+
+ toggleVisible() {
+ this.state.visible = !this.state.visible;
+ }
+}
diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml
new file mode 100644
index 00000000000..57ff80c37b6
--- /dev/null
+++ b/awesome_owl/static/src/card/card.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js
new file mode 100644
index 00000000000..d6d92784524
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.js
@@ -0,0 +1,22 @@
+import { useState, Component } from "@odoo/owl";
+
+export class Counter extends Component {
+ static template = "awesome_owl.counter";
+ static props = {
+ onChange: {
+ type: Function,
+ optional: true,
+ },
+ };
+
+ setup() {
+ this.state = useState({ value: 1 });
+ }
+
+ increment() {
+ this.state.value++;
+ if (this.props.onChange) {
+ 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..08590167539
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ Counter:
+
+
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js
index 4ac769b0aa5..560db73de0a 100644
--- a/awesome_owl/static/src/playground.js
+++ b/awesome_owl/static/src/playground.js
@@ -1,5 +1,20 @@
-import { Component } from "@odoo/owl";
+import { markup, useState, Component } 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 components = { Counter, Card, TodoList };
+
+ setup() {
+ this.state = useState({ sum: 2 });
+ }
+
+ incrementSum() {
+ this.state.sum++;
+ }
+
+ someHtmlEscaped = "My content
";
+ someHtml = markup("My content
");
}
diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml
index 4fb905d59f9..5699ac0c128 100644
--- a/awesome_owl/static/src/playground.xml
+++ b/awesome_owl/static/src/playground.xml
@@ -2,9 +2,22 @@
-
- hello world
-
+
+
+
+
+
+
+
+
+
+
+
+ The sum is
+
+
+
+
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..3f05f1ddc7d
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo-item.js
@@ -0,0 +1,13 @@
+import { Component } from "@odoo/owl";
+
+export class TodoItem extends Component {
+ static template = "awesome_owl.todo-item";
+ static props = {
+ todo: {
+ type: Object,
+ shape: { id: Number, description: String, isCompleted: Boolean},
+ },
+ toggleState: Function,
+ removeTodo: Function,
+ };
+}
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..572f326dc9a
--- /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..d54a0813c34
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo-list.js
@@ -0,0 +1,38 @@
+import { onMounted, useRef, useState, Component } 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({todos: [], nextId: 1});
+ useAutoFocus("new-todo-input");
+ }
+
+ input_event_handler(event) {
+ // keyCode is deprecated, use key instead
+ if (event.key === "Enter") {
+ if (event.target.value) {
+ this.state.todos.push({id: this.state.nextId, description: event.target.value, isCompleted: false});
+ this.state.nextId++;
+ event.target.value = "";
+ }
+ }
+ }
+
+ toggleState(todoId) {
+ const selectedTodo = this.state.todos.find((todo) => todo.id === todoId);
+ if (selectedTodo) {
+ selectedTodo.isCompleted = !selectedTodo.isCompleted;
+ }
+ }
+
+ removeTodo(todoId) {
+ const index = this.state.todos.findIndex((todo) => todo.id === todoId);
+ if (index >= 0) {
+ this.state.todos.splice(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..f38db5b98f1
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo-list.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js
new file mode 100644
index 00000000000..4d9e30c9eff
--- /dev/null
+++ b/awesome_owl/static/src/utils.js
@@ -0,0 +1,9 @@
+import { useEffect, useRef } from "@odoo/owl";
+
+export function useAutoFocus(name) {
+ let ref = useRef(name);
+ useEffect(
+ (el) => el && el.focus(),
+ () => [ref.el]
+ );
+}