feat: reports center page

This commit is contained in:
sokumon 2025-11-11 13:33:09 +05:30
parent 9122444258
commit 673959565c
20 changed files with 422 additions and 19 deletions

View file

@ -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(

View file

@ -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) {
// },
// });

View file

@ -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": []
}

View file

@ -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

View file

@ -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

View file

@ -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": []
}

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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;
}

View file

@ -0,0 +1,11 @@
<!-- jinja -->
<div class="reports-center-wrapper">
<h3>Reports Center</h3>
<div class="reports-list-container">
<div class="report-card mb-2 p-2 border rounded cursor-pointer" >
<div class="report-links">
</div>
</div>
</div>
</div>

View file

@ -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(`<div class="text-muted">No reports found.</div>`);
return;
}
Object.values(reports).forEach((report_data) => {
const report_html = `
<div class='report-details'>
<div class="report-name fw-bold" data-report="${report_data.title}">${report_data.title}</div>
</div>
`;
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);
// }
};

View file

@ -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"
}

View file

@ -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

View file

@ -1,4 +1,4 @@
<div class="body-sidebar-container">
<div class="body-sidebar-container {%= expanded ? 'expanded' : ''%}">
<div class="body-sidebar-placeholder"></div>
<div class="body-sidebar">
<div class="body-sidebar-top">

View file

@ -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;
// $(`<span class="text-muted">${__("Loading Filters...")}</span>`).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");

View file

@ -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]) {