diff --git a/frappe/boot.py b/frappe/boot.py
index bcd4c6c793..29c1666215 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -548,6 +548,8 @@ def get_sidebar_items():
"display_depends_on": si.display_depends_on,
"url": si.url,
"show_arrow": si.show_arrow,
+ "filters": si.filters,
+ "route_options": si.route_options,
}
if si.link_type == "Report" and si.link_to:
report_type, ref_doctype = frappe.db.get_value(
diff --git a/frappe/desk/doctype/report_center/__init__.py b/frappe/desk/doctype/report_center/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/report_center/report_center.js b/frappe/desk/doctype/report_center/report_center.js
new file mode 100644
index 0000000000..13028656e4
--- /dev/null
+++ b/frappe/desk/doctype/report_center/report_center.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2025, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Report Center", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/frappe/desk/doctype/report_center/report_center.json b/frappe/desk/doctype/report_center/report_center.json
new file mode 100644
index 0000000000..8a267eb81d
--- /dev/null
+++ b/frappe/desk/doctype/report_center/report_center.json
@@ -0,0 +1,55 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:sidebar",
+ "creation": "2025-11-10 12:49:52.421973",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "sidebar",
+ "links"
+ ],
+ "fields": [
+ {
+ "fieldname": "sidebar",
+ "fieldtype": "Link",
+ "label": "Sidebar",
+ "options": "Workspace Sidebar",
+ "unique": 1
+ },
+ {
+ "fieldname": "links",
+ "fieldtype": "Table",
+ "label": "Links",
+ "options": "Report Center Link"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2025-11-10 12:51:46.093654",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Report Center",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": []
+}
diff --git a/frappe/desk/doctype/report_center/report_center.py b/frappe/desk/doctype/report_center/report_center.py
new file mode 100644
index 0000000000..406aee33ff
--- /dev/null
+++ b/frappe/desk/doctype/report_center/report_center.py
@@ -0,0 +1,21 @@
+# Copyright (c) 2025, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class ReportCenter(Document):
+ # begin: auto-generated types
+ # This code is auto-generated. Do not modify anything in this block.
+
+ from typing import TYPE_CHECKING
+
+ if TYPE_CHECKING:
+ from frappe.desk.doctype.report_center_link.report_center_link import ReportCenterLink
+ from frappe.types import DF
+
+ links: DF.Table[ReportCenterLink]
+ sidebar: DF.Link | None
+ # end: auto-generated types
+ pass
diff --git a/frappe/desk/doctype/report_center/test_report_center.py b/frappe/desk/doctype/report_center/test_report_center.py
new file mode 100644
index 0000000000..f97f5758d2
--- /dev/null
+++ b/frappe/desk/doctype/report_center/test_report_center.py
@@ -0,0 +1,20 @@
+# Copyright (c) 2025, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests import IntegrationTestCase
+
+# On IntegrationTestCase, the doctype test records and all
+# link-field test record dependencies are recursively loaded
+# Use these module variables to add/remove to/from that list
+EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+
+
+class IntegrationTestReportCenter(IntegrationTestCase):
+ """
+ Integration tests for ReportCenter.
+ Use this class for testing interactions between multiple components.
+ """
+
+ pass
diff --git a/frappe/desk/doctype/report_center_link/__init__.py b/frappe/desk/doctype/report_center_link/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/report_center_link/report_center_link.json b/frappe/desk/doctype/report_center_link/report_center_link.json
new file mode 100644
index 0000000000..b041266f8c
--- /dev/null
+++ b/frappe/desk/doctype/report_center_link/report_center_link.json
@@ -0,0 +1,35 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2025-11-10 12:51:15.244944",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "report"
+ ],
+ "fields": [
+ {
+ "fieldname": "report",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Report",
+ "options": "Report"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2025-11-10 12:52:34.403583",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Report Center Link",
+ "owner": "Administrator",
+ "permissions": [],
+ "row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": []
+}
diff --git a/frappe/desk/doctype/report_center_link/report_center_link.py b/frappe/desk/doctype/report_center_link/report_center_link.py
new file mode 100644
index 0000000000..369261b9e0
--- /dev/null
+++ b/frappe/desk/doctype/report_center_link/report_center_link.py
@@ -0,0 +1,23 @@
+# Copyright (c) 2025, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class ReportCenterLink(Document):
+ # begin: auto-generated types
+ # This code is auto-generated. Do not modify anything in this block.
+
+ from typing import TYPE_CHECKING
+
+ if TYPE_CHECKING:
+ from frappe.types import DF
+
+ parent: DF.Data
+ parentfield: DF.Data
+ parenttype: DF.Data
+ report: DF.Link | None
+ # end: auto-generated types
+
+ pass
diff --git a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json
index 9b331e395e..ab0a89d5bc 100644
--- a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json
+++ b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json
@@ -21,7 +21,10 @@
"keep_closed",
"show_arrow",
"column_break_jexf",
- "display_depends_on"
+ "display_depends_on",
+ "section_break_whjq",
+ "filters",
+ "route_options"
],
"fields": [
{
@@ -60,7 +63,8 @@
"fieldname": "icon",
"fieldtype": "Icon",
"in_list_view": 1,
- "label": "Icon"
+ "label": "Icon",
+ "options": "Emojis"
},
{
"default": "0",
@@ -134,13 +138,29 @@
"fieldname": "show_arrow",
"fieldtype": "Check",
"label": "Show Arrow"
+ },
+ {
+ "fieldname": "section_break_whjq",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "filters",
+ "fieldtype": "Code",
+ "label": "Filters",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "route_options",
+ "fieldtype": "Code",
+ "label": "Route Options",
+ "options": "JSON"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2025-11-07 10:47:58.882767",
+ "modified": "2025-11-11 11:46:36.949606",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace Sidebar Item",
diff --git a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py
index 9f82c7271e..db9fa4eb47 100644
--- a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py
+++ b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py
@@ -17,6 +17,7 @@ class WorkspaceSidebarItem(Document):
child: DF.Check
collapsible: DF.Check
display_depends_on: DF.Code | None
+ filters: DF.Code | None
indent: DF.Check
keep_closed: DF.Check
label: DF.Data | None
@@ -25,6 +26,7 @@ class WorkspaceSidebarItem(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
+ route_options: DF.Code | None
show_arrow: DF.Check
type: DF.Literal["Link", "Section Break", "Spacer"]
url: DF.Data | None
diff --git a/frappe/desk/page/reports_center/__init__.py b/frappe/desk/page/reports_center/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/page/reports_center/reports_center.css b/frappe/desk/page/reports_center/reports_center.css
new file mode 100644
index 0000000000..6033741f3c
--- /dev/null
+++ b/frappe/desk/page/reports_center/reports_center.css
@@ -0,0 +1,14 @@
+.reports-center-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ margin-top: 20px;
+}
+.reports-list-container{
+ width: 70%;
+}
+.report-details{
+ margin-top: 4px;
+ margin-bottom: 4px;
+}
\ No newline at end of file
diff --git a/frappe/desk/page/reports_center/reports_center.html b/frappe/desk/page/reports_center/reports_center.html
new file mode 100644
index 0000000000..6de5341032
--- /dev/null
+++ b/frappe/desk/page/reports_center/reports_center.html
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/frappe/desk/page/reports_center/reports_center.js b/frappe/desk/page/reports_center/reports_center.js
new file mode 100644
index 0000000000..67c85310b7
--- /dev/null
+++ b/frappe/desk/page/reports_center/reports_center.js
@@ -0,0 +1,72 @@
+frappe.pages["reports-center"].on_page_load = async function (wrapper) {
+ var page = frappe.ui.make_app_page({
+ parent: wrapper,
+ title: "Reports Center",
+ single_column: true,
+ });
+
+ page.page_head.hide();
+ let module = frappe.route_options?.module;
+ let reports_data;
+ $(frappe.render_template("reports_center")).appendTo(page.body);
+ await frappe.call({
+ method: "frappe.desk.page.reports_center.reports_center.get_reports",
+ args: {
+ module_name: module,
+ },
+ callback: function (r) {
+ reports_data = r.message;
+ render_list(module, reports_data);
+ },
+ });
+
+ // $('.report-name').on('click', function() {
+ // const report_name = $(this).data('report');
+ // frappe.set_route('query-report', report_name);
+ // });
+};
+
+function render_list(module_name, reports) {
+ let module_name_wrapper = $(".module-name");
+ module_name_wrapper.text(module_name);
+ const container = $(".report-links");
+ container.empty();
+ if (!reports || Object.keys(reports).length === 0) {
+ container.append(`No reports found.
`);
+ return;
+ }
+
+ Object.values(reports).forEach((report_data) => {
+ const report_html = `
+
+ `;
+ container.append(report_html);
+ });
+
+ // Optional: Add click handler
+ container.find(".report-name").on("click", function () {
+ const report_name = $(this).data("report");
+ frappe.set_route("query-report", report_name);
+ });
+}
+
+frappe.pages["reports-center"].on_page_show = function (wrapper) {
+ // if (frappe.has_route_options()) {
+ // let module = frappe.route_options?.module;
+ // if (module) {
+ // const filtered = Object.fromEntries(
+ // Object.entries(frappe.boot.allowed_reports).filter(
+ // ([, value]) => value.module === module
+ // )
+ // );
+ // console.log("Filtered Reports:", filtered);
+ // render_list(module,filtered);
+ // } else {
+ // render_list(module, frappe.boot.allowed_reports);
+ // }
+ // } else {
+ // render_list(frappe.boot.allowed_reports);
+ // }
+};
diff --git a/frappe/desk/page/reports_center/reports_center.json b/frappe/desk/page/reports_center/reports_center.json
new file mode 100644
index 0000000000..7d8e0995ff
--- /dev/null
+++ b/frappe/desk/page/reports_center/reports_center.json
@@ -0,0 +1,19 @@
+{
+ "content": null,
+ "creation": "2025-11-09 23:41:57.731008",
+ "docstatus": 0,
+ "doctype": "Page",
+ "idx": 0,
+ "modified": "2025-11-09 23:41:57.731008",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "reports-center",
+ "owner": "Administrator",
+ "page_name": "reports-center",
+ "roles": [],
+ "script": null,
+ "standard": "Yes",
+ "style": null,
+ "system_page": 1,
+ "title": "Reports Center"
+}
diff --git a/frappe/desk/page/reports_center/reports_center.py b/frappe/desk/page/reports_center/reports_center.py
new file mode 100644
index 0000000000..76aae0b908
--- /dev/null
+++ b/frappe/desk/page/reports_center/reports_center.py
@@ -0,0 +1,15 @@
+from json import dumps
+
+import frappe
+from frappe.boot import get_allowed_pages, get_allowed_reports
+
+
+@frappe.whitelist()
+def get_reports(module_name=None):
+ reports_info = []
+ if module_name:
+ report_center = frappe.get_doc("Report Center", module_name)
+ for report_links in report_center.links:
+ if report_links.report in get_allowed_reports().keys():
+ reports_info.append(get_allowed_reports()[report_links.report])
+ return reports_info
diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.html b/frappe/public/js/frappe/ui/sidebar/sidebar.html
index 57e2e0f2db..97d85f380f 100644
--- a/frappe/public/js/frappe/ui/sidebar/sidebar.html
+++ b/frappe/public/js/frappe/ui/sidebar/sidebar.html
@@ -1,4 +1,4 @@
-
+
diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js
index 473e190b26..f6b84360ca 100644
--- a/frappe/public/js/frappe/ui/sidebar/sidebar.js
+++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js
@@ -31,13 +31,20 @@ frappe.ui.Sidebar = class Sidebar {
}
choose_app_name() {
- if (frappe.boot.app_name_style == "Default") return;
- frappe.boot.app_data.forEach((a) => {
- if (a.workspaces.includes(this.workspace_title)) {
- this.app_name = a.app_title;
- this.app_logo_url = a.app_logo_url;
+ if (frappe.boot.app_name_style === "Default") return;
+
+ for (const app of frappe.boot.app_data) {
+ if (app.workspaces.includes(this.workspace_title)) {
+ this.app_name = app.app_title;
+ this.app_logo_url = app.app_logo_url;
+ return;
}
- });
+ }
+
+ const icon = frappe.boot.desktop_icons.find((i) => i.label === this.workspace_title);
+ if (icon) {
+ this.app_name = icon.parent_icon;
+ }
}
find_nested_items() {
@@ -96,6 +103,7 @@ frappe.ui.Sidebar = class Sidebar {
}
}
make_dom() {
+ this.load_sidebar_state();
this.wrapper = $(
frappe.render_template("sidebar", {
expanded: this.sidebar_expanded,
@@ -122,7 +130,8 @@ frappe.ui.Sidebar = class Sidebar {
let match = false;
const that = this;
$(".item-anchor").each(function () {
- if ($(this).attr("href") == decodeURIComponent(window.location.pathname)) {
+ let href = $(this).attr("href")?.split("?")[0];
+ if (href == decodeURIComponent(window.location.pathname)) {
match = true;
if (that.active_item) that.active_item.removeClass("active-sidebar");
that.active_item = $(this).parent();
@@ -134,6 +143,15 @@ frappe.ui.Sidebar = class Sidebar {
}
set_sidebar_state() {
+ this.load_sidebar_state();
+ if (this.workspace_sidebar_items.length === 0) {
+ this.sidebar_expanded = true;
+ }
+
+ this.expand_sidebar();
+ }
+
+ load_sidebar_state() {
this.sidebar_expanded = true;
if (localStorage.getItem("sidebar-expanded") !== null) {
this.sidebar_expanded = JSON.parse(localStorage.getItem("sidebar-expanded"));
@@ -142,12 +160,6 @@ frappe.ui.Sidebar = class Sidebar {
if (frappe.is_mobile()) {
this.sidebar_expanded = false;
}
-
- if (this.workspace_sidebar_items.length === 0) {
- this.sidebar_expanded = true;
- }
-
- this.expand_sidebar();
}
empty() {
if (this.wrapper.find(".sidebar-items")[0]) {
@@ -490,6 +502,14 @@ frappe.ui.Sidebar = class Sidebar {
in_list_view: 1,
label: "Link To",
options: "link_type",
+ onchange: function () {
+ if (d.get_value("link_type") == "DocType") {
+ let doctype = this.get_value();
+ if (doctype) {
+ me.setup_filter(d, doctype);
+ }
+ }
+ },
},
{
depends_on: 'eval: doc.link_type == "URL"',
@@ -502,9 +522,14 @@ frappe.ui.Sidebar = class Sidebar {
'eval: doc.type == "Link" || (doc.indent == 1 && doc.type == "Section Break")',
fieldname: "icon",
fieldtype: "Icon",
+ options: "Emojis",
in_list_view: 1,
label: "Icon",
},
+ {
+ fieldtype: "HTML",
+ fieldname: "filter_area",
+ },
{
depends_on: 'eval: doc.type == "Section Break"',
fieldname: "display_section",
@@ -558,6 +583,17 @@ frappe.ui.Sidebar = class Sidebar {
options: "JS",
max_height: "10px",
},
+ {
+ fieldtype: "Section Break",
+ },
+ {
+ fieldname: "route_options",
+ fieldtype: "Code",
+ display_depends_on: "eval: doc.link_type == 'Page'",
+ label: "Route Options",
+ options: "JSON",
+ max_height: "50px",
+ },
];
if (opts && opts.item) {
dialog_fields.forEach((f) => {
@@ -571,12 +607,17 @@ frappe.ui.Sidebar = class Sidebar {
});
title = "Edit Sidebar Item";
}
- let d = new frappe.ui.Dialog({
+ let d;
+ this.dialog = d = new frappe.ui.Dialog({
title: title,
fields: dialog_fields,
primary_action_label: "Save",
size: "small",
primary_action(values) {
+ if (me.filter_group) {
+ me.filter_group.get_filters();
+ }
+
if (me.new_sidebar_items.length === 0) {
me.new_sidebar_items = Array.from(me.workspace_sidebar_items);
}
@@ -619,7 +660,36 @@ frappe.ui.Sidebar = class Sidebar {
return d;
}
+ setup_filter(d, doctype) {
+ if (this.filter_group) {
+ this.filter_group.wrapper.empty();
+ delete this.filter_group;
+ }
+ // let $loading = this.dialog.get_field("filter_area_loading").$wrapper;
+ // $(`${__("Loading Filters...")}`).appendTo($loading);
+
+ this.filters = [];
+
+ this.generate_filter_from_json && this.generate_filter_from_json();
+
+ this.filter_group = new frappe.ui.FilterGroup({
+ parent: d.get_field("filter_area").$wrapper,
+ doctype: doctype,
+ on_change: () => {},
+ });
+
+ frappe.model.with_doctype(doctype, () => {
+ this.filter_group.add_filters_to_filter_group(this.filters);
+ });
+ }
+ hide_field(fieldname) {
+ this.dialog.set_df_property(fieldname, "hidden", true);
+ }
+
+ show_field(fieldname) {
+ this.dialog.set_df_property(fieldname, "hidden", false);
+ }
setup_editing_controls() {
const me = this;
this.save_sidebar_button = this.wrapper.find(".save-sidebar");
diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js
index b71d464c1b..dc1a928fa2 100644
--- a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js
+++ b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js
@@ -32,6 +32,12 @@ frappe.ui.sidebar_item.TypeLink = class SidebarItem {
}
} else if (this.item.link_type === "URL") {
path = this.item.url;
+ } else if (this.item.link_type == "Page" && this.item.route_options) {
+ path = frappe.utils.generate_route({
+ type: this.item.link_type,
+ name: this.item.link_to,
+ route_options: JSON.parse(this.item.route_options),
+ });
} else {
path = frappe.utils.generate_route({
type: this.item.link_type,
@@ -176,7 +182,6 @@ frappe.ui.sidebar_item.TypeSectionBreak = class SectionBreakSidebarItem extends
.appendTo(sidebar_control);
this.$drop_icon.removeClass("hidden");
- this.setup_event_listner($item_container);
}
if (item.keep_closed) {
@@ -190,6 +195,8 @@ frappe.ui.sidebar_item.TypeSectionBreak = class SectionBreakSidebarItem extends
}
if (item.show_arrow) {
this.$drop_icon = this.wrapper.find('[item-icon="chevron-right"]');
+ }
+ if (item.collapsible || item.show_arrow) {
this.setup_event_listner();
}
}
@@ -214,6 +221,15 @@ frappe.ui.sidebar_item.TypeSectionBreak = class SectionBreakSidebarItem extends
me.save_section_break_state();
}
});
+
+ $(this.wrapper.find(".standard-sidebar-item")[0]).on("click", (e) => {
+ me.collapsed = me.$drop_icon.find("use").attr("href") === "#icon-chevron-down";
+ me.toggle();
+
+ if (e.originalEvent.isTrusted) {
+ me.save_section_break_state();
+ }
+ });
}
save_section_break_state() {
if (!this.section_breaks_state[this.workspace_title]) {