diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py
index 31406e8addb..9ee31a5f7a3 100644
--- a/awesome_dashboard/__manifest__.py
+++ b/awesome_dashboard/__manifest__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
{
- 'name': "Awesome Dashboard",
+ 'name': 'Awesome Dashboard',
'summary': """
Starting module for "Discover the JS framework, chapter 2: Build a dashboard"
@@ -10,8 +10,8 @@
Starting module for "Discover the JS framework, chapter 2: Build a dashboard"
""",
- 'author': "Odoo",
- 'website': "https://www.odoo.com/",
+ 'author': 'Odoo',
+ 'website': 'https://www.odoo.com/',
'category': 'Tutorials/AwesomeDashboard',
'version': '0.1',
'application': True,
@@ -24,7 +24,11 @@
'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/i18n/fr.po b/awesome_dashboard/i18n/fr.po
new file mode 100644
index 00000000000..aafd453f36d
--- /dev/null
+++ b/awesome_dashboard/i18n/fr.po
@@ -0,0 +1,119 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * awesome_dashboard
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-05-30 09:40+0000\n"
+"PO-Revision-Date: 2025-05-30 09:40+0000\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard_items.js:0
+msgid "Average amount of t-shirt"
+msgstr "Montant moyen des t-shirts"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard_items.js:0
+msgid "Average amount of t-shirt by order this month"
+msgstr "Montant moyen des t-shirts par commande ce mois-ci"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard_items.js:0
+msgid "Average time for an order"
+msgstr "Temps moyen pour une commande"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard_items.js:0
+msgid "Average time for an order to go from 'new' to 'sent' or 'cancelled'"
+msgstr ""
+"Temps moyen pour qu'une commande passe de « nouvelle » à « envoyée » ou « "
+"annulée »"
+
+#. module: awesome_dashboard
+#: model:ir.ui.menu,name:awesome_dashboard.menu_root
+msgid "Awesome Dashboard"
+msgstr "Tableau de bord génial"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard_items.js:0
+msgid "Cancelled orders this month"
+msgstr "Commandes annulées ce mois-ci"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard.xml:0
+msgid "Customers"
+msgstr "Clients"
+
+#. module: awesome_dashboard
+#: model:ir.actions.client,name:awesome_dashboard.dashboard
+#: model:ir.ui.menu,name:awesome_dashboard.dashboard_menu
+msgid "Dashboard"
+msgstr "Tableau de bord"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard.xml:0
+msgid "Done"
+msgstr "Terminé"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard.xml:0
+msgid "Leads"
+msgstr "Pistes"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard_items.js:0
+msgid "New orders this month"
+msgstr "Nouvelles commandes ce mois-ci"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard_items.js:0
+msgid "Number of cancelled orders this month"
+msgstr "Nombre de commandes annulées ce mois-ci"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard_items.js:0
+msgid "Number of new orders this month"
+msgstr "Nombre de nouvelles commandes ce mois-ci"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard_items.js:0
+msgid "Shirt orders by size"
+msgstr "Commandes de t-shirts par taille"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard_items.js:0
+msgid "Total amount of new orders this month"
+msgstr "Montant total des nouvelles commandes ce mois-ci"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard.xml:0
+msgid "Which cards do you whish to see ?"
+msgstr "Quelles cartes souhaitez-vous voir ?"
+
+#. module: awesome_dashboard
+#. odoo-javascript
+#: code:addons/awesome_dashboard/static/src/dashboard/dashboard_items.js:0
+msgid "amount orders this month"
+msgstr "montant des commandes ce mois-ci"
diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js
deleted file mode 100644
index 637fa4bb972..00000000000
--- a/awesome_dashboard/static/src/dashboard.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/** @odoo-module **/
-
-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/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js
new file mode 100644
index 00000000000..39c3982538e
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.js
@@ -0,0 +1,86 @@
+/** @odoo-module **/
+
+import { Component, useState } from "@odoo/owl";
+import { LazyComponent } from "@web/core/assets";
+import { browser } from "@web/core/browser/browser";
+import { CheckBox } from "@web/core/checkbox/checkbox";
+import { Dialog } from "@web/core/dialog/dialog";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+import { Layout } from "@web/search/layout";
+import { PieChart } from "../pie_chart/pie_chart";
+import { DashboardItem } from "./dashboard_item/dashboard_item";
+
+class AwesomeDashboard extends Component {
+ static template = "awesome_dashboard.dashboard";
+ static components = { Layout, DashboardItem, PieChart, LazyComponent }
+ async setup() {
+ super.setup();
+ this.display = {
+ controlPanel: {},
+ };
+ this.action = useService("action");
+ this.statistics = useState(useService("awesome_dashboard.statistics"));
+ this.items = registry.category("awesome_dashboard").getAll();
+ this.dialog = useService("dialog");
+ this.state = useState({
+ uncheckedItems: browser.localStorage.getItem("uncheckedItems")?.split(",") || [],
+ });
+ }
+
+ openCustomers() {
+ this.action.doAction("base.action_partner_form");
+ }
+
+ openLeads() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: "All leads",
+ res_model: "crm.lead",
+ views: [
+ [false, "list"],
+ [false, "form"],
+ ],
+ });
+ }
+
+ openConfigurations() {
+ this.dialog.add(DashboardConfiguration, {
+ items: this.items,
+ uncheckedItems: this.state.uncheckedItems,
+ update: this.update.bind(this),
+ });
+ }
+
+ update(updatedUncheckedItems) {
+ this.state.uncheckedItems = updatedUncheckedItems;
+ }
+}
+
+class DashboardConfiguration extends Component {
+ static template = "awesome_dashboard.dashboard_configuration";
+ static components = { Dialog, CheckBox };
+ static props = ["close", "items", "uncheckedItems", "update"];
+
+ setup() {
+ this.items = useState(this.props.items.map((item) => {
+ return {
+ ...item,
+ enabled: !this.props.uncheckedItems.includes(item.id),
+ }
+ }));
+ }
+
+ done() {
+ const updatedUncheckedItems = this.items.filter((i) => !i.enabled).map((i) => i.id);
+ browser.localStorage.setItem("uncheckedItems", updatedUncheckedItems);
+ this.props.update(updatedUncheckedItems);
+ this.props.close();
+ }
+
+ update(ev, item) {
+ item.enabled = ev.target.checked;
+ }
+}
+
+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..066914db228
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.scss
@@ -0,0 +1,3 @@
+.o_dashboard {
+ background-color: #7f5a7b;
+}
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml
new file mode 100644
index 00000000000..efa7b485046
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..d6262ae0c44
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js
@@ -0,0 +1,18 @@
+import { Component } from "@odoo/owl";
+
+export class DashboardItem extends Component {
+ static template = "awesome_dashboard.dashboard_item";
+ static props = {
+ "size": {
+ type: Number,
+ optional: true,
+ default: 1
+ },
+ slots: {
+ type: Object,
+ shape: {
+ default: true
+ },
+ }
+ };
+}
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..9eac1c38a83
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
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..a5e19bc57d5
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js
@@ -0,0 +1,68 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { NumberCard } from "./number_card/number_card";
+import { PieChartCard } from "./pie_chart_card/pie_chart_card";
+import { _t } from "@web/core/l10n/translation";
+
+const items = [
+ {
+ id: "average_quantity",
+ description: _t("Average amount of t-shirt"),
+ Component: NumberCard,
+ 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: "number_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: "cancelled_orders",
+ description: _t("Cancelled orders this month"),
+ Component: NumberCard,
+ props: (data) => ({
+ title: _t("Number of cancelled orders this month"),
+ value: data.nb_cancelled_orders,
+ })
+ },
+ {
+ id: "amount_new_orders",
+ description: _t("amount orders this month"),
+ Component: NumberCard,
+ props: (data) => ({
+ title: _t("Total amount of new orders this month"),
+ value: data.total_amount,
+ })
+ },
+ {
+ id: "pie_chart",
+ description: _t("Shirt orders by size"),
+ Component: PieChartCard,
+ size: 2,
+ props: (data) => ({
+ title: _t("Shirt orders by size"),
+ values: data.orders_by_size,
+ })
+ }
+]
+
+items.forEach(item => {
+ registry.category("awesome_dashboard").add(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..fb35d41957f
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js
@@ -0,0 +1,15 @@
+/** @odoo-module **/
+
+import { Component } from "@odoo/owl";
+
+export class NumberCard extends Component {
+ static template = "awesome_dashboard.number_card";
+ static props = {
+ title: {
+ type: String,
+ },
+ value: {
+ type: Number,
+ },
+ }
+}
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..59043eba235
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
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..91657329fdb
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js
@@ -0,0 +1,17 @@
+/** @odoo-module **/
+
+import { Component } from "@odoo/owl";
+import { PieChart } from "../../pie_chart/pie_chart";
+
+export class PieChartCard extends Component {
+ static template = "awesome_dashboard.pie_chart_card";
+ static components = { PieChart };
+ static props = {
+ title: {
+ type: String,
+ },
+ values: {
+ type: Object,
+ },
+ };
+}
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..5f27c515862
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js
new file mode 100644
index 00000000000..32b7f652738
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_action.js
@@ -0,0 +1,10 @@
+import { Component, xml } from "@odoo/owl";
+import { LazyComponent } from "@web/core/assets";
+import { registry } from "@web/core/registry";
+
+class AwesomeDashboardAction extends Component {
+ static components = { LazyComponent };
+ static template = xml``;
+}
+
+registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardAction);
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..005c57890e2
--- /dev/null
+++ b/awesome_dashboard/static/src/pie_chart/pie_chart.js
@@ -0,0 +1,41 @@
+import { Component, onMounted, onWillStart, onWillUnmount, useRef } from "@odoo/owl";
+import { loadJS } from "@web/core/assets";
+import { getColor } from "@web/core/colors/colors";
+
+export class PieChart extends Component {
+ static template = "awesome_dashboard.pie_chart";
+ static props = {
+ label: String,
+ data: Object
+ };
+
+ setup() {
+ this.canvasRef = useRef("canvas");
+ onWillStart(() => loadJS(["/web/static/lib/Chart/Chart.js"]));
+ onMounted(() => {
+ this.renderChart();
+ });
+ onWillUnmount(() => {
+ this.chart.destroy();
+ });
+ }
+
+ renderChart() {
+ const labels = Object.keys(this.props.data);
+ const data = Object.values(this.props.data);
+ const color = labels.map((_, index) => getColor(index));
+ this.chart = new Chart(this.canvasRef.el, {
+ type: "pie",
+ data: {
+ labels: labels,
+ datasets: [
+ {
+ label: this.props.label,
+ data: data,
+ backgroundColor: color,
+ },
+ ],
+ },
+ });
+ }
+}
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..242451cf26c
--- /dev/null
+++ b/awesome_dashboard/static/src/pie_chart/pie_chart.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/services/statistics_service.js b/awesome_dashboard/static/src/services/statistics_service.js
new file mode 100644
index 00000000000..aac02baf7da
--- /dev/null
+++ b/awesome_dashboard/static/src/services/statistics_service.js
@@ -0,0 +1,20 @@
+/** @odoo-module **/
+
+import { reactive } from "@odoo/owl";
+import { rpc } from "@web/core/network/rpc";
+import { registry } from "@web/core/registry";
+
+const statisticsService = {
+ start(env) {
+ const statistics = reactive({ isReady: false });
+ async function loadData() {
+ const updates = await rpc("/awesome_dashboard/statistics");
+ Object.assign(statistics, updates, { isReady: true });
+ }
+ setInterval(loadData, 10 * 60 *1000);
+ loadData();
+ 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..24adb684763
--- /dev/null
+++ b/awesome_owl/static/src/card/card.js
@@ -0,0 +1,25 @@
+import { Component, useState } from "@odoo/owl";
+
+export class Card extends Component {
+ static template = "awesome_owl.card";
+ static props = {
+ "title": {
+ type: String,
+ validate: s => s.startsWith("[Odoo]")
+ },
+ slots: {
+ type: Object,
+ shape: {
+ default: true
+ },
+ }
+ }
+
+ setup() {
+ this.state = useState({value: true});
+ }
+
+ toggleState() {
+ this.state.value = !this.state.value;
+ }
+}
diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml
new file mode 100644
index 00000000000..04a1bed081b
--- /dev/null
+++ b/awesome_owl/static/src/card/card.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+ Title:
+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js
new file mode 100644
index 00000000000..9597136d7ca
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.js
@@ -0,0 +1,21 @@
+import { Component, useState } 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: 0 });
+ }
+
+ 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..01755ae4006
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+ Counter:
+
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js
index 657fb8b07bb..fead2823420 100644
--- a/awesome_owl/static/src/playground.js
+++ b/awesome_owl/static/src/playground.js
@@ -1,7 +1,20 @@
/** @odoo-module **/
-import { Component } from "@odoo/owl";
+import { Component, useState } from "@odoo/owl";
+import { Card } from "./card/card";
+import { Counter } from "./counter/counter";
+import { TodoList } from "./todo_list/todo_list";
export class Playground extends Component {
static template = "awesome_owl.playground";
+ static components = { Counter, Card, TodoList };
+
+ setup() {
+ this.title = "[Odoo] New version here!!!";
+ this.sum = useState({value: 0});
+ }
+
+ incrementSum() {
+ this.sum.value++;
+ }
}
diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml
index 4fb905d59f9..c8ce7b6468b 100644
--- a/awesome_owl/static/src/playground.xml
+++ b/awesome_owl/static/src/playground.xml
@@ -4,7 +4,19 @@
hello world
+
+
+
+
+ Total counter sum:
+
+
+
+
+
+
+
+
-
diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js
new file mode 100644
index 00000000000..d47928ff5ee
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_item.js
@@ -0,0 +1,25 @@
+import { Component } from "@odoo/owl";
+
+export class TodoItem extends Component {
+ static template = "awesome_owl.TodoItem";
+ static props = {
+ todo: {
+ type: Object,
+ shape: {
+ id: Number,
+ description: String,
+ isCompleted: Boolean
+ }
+ },
+ toggleState: Function,
+ deleteTodo: Function
+ }
+
+ onChange() {
+ this.props.toggleState(this.props.todo.id);
+ }
+
+ onDelete() {
+ this.props.deleteTodo(this.props.todo.id);
+ }
+}
diff --git a/awesome_owl/static/src/todo_list/todo_item.xml b/awesome_owl/static/src/todo_list/todo_item.xml
new file mode 100644
index 00000000000..a6e42688ddd
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_item.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js
new file mode 100644
index 00000000000..a97d431cde2
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_list.js
@@ -0,0 +1,41 @@
+import { Component, useState } from "@odoo/owl";
+import { useAutofocus } from "../utils";
+import { TodoItem } from "./todo_item";
+
+export class TodoList extends Component {
+ static template = "awesome_owl.TodoList";
+ static components = {TodoItem};
+
+ setup() {
+ this.idCounter = 0;
+ this.todos = useState([]);
+ useAutofocus("input");
+ this.toggleState = this.toggleState.bind(this);
+ this.deleteTodo = this.deleteTodo.bind(this);
+ }
+
+ addTodo(ev) {
+ if (ev.keyCode === 13 && ev.target.value != "") {
+ this.todos.push({
+ id: this.idCounter++,
+ description: ev.target.value,
+ isCompleted: false
+ });
+ ev.target.value = "";
+ }
+ }
+
+ toggleState(id) {
+ const todo = this.todos.find((t) => t.id === id);
+ if (todo) {
+ todo.isCompleted = !todo.isCompleted;
+ }
+ }
+
+ deleteTodo(id) {
+ const index = this.todos.findIndex((t) => t.id === id);
+ if (index >= 0) {
+ this.todos.splice(index, 1);
+ }
+ }
+}
diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml
new file mode 100644
index 00000000000..b903290cd5c
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_list.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
Todo List
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js
new file mode 100644
index 00000000000..a392f3f1be2
--- /dev/null
+++ b/awesome_owl/static/src/utils.js
@@ -0,0 +1,8 @@
+import { onMounted, useRef } from "@odoo/owl";
+
+export function useAutofocus(refName) {
+ var inputRef = useRef(refName);
+ onMounted(() => {
+ inputRef.el.focus();
+ });
+}
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000000..cccaef5d2ce
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,17 @@
+[tool.ruff]
+line-length = 120
+indent-width = 4
+target-version = "py312"
+
+[tool.ruff.lint]
+select = ["E", "F", "Q", "I"]
+ignore = ["F401"]
+
+
+[tool.ruff.lint.flake8-quotes]
+inline-quotes = "single"
+multiline-quotes = "double"
+docstring-quotes = "double"
+
+[tool.ruff.format]
+quote-style = "single"