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] + ); +}