From f7e896a6957a8c8f77e610c18f6c9660ee8c3cc8 Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Fri, 5 Jun 2026 12:26:27 +0530 Subject: [PATCH 1/6] fix(backend): update dashboard stats to handle multiple user stages and child table status --- .../audit_management/dashboard.py | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/audit_management/audit_management/dashboard.py b/audit_management/audit_management/dashboard.py index f2f03f1..216bf7f 100644 --- a/audit_management/audit_management/dashboard.py +++ b/audit_management/audit_management/dashboard.py @@ -47,29 +47,41 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, elif isinstance(time_filter, list): time_filter_list = time_filter try: + # Get All User's Assigned Stages + assigned_stages = frappe.get_all("Audit Items", + filters={"user_id": user, "status": ["in", ["Pending", "No Response", "Responded"]]}, + fields=["stage_name"], pluck="stage_name") + user_stages = list(set(assigned_stages)) if assigned_stages else ["BM"] + # 1. 🟢 FETCH PENDING FOR ME (From Child Table - Stage User Logic) pending_items_query = """ SELECT DISTINCT parent FROM `tabAudit Items` WHERE status = 'Pending' AND (user_id = %s OR email = %s) + AND stage_name IN %s """ responded_items_query = """ SELECT DISTINCT parent FROM `tabAudit Items` WHERE status = 'Responded' AND (user_id = %s OR email = %s) + AND stage_name IN %s """ not_responded_items_query = """ SELECT DISTINCT parent FROM `tabAudit Items` WHERE status = 'No Response' AND (user_id = %s OR email = %s) + AND stage_name IN %s """ - pending_records = frappe.db.sql(pending_items_query, (user, user), as_dict=True) - responded_records = frappe.db.sql(responded_items_query, (user, user), as_dict=True) - not_responded_records = frappe.db.sql(not_responded_items_query, (user, user), as_dict=True) + # Ensure user_stages is a tuple for SQL IN clause + stages_tuple = tuple(user_stages) if len(user_stages) > 1 else (user_stages[0],) + + pending_records = frappe.db.sql(pending_items_query, (user, user, stages_tuple), as_dict=True) + responded_records = frappe.db.sql(responded_items_query, (user, user, stages_tuple), as_dict=True) + not_responded_records = frappe.db.sql(not_responded_items_query, (user, user, stages_tuple), as_dict=True) pending_for_me_count = len(pending_records) responded_by_me_count = len(responded_records) @@ -80,12 +92,16 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, # Identify which parent records to fetch for the stage user UI list selected_parents_stage_user = [] + target_child_status = None if 'Responded' in status_list: selected_parents_stage_user = [r.parent for r in responded_records] + target_child_status = 'Responded' elif 'No Response' in status_list: selected_parents_stage_user = [r.parent for r in not_responded_records] + target_child_status = 'No Response' elif 'Pending' in status_list: selected_parents_stage_user = [r.parent for r in pending_records] + target_child_status = 'Pending' else: selected_parents_stage_user = list(set( [r.parent for r in pending_records] + @@ -95,9 +111,7 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, if selected_parents_stage_user: p_filters = {"name": ["in", selected_parents_stage_user]} - if status_list: - actual_statuses = [s for s in status_list if s in ['Draft', 'Pending', 'Closed']] - if actual_statuses: p_filters["status"] = ["in", actual_statuses] + # ... parent filters ... (status filter removed for stage_user to show based on child) if risk_list: if "Normal" in risk_list: p_filters["risk"] = ["in", risk_list + [None, ""]] @@ -119,8 +133,15 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, has_more_pending = True pending_for_me_list = pending_for_me_list[:page_length] + # Override parent status with child status for stage_user for idx, item in enumerate(pending_for_me_list, start=pending_start + 1): item["sr_no"] = idx + if target_child_status: + item["status"] = target_child_status + else: + # Fetch actual child status for the user's stage(s) + item["status"] = frappe.db.get_value("Audit Items", + {"parent": item.name, "user_id": user, "stage_name": ["in", user_stages]}, "status") or item.status # 2. 🔵 FETCH GLOBAL/ROLE STATS (Manager/Admin/Member) from audit_management.audit_management.utils import get_user_allowed_divisions @@ -263,6 +284,7 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, return { "role_type": "manager" if (is_manager or is_admin) else ("member" if is_member else "stage_user"), + "user_stages": user_stages if (is_manager or is_admin or is_member) == False else [], "pending_for_me": pending_for_me_count, "responded_by_me": responded_by_me_count if (is_member or is_manager or is_admin) == False else responded_count_manager, "not_responded_count": not_responded_count_me if (is_member or is_manager or is_admin) == False else not_responded_count_manager, From f39f690680f4b265e0bb97ca18fc8294fcbb22d0 Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Fri, 5 Jun 2026 12:26:27 +0530 Subject: [PATCH 2/6] fix(frontend): update card redirection to use child table filters and fix total card clear --- audit_management/fixtures/custom_html_block.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audit_management/fixtures/custom_html_block.json b/audit_management/fixtures/custom_html_block.json index 1d78b50..b74557c 100644 --- a/audit_management/fixtures/custom_html_block.json +++ b/audit_management/fixtures/custom_html_block.json @@ -7,7 +7,7 @@ "name": "Audit Management", "private": 0, "roles": [], - "script": "(function () {\n const root = root_element || document;\n let userRole = '';\n let currentStatusFilter = [];\n let currentRiskFilter = [];\n let pendingStart = 0;\n let recentStart = 0;\n let currentItemStages = [];\n let currentTimeFilter = [];\n let currentQueryTypeFilter = [];\n\n const STORAGE_KEY = `audit_dashboard_settings_${frappe.session.user}`;\n\n const saveFilters = () => {\n const data = {\n status: currentStatusFilter,\n risk: currentRiskFilter,\n stages: currentItemStages,\n time: currentTimeFilter,\n query_type: currentQueryTypeFilter\n };\n localStorage.setItem(STORAGE_KEY, JSON.stringify(data));\n };\n\n const loadFilters = () => {\n const saved = localStorage.getItem(STORAGE_KEY);\n if (saved) {\n try {\n const data = JSON.parse(saved);\n currentStatusFilter = data.status || [];\n currentRiskFilter = data.risk || [];\n currentItemStages = data.stages || [];\n currentTimeFilter = data.time || [];\n currentQueryTypeFilter = data.query_type || [];\n } catch (e) { console.error('Error loading filters', e); }\n }\n };\n\n loadFilters();\n\n const upd = (id, val) => { \n const el = root.querySelector('#' + id); \n if (el) el.innerText = val ?? 0; \n };\n\n window.toggle_dropdown = (e) => {\n e.stopPropagation();\n const dd = root.querySelector('#actions-dropdown');\n if (dd) dd.style.display = dd.style.display === 'none' ? 'block' : 'none';\n };\n\n window.toggle_filter_dropdown = (e, type) => {\n e.stopPropagation();\n root.querySelectorAll('.multiselect-list').forEach(d => {\n if (d.id !== 'filter-dropdown-' + type) d.style.display = 'none';\n });\n const dd = root.querySelector('#filter-dropdown-' + type);\n if (dd) dd.style.display = dd.style.display === 'none' ? 'block' : 'none';\n };\n\n window.addEventListener('click', () => {\n root.querySelectorAll('.custom-dropdown, .multiselect-list').forEach(d => d.style.display = 'none');\n });\n\n const resetChildFilters = () => {\n currentItemStages = [];\n currentTimeFilter = [];\n currentQueryTypeFilter = [];\n root.querySelectorAll('.stage-item-checkbox, .time-checkbox, .query-type-item-checkbox').forEach(cb => cb.checked = false);\n };\n\n window.handle_checkbox_change = (type) => {\n const checkboxes = root.querySelectorAll('.' + type + '-checkbox:checked');\n const selected = Array.from(checkboxes).map(cb => cb.value);\n if (type === 'status') currentStatusFilter = selected; else currentRiskFilter = selected;\n \n const label = root.querySelector('#selected-' + type + '-label');\n const defaultLabel = type.charAt(0).toUpperCase() + type.slice(1);\n \n if (selected.length === 0) label.innerText = defaultLabel;\n else if (selected.length === 1) label.innerText = selected[0];\n else label.innerText = selected.length + ' Selected';\n\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (clearBtn) clearBtn.style.display = (currentStatusFilter.length > 0 || currentRiskFilter.length > 0) ? 'flex' : 'none';\n \n resetChildFilters();\n saveFilters();\n refresh();\n };\n\n window.clear_filters = () => {\n root.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = false);\n currentStatusFilter = []; currentRiskFilter = []; currentItemStages = []; currentTimeFilter = []; currentQueryTypeFilter = [];\n root.querySelector('#selected-status-label').innerText = 'Status';\n root.querySelector('#selected-risk-label').innerText = 'Risk';\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (clearBtn) clearBtn.style.display = 'none';\n saveFilters();\n refresh();\n };\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name.split('-').pop()}\n ${i.emp_branch || '---'}\n ${i.audit_query_subject_box || '---'}\n ${i.emp_division || '---'}\n ${i.status || '---'}\n ${i.risk || 'Normal'}\n ${i.aging || 0}\n ${frappe.datetime.str_to_user(i.creation).split(' ')[0]}${frappe.datetime.comment_when(i.creation)}\n `).join('');\n\n window.load_more_data = (type) => {\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: {\n pending_start: type === 'pending' ? pendingStart + 10 : pendingStart,\n recent_start: type === 'recent' ? recentStart + 10 : recentStart,\n status: currentStatusFilter.join(','),\n risk: currentRiskFilter.join(','),\n item_stages: currentItemStages.join(','),\n time_filter: currentTimeFilter.join(','),\n query_type: currentQueryTypeFilter.join(',')\n },\n callback: function (r) {\n if (!r.message || !r.message.success) return;\n const d = r.message;\n if (type === 'pending') {\n pendingStart += 10;\n root.querySelector('#stage-items').insertAdjacentHTML('beforeend', renderRows(d.pending_list));\n const btn = root.querySelector('#pending-more-btn');\n if (btn) btn.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n recentStart += 10;\n root.querySelector('#activity-body').insertAdjacentHTML('beforeend', renderRows(d.recent_list));\n const btn = root.querySelector('#recent-more-btn');\n if (btn) btn.style.display = d.has_more_recent ? 'flex' : 'none';\n }\n }\n });\n };\n\n function refresh() {\n pendingStart = 0; recentStart = 0;\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: { \n pending_start: 0, recent_start: 0, \n status: currentStatusFilter.join(','), \n risk: currentRiskFilter.join(','),\n item_stages: currentItemStages.join(','),\n time_filter: currentTimeFilter.join(','),\n query_type: currentQueryTypeFilter.join(',')\n },\n callback: function (r) {\n if (!r.message || !r.message.success) return;\n const d = r.message; userRole = d.role_type;\n console.log('Dashboard Refresh:', { role: userRole, status: currentStatusFilter, time_counts: d.time_counts });\n\n const statusDropdown = root.querySelector('#filter-dropdown-status');\n const riskDropdown = root.querySelector('#filter-dropdown-risk');\n \n if (statusDropdown && !statusDropdown.hasChildNodes()) {\n const statusOps = userRole === 'stage_user' ? ['Pending', 'Responded', 'No Response'] : ['Draft', 'Pending', 'Closed'];\n statusDropdown.innerHTML = statusOps.map(opt => `
${opt}
`).join('');\n const riskOps = ['High', 'Medium', 'Normal'];\n riskDropdown.innerHTML = riskOps.map(opt => `
${opt}
`).join('');\n \n if (currentStatusFilter.length > 0) {\n const label = root.querySelector('#selected-status-label');\n if (label) label.innerText = currentStatusFilter.length === 1 ? currentStatusFilter[0] : currentStatusFilter.length + ' Selected';\n }\n if (currentRiskFilter.length > 0) {\n const label = root.querySelector('#selected-risk-label');\n if (label) label.innerText = currentRiskFilter.length === 1 ? currentRiskFilter[0] : currentRiskFilter.length + ' Selected';\n }\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (clearBtn) clearBtn.style.display = (currentStatusFilter.length > 0 || currentRiskFilter.length > 0) ? 'flex' : 'none';\n }\n\n // Sync Timeframe Checkboxes (Always run)\n root.querySelectorAll('.time-checkbox').forEach(cb => {\n cb.checked = currentTimeFilter.includes(cb.value);\n });\n\n const stageView = root.querySelector('#stage-view');\n const managerView = root.querySelector('#manager-view');\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n const totalCard = root.querySelector('#total-card');\n const nrCard = root.querySelector('#nr-card');\n const respCard = root.querySelector('#resp-card');\n const closedCard = root.querySelector('#c-card');\n const sReportBtn = root.querySelector('#stage-report-btn');\n \n root.querySelectorAll('.compact-stat-card').forEach(c => c.classList.remove('active-card'));\n\n if (userRole === 'stage_user') {\n if (stageView) stageView.style.display = 'block';\n if (managerView) managerView.style.display = 'none';\n if (masterCapsules) {\n masterCapsules.style.display = 'flex';\n root.querySelectorAll('.multiselect-container, .dropdown-wrapper, .create-btn-capsule').forEach(el => el.style.display = 'none');\n if (sReportBtn) sReportBtn.style.display = 'flex';\n }\n if (draftCard) draftCard.style.display = 'none';\n if (totalCard) totalCard.style.display = 'none';\n if (respCard) respCard.style.display = 'none';\n if (nrCard) nrCard.style.display = 'block';\n \n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-nr', d.not_responded_count);\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n\n if (currentStatusFilter.length > 0) {\n const activeStatus = currentStatusFilter[0];\n if (activeStatus === 'Pending') root.querySelector('#p-card')?.classList.add('active-card');\n else if (activeStatus === 'No Response') root.querySelector('#nr-card')?.classList.add('active-card');\n else if (activeStatus === 'Responded') root.querySelector('#c-card')?.classList.add('active-card');\n }\n\n const stageItems = root.querySelector('#stage-items');\n const pendingMore = root.querySelector('#pending-more-btn');\n if (d.pending_list && d.pending_list.length > 0) {\n stageItems.innerHTML = renderRows(d.pending_list);\n if (pendingMore) pendingMore.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n stageItems.innerHTML = 'No records found';\n if (pendingMore) pendingMore.style.display = 'none';\n }\n\n } else {\n if (stageView) stageView.style.display = 'none';\n if (managerView) managerView.style.display = 'block';\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (sReportBtn) sReportBtn.style.display = 'none';\n if (draftCard) draftCard.style.display = 'block';\n if (totalCard) totalCard.style.display = 'block';\n if (nrCard) nrCard.style.display = 'block';\n if (respCard) respCard.style.display = 'block';\n if (closedCard) closedCard.style.display = 'block';\n \n upd('val-draft', d.draft_count);\n upd('val-total', (d.draft_count || 0) + (d.total_pending || 0) + (d.closed_count || 0));\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n upd('val-nr', d.not_responded_count);\n upd('val-resp', d.responded_by_me);\n\n if (currentStatusFilter.length > 0) {\n const activeStatus = currentStatusFilter[0];\n if (activeStatus === 'Draft') root.querySelector('#draft-card')?.classList.add('active-card');\n else if (activeStatus === 'Pending') root.querySelector('#p-card')?.classList.add('active-card');\n else if (activeStatus === 'Closed') root.querySelector('#c-card')?.classList.add('active-card');\n else if (activeStatus === 'Responded') root.querySelector('#resp-card')?.classList.add('active-card');\n else if (activeStatus === 'No Response') root.querySelector('#nr-card')?.classList.add('active-card');\n } else if (currentRiskFilter.length === 0) {\n root.querySelector('#total-card')?.classList.add('active-card');\n }\n\n const drilldown = root.querySelector('#drilldown-section');\n if (drilldown) {\n const showDrill = (currentStatusFilter.includes('Responded') || currentStatusFilter.includes('No Response'));\n drilldown.style.display = showDrill ? 'block' : 'none';\n \n if (showDrill) {\n const tc = d.time_counts || {}; \n const setTC = (id, val) => { const el = root.querySelector(id); if (el) el.innerText = '(' + (val || 0) + ')'; };\n setTC('#count-today', tc.Today);\n setTC('#count-yest', tc.Yesterday);\n setTC('#count-week', tc['Last Week']);\n setTC('#count-all', tc['All Time']);\n\n const stageList = root.querySelector('#stage-checkbox-list');\n const queryTypeList = root.querySelector('#query-type-checkbox-list');\n \n if (stageList) {\n frappe.call({\n method: 'frappe.client.get_list',\n args: { doctype: 'Audit Stage', fields: ['name'], order_by: 'name asc' },\n callback: (r) => {\n if (r.message) {\n const counts = d.stage_counts || {};\n stageList.innerHTML = r.message.map(s => {\n const count = counts[s.name] || 0;\n const isChecked = currentItemStages.includes(s.name) ? 'checked' : '';\n return ``;\n }).join('');\n }\n }\n });\n }\n if (queryTypeList) {\n frappe.call({\n method: 'frappe.client.get_list',\n args: { doctype: 'Audit Query Type', fields: ['name'], order_by: 'name asc' },\n callback: (r_qt) => {\n if (r_qt.message) {\n queryTypeList.innerHTML = r_qt.message.map(qt => {\n const isChecked = currentQueryTypeFilter.includes(qt.name) ? 'checked' : '';\n return ``;\n }).join('');\n }\n }\n });\n }\n }\n }\n\n const activityBody = root.querySelector('#activity-body');\n const recentMore = root.querySelector('#recent-more-btn');\n if (d.recent_list && d.recent_list.length > 0) {\n activityBody.innerHTML = renderRows(d.recent_list);\n if (recentMore) recentMore.style.display = d.has_more_recent ? 'flex' : 'none';\n } else {\n activityBody.innerHTML = 'No recent activity';\n if (recentMore) recentMore.style.display = 'none';\n }\n }\n }\n });\n }\n\n window.show_reports_selection = () => {\n const reports = ['My Audits Report', 'Pending Audit Queries Aging Report', 'Process Technical Improvement Commitment Report', 'Recurring Operational Reports'];\n let html = `
\n ${reports.map(r => ``).join('')}\n
`;\n let d = new frappe.ui.Dialog({ title: 'Select Report', fields: [{ fieldtype: 'HTML', fieldname: 'reports_html', options: html }] });\n d.show();\n };\n\n window.handle_total_click = () => { \n frappe.set_route('List', 'My Audits');\n };\n\n window.handle_draft_click = () => {\n frappe.set_route('List', 'My Audits', { status: 'Draft' });\n };\n\n window.handle_responded_click = () => {\n currentStatusFilter = ['Responded']; currentItemStages = []; currentTimeFilter = ['Today']; currentQueryTypeFilter = [];\n root.querySelectorAll('.time-checkbox').forEach(cb => cb.checked = (cb.value === 'Today'));\n saveFilters();\n refresh();\n };\n\n window.handle_not_responded_click = () => {\n currentStatusFilter = ['No Response']; currentItemStages = []; currentTimeFilter = ['Today']; currentQueryTypeFilter = [];\n root.querySelectorAll('.time-checkbox').forEach(cb => cb.checked = (cb.value === 'Today'));\n saveFilters();\n refresh();\n };\n\n window.handle_pending_click = () => { \n frappe.set_route('List', 'My Audits', { status: 'Pending' });\n };\n\n window.handle_closed_click = () => { \n frappe.set_route('List', 'My Audits', { status: 'Closed' });\n };\n\n window.handle_item_stage_change = () => {\n currentItemStages = Array.from(root.querySelectorAll('.stage-item-checkbox:checked')).map(cb => cb.value);\n saveFilters();\n refresh();\n };\n\n window.handle_query_type_change = () => {\n currentQueryTypeFilter = Array.from(root.querySelectorAll('.query-type-item-checkbox:checked')).map(cb => cb.value);\n saveFilters();\n refresh();\n };\n\n window.handle_time_filter_change = () => {\n currentTimeFilter = Array.from(root.querySelectorAll('.time-checkbox:checked')).map(cb => cb.value);\n saveFilters();\n refresh();\n };\n\n $(document).on('workspace_render', refresh); refresh();\n})();\n", + "script": "(function () {\n const root = root_element || document;\n let userRole = '';\n let userStages = [];\n let currentStatusFilter = [];\n let currentRiskFilter = [];\n let pendingStart = 0;\n let recentStart = 0;\n let currentItemStages = [];\n let currentTimeFilter = [];\n let currentQueryTypeFilter = [];\n\n const STORAGE_KEY = `audit_dashboard_settings_${frappe.session.user}`;\n\n const saveFilters = () => {\n const data = {\n status: currentStatusFilter,\n risk: currentRiskFilter,\n stages: currentItemStages,\n time: currentTimeFilter,\n query_type: currentQueryTypeFilter\n };\n localStorage.setItem(STORAGE_KEY, JSON.stringify(data));\n };\n\n const loadFilters = () => {\n const saved = localStorage.getItem(STORAGE_KEY);\n if (saved) {\n try {\n const data = JSON.parse(saved);\n currentStatusFilter = data.status || [];\n currentRiskFilter = data.risk || [];\n currentItemStages = data.stages || [];\n currentTimeFilter = data.time || [];\n currentQueryTypeFilter = data.query_type || [];\n } catch (e) { console.error('Error loading filters', e); }\n }\n };\n\n loadFilters();\n\n const upd = (id, val) => { \n const el = root.querySelector('#' + id); \n if (el) el.innerText = val ?? 0; \n };\n\n window.toggle_dropdown = (e) => {\n e.stopPropagation();\n const dd = root.querySelector('#actions-dropdown');\n if (dd) dd.style.display = dd.style.display === 'none' ? 'block' : 'none';\n };\n\n window.toggle_filter_dropdown = (e, type) => {\n e.stopPropagation();\n root.querySelectorAll('.multiselect-list').forEach(d => {\n if (d.id !== 'filter-dropdown-' + type) d.style.display = 'none';\n });\n const dd = root.querySelector('#filter-dropdown-' + type);\n if (dd) dd.style.display = dd.style.display === 'none' ? 'block' : 'none';\n };\n\n window.addEventListener('click', () => {\n root.querySelectorAll('.custom-dropdown, .multiselect-list').forEach(d => d.style.display = 'none');\n });\n\n const resetChildFilters = () => {\n currentItemStages = [];\n currentTimeFilter = [];\n currentQueryTypeFilter = [];\n root.querySelectorAll('.stage-item-checkbox, .time-checkbox, .query-type-item-checkbox').forEach(cb => cb.checked = false);\n };\n\n window.handle_checkbox_change = (type) => {\n const checkboxes = root.querySelectorAll('.' + type + '-checkbox:checked');\n const selected = Array.from(checkboxes).map(cb => cb.value);\n if (type === 'status') currentStatusFilter = selected; else currentRiskFilter = selected;\n \n const label = root.querySelector('#selected-' + type + '-label');\n const defaultLabel = type.charAt(0).toUpperCase() + type.slice(1);\n \n if (selected.length === 0) label.innerText = defaultLabel;\n else if (selected.length === 1) label.innerText = selected[0];\n else label.innerText = selected.length + ' Selected';\n\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (clearBtn) clearBtn.style.display = (currentStatusFilter.length > 0 || currentRiskFilter.length > 0) ? 'flex' : 'none';\n \n resetChildFilters();\n saveFilters();\n refresh();\n };\n\n window.clear_filters = () => {\n root.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = false);\n currentStatusFilter = []; currentRiskFilter = []; currentItemStages = []; currentTimeFilter = []; currentQueryTypeFilter = [];\n root.querySelector('#selected-status-label').innerText = 'Status';\n root.querySelector('#selected-risk-label').innerText = 'Risk';\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (clearBtn) clearBtn.style.display = 'none';\n saveFilters();\n refresh();\n };\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name.split('-').pop()}\n ${i.emp_branch || '---'}\n ${i.audit_query_subject_box || '---'}\n ${i.emp_division || '---'}\n ${i.status || '---'}\n ${i.risk || 'Normal'}\n ${i.aging || 0}\n ${frappe.datetime.str_to_user(i.creation).split(' ')[0]}${frappe.datetime.comment_when(i.creation)}\n `).join('');\n\n window.load_more_data = (type) => {\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: {\n pending_start: type === 'pending' ? pendingStart + 10 : pendingStart,\n recent_start: type === 'recent' ? recentStart + 10 : recentStart,\n status: currentStatusFilter.join(','),\n risk: currentRiskFilter.join(','),\n item_stages: currentItemStages.join(','),\n time_filter: currentTimeFilter.join(','),\n query_type: currentQueryTypeFilter.join(',')\n },\n callback: function (r) {\n if (!r.message || !r.message.success) return;\n const d = r.message;\n if (type === 'pending') {\n pendingStart += 10;\n root.querySelector('#stage-items').insertAdjacentHTML('beforeend', renderRows(d.pending_list));\n const btn = root.querySelector('#pending-more-btn');\n if (btn) btn.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n recentStart += 10;\n root.querySelector('#activity-body').insertAdjacentHTML('beforeend', renderRows(d.recent_list));\n const btn = root.querySelector('#recent-more-btn');\n if (btn) btn.style.display = d.has_more_recent ? 'flex' : 'none';\n }\n }\n });\n };\n\n function refresh() {\n pendingStart = 0; recentStart = 0;\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: { \n pending_start: 0, recent_start: 0, \n status: currentStatusFilter.join(','), \n risk: currentRiskFilter.join(','),\n item_stages: currentItemStages.join(','),\n time_filter: currentTimeFilter.join(','),\n query_type: currentQueryTypeFilter.join(',')\n },\n callback: function (r) {\n if (!r.message || !r.message.success) return;\n const d = r.message; userRole = d.role_type; userStages = d.user_stages || [];\n console.log('Dashboard Refresh:', { role: userRole, status: currentStatusFilter, time_counts: d.time_counts });\n\n const statusDropdown = root.querySelector('#filter-dropdown-status');\n const riskDropdown = root.querySelector('#filter-dropdown-risk');\n \n if (statusDropdown && !statusDropdown.hasChildNodes()) {\n const statusOps = userRole === 'stage_user' ? ['Pending', 'Responded', 'No Response'] : ['Draft', 'Pending', 'Closed'];\n statusDropdown.innerHTML = statusOps.map(opt => `
${opt}
`).join('');\n const riskOps = ['High', 'Medium', 'Normal'];\n riskDropdown.innerHTML = riskOps.map(opt => `
${opt}
`).join('');\n \n if (currentStatusFilter.length > 0) {\n const label = root.querySelector('#selected-status-label');\n if (label) label.innerText = currentStatusFilter.length === 1 ? currentStatusFilter[0] : currentStatusFilter.length + ' Selected';\n }\n if (currentRiskFilter.length > 0) {\n const label = root.querySelector('#selected-risk-label');\n if (label) label.innerText = currentRiskFilter.length === 1 ? currentRiskFilter[0] : currentRiskFilter.length + ' Selected';\n }\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (clearBtn) clearBtn.style.display = (currentStatusFilter.length > 0 || currentRiskFilter.length > 0) ? 'flex' : 'none';\n }\n\n // Sync Timeframe Checkboxes (Always run)\n root.querySelectorAll('.time-checkbox').forEach(cb => {\n cb.checked = currentTimeFilter.includes(cb.value);\n });\n\n const stageView = root.querySelector('#stage-view');\n const managerView = root.querySelector('#manager-view');\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n const totalCard = root.querySelector('#total-card');\n const nrCard = root.querySelector('#nr-card');\n const respCard = root.querySelector('#resp-card');\n const closedCard = root.querySelector('#c-card');\n const sReportBtn = root.querySelector('#stage-report-btn');\n \n root.querySelectorAll('.compact-stat-card').forEach(c => c.classList.remove('active-card'));\n\n if (userRole === 'stage_user') {\n if (stageView) stageView.style.display = 'block';\n if (managerView) managerView.style.display = 'none';\n if (masterCapsules) {\n masterCapsules.style.display = 'flex';\n root.querySelectorAll('.multiselect-container, .dropdown-wrapper, .create-btn-capsule').forEach(el => el.style.display = 'none');\n if (sReportBtn) sReportBtn.style.display = 'flex';\n }\n if (draftCard) draftCard.style.display = 'none';\n if (totalCard) totalCard.style.display = 'none';\n if (respCard) respCard.style.display = 'none';\n if (nrCard) nrCard.style.display = 'block';\n \n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-nr', d.not_responded_count);\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n\n if (currentStatusFilter.length > 0) {\n const activeStatus = currentStatusFilter[0];\n if (activeStatus === 'Pending') root.querySelector('#p-card')?.classList.add('active-card');\n else if (activeStatus === 'No Response') root.querySelector('#nr-card')?.classList.add('active-card');\n else if (activeStatus === 'Responded') root.querySelector('#c-card')?.classList.add('active-card');\n }\n\n const stageItems = root.querySelector('#stage-items');\n const pendingMore = root.querySelector('#pending-more-btn');\n if (d.pending_list && d.pending_list.length > 0) {\n stageItems.innerHTML = renderRows(d.pending_list);\n if (pendingMore) pendingMore.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n stageItems.innerHTML = 'No records found';\n if (pendingMore) pendingMore.style.display = 'none';\n }\n\n } else {\n if (stageView) stageView.style.display = 'none';\n if (managerView) managerView.style.display = 'block';\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (sReportBtn) sReportBtn.style.display = 'none';\n if (draftCard) draftCard.style.display = 'block';\n if (totalCard) totalCard.style.display = 'block';\n if (nrCard) nrCard.style.display = 'block';\n if (respCard) respCard.style.display = 'block';\n if (closedCard) closedCard.style.display = 'block';\n \n upd('val-draft', d.draft_count);\n upd('val-total', (d.draft_count || 0) + (d.total_pending || 0) + (d.closed_count || 0));\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n upd('val-nr', d.not_responded_count);\n upd('val-resp', d.responded_by_me);\n\n if (currentStatusFilter.length > 0) {\n const activeStatus = currentStatusFilter[0];\n if (activeStatus === 'Draft') root.querySelector('#draft-card')?.classList.add('active-card');\n else if (activeStatus === 'Pending') root.querySelector('#p-card')?.classList.add('active-card');\n else if (activeStatus === 'Closed') root.querySelector('#c-card')?.classList.add('active-card');\n else if (activeStatus === 'Responded') root.querySelector('#resp-card')?.classList.add('active-card');\n else if (activeStatus === 'No Response') root.querySelector('#nr-card')?.classList.add('active-card');\n } else if (currentRiskFilter.length === 0) {\n root.querySelector('#total-card')?.classList.add('active-card');\n }\n\n const drilldown = root.querySelector('#drilldown-section');\n if (drilldown) {\n const showDrill = (currentStatusFilter.includes('Responded') || currentStatusFilter.includes('No Response'));\n drilldown.style.display = showDrill ? 'block' : 'none';\n \n if (showDrill) {\n const tc = d.time_counts || {}; \n const setTC = (id, val) => { const el = root.querySelector(id); if (el) el.innerText = '(' + (val || 0) + ')'; };\n setTC('#count-today', tc.Today);\n setTC('#count-yest', tc.Yesterday);\n setTC('#count-week', tc['Last Week']);\n setTC('#count-all', tc['All Time']);\n\n const stageList = root.querySelector('#stage-checkbox-list');\n const queryTypeList = root.querySelector('#query-type-checkbox-list');\n \n if (stageList) {\n frappe.call({\n method: 'frappe.client.get_list',\n args: { doctype: 'Audit Stage', fields: ['name'], order_by: 'name asc' },\n callback: (r) => {\n if (r.message) {\n const counts = d.stage_counts || {};\n stageList.innerHTML = r.message.map(s => {\n const count = counts[s.name] || 0;\n const isChecked = currentItemStages.includes(s.name) ? 'checked' : '';\n return ``;\n }).join('');\n }\n }\n });\n }\n if (queryTypeList) {\n frappe.call({\n method: 'frappe.client.get_list',\n args: { doctype: 'Audit Query Type', fields: ['name'], order_by: 'name asc' },\n callback: (r_qt) => {\n if (r_qt.message) {\n queryTypeList.innerHTML = r_qt.message.map(qt => {\n const isChecked = currentQueryTypeFilter.includes(qt.name) ? 'checked' : '';\n return ``;\n }).join('');\n }\n }\n });\n }\n }\n }\n\n const activityBody = root.querySelector('#activity-body');\n const recentMore = root.querySelector('#recent-more-btn');\n if (d.recent_list && d.recent_list.length > 0) {\n activityBody.innerHTML = renderRows(d.recent_list);\n if (recentMore) recentMore.style.display = d.has_more_recent ? 'flex' : 'none';\n } else {\n activityBody.innerHTML = 'No recent activity';\n if (recentMore) recentMore.style.display = 'none';\n }\n }\n }\n });\n }\n\n window.show_reports_selection = () => {\n const reports = ['My Audits Report', 'Pending Audit Queries Aging Report', 'Process Technical Improvement Commitment Report', 'Recurring Operational Reports'];\n let html = `
\n ${reports.map(r => ``).join('')}\n
`;\n let d = new frappe.ui.Dialog({ title: 'Select Report', fields: [{ fieldtype: 'HTML', fieldname: 'reports_html', options: html }] });\n d.show();\n };\n\n window.handle_total_click = () => { \n frappe.set_route('List', 'My Audits', { status: '' });\n };\n\n window.handle_draft_click = () => {\n frappe.set_route('List', 'My Audits', { status: 'Draft' });\n };\n\n window.handle_responded_click = () => {\n if (userRole === 'stage_user') {\n frappe.set_route('List', 'My Audits', { 'Audit Items.status': 'Responded', 'Audit Items.stage_name': ['in', userStages] });\n } else {\n currentStatusFilter = ['Responded']; currentItemStages = []; currentTimeFilter = ['Today']; currentQueryTypeFilter = [];\n root.querySelectorAll('.time-checkbox').forEach(cb => cb.checked = (cb.value === 'Today'));\n saveFilters();\n refresh();\n }\n };\n\n window.handle_not_responded_click = () => {\n if (userRole === 'stage_user') {\n frappe.set_route('List', 'My Audits', { 'Audit Items.status': 'No Response', 'Audit Items.stage_name': ['in', userStages] });\n } else {\n currentStatusFilter = ['No Response']; currentItemStages = []; currentTimeFilter = ['Today']; currentQueryTypeFilter = [];\n root.querySelectorAll('.time-checkbox').forEach(cb => cb.checked = (cb.value === 'Today'));\n saveFilters();\n refresh();\n }\n };\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') {\n frappe.set_route('List', 'My Audits', { 'Audit Items.status': 'Pending', 'Audit Items.stage_name': ['in', userStages] });\n } else {\n frappe.set_route('List', 'My Audits', { status: 'Pending' });\n }\n };\n\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') {\n frappe.set_route('List', 'My Audits', { 'Audit Items.status': 'Responded', 'Audit Items.stage_name': ['in', userStages] });\n } else {\n frappe.set_route('List', 'My Audits', { status: 'Closed' });\n }\n };\n\n window.handle_item_stage_change = () => {\n currentItemStages = Array.from(root.querySelectorAll('.stage-item-checkbox:checked')).map(cb => cb.value);\n saveFilters();\n refresh();\n };\n\n window.handle_query_type_change = () => {\n currentQueryTypeFilter = Array.from(root.querySelectorAll('.query-type-item-checkbox:checked')).map(cb => cb.value);\n saveFilters();\n refresh();\n };\n\n window.handle_time_filter_change = () => {\n currentTimeFilter = Array.from(root.querySelectorAll('.time-checkbox:checked')).map(cb => cb.value);\n saveFilters();\n refresh();\n };\n\n $(document).on('workspace_render', refresh); refresh();\n})();\n", "style": ".audit-dashboard-light { background: #f1f5f9; color: #1e293b; padding: 20px; border-radius: 16px; font-family: 'Inter', sans-serif; border: 1px solid #e2e8f0; }\n.db-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; flex-wrap: nowrap; gap: 15px; }\n.title-wrap { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }\n.db-title { font-size: 20px; font-weight: 800; color: #0f172a; margin: 0; white-space: nowrap; }\n.live-dot { width: 8px; height: 8px; background: #22c55e; border-radius: 50%; box-shadow: 0 0 10px rgba(34, 197, 94, 0.4); }\n\n.master-capsule-container { display: flex; gap: 6px; flex-wrap: nowrap; align-items: center; justify-content: flex-end; flex-grow: 1; }\n.master-capsule { background: #ffffff; padding: 4px 10px; border-radius: 50px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 6px; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); height: 30px; box-sizing: border-box; }\n.master-capsule:hover { border-color: #3b82f6; transform: translateY(-1px); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); }\n.master-capsule i { font-size: 13px; flex-shrink: 0; }\n.master-capsule span { font-size: 10px; font-weight: 700; color: #475569; white-space: nowrap; }\n.label-text { min-width: 40px; text-align: center; }\n\n.create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; }\n.create-btn-capsule i, .create-btn-capsule span { color: white !important; }\n.create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.02); }\n\n.dropdown-wrapper { position: relative; }\n.custom-dropdown { position: absolute; top: 110%; right: 0; background: white; border: 1px solid #e2e8f0; border-radius: 12px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); z-index: 1000; min-width: 180px; padding: 8px 0; }\n.dropdown-item { padding: 10px 16px; display: flex; align-items: center; gap: 12px; cursor: pointer; transition: 0.2s; font-size: 12px; font-weight: 600; color: #475569; }\n.dropdown-item:hover { background: #f1f5f9; color: #1e293b; }\n\n.grid-compact { display: grid; gap: 10px; margin-bottom: 24px; }\n.stats-grid { grid-template-columns: repeat(6, 1fr); }\n\n.blue-txt { color: #2563eb; } .purple-txt { color: #7c3aed; } .orange-txt { color: #ea580c; } .green-txt { color: #16a34a; } .red-txt { color: #dc2626; }\n\n.compact-stat-card { background: #ffffff; padding: 10px 5px; border-radius: 12px; border: 1px solid #e2e8f0; position: relative; overflow: hidden; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.02); display: flex; flex-direction: column; align-items: center; text-align: center; gap: 4px; }\n.stat-label-small { font-size: 10px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.4px; }\n.stat-val-small { font-size: 18px; font-weight: 800; color: #0f172a; }\n.icon-stat { font-size: 18px; opacity: 0.9; margin-bottom: 2px; }\n.blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; } .orange-bg { background: #f59e0b; } .red-bg { background: #ef4444; }\n\n.multiselect-container { position: relative; }\n.multiselect-list { position: absolute; top: 110%; left: 0; background: white; border: 1px solid #e2e8f0; border-radius: 12px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); z-index: 1000; min-width: 160px; padding: 8px 0; display: none; }\n.multiselect-item { padding: 8px 16px; display: flex; align-items: center; gap: 10px; cursor: pointer; transition: 0.2s; font-size: 12px; font-weight: 600; color: #475569; }\n.multiselect-item:hover { background: #f1f5f9; }\n.multiselect-item input[type='checkbox'] { cursor: pointer; width: 14px; height: 14px; }\n\n.mini-table { width: 100%; border-collapse: collapse; font-size: 12px; }\n.mini-table th { background: #f1f5f9; color: #475569; text-align: left; padding: 12px 12px; border-bottom: 2px solid #e2e8f0; font-weight: 700; text-transform: uppercase; font-size: 10px; }\n.mini-table td { padding: 14px 16px; color: #334155; border-bottom: 1px solid #f1f5f9; }\n.mini-table tr:hover { background: #f9fafb; cursor: pointer; }\n.t-id { font-weight: 800; color: #2563eb; }\n.t-status { background: #f1f5f9; padding: 4px 8px; border-radius: 6px; color: #475569; font-weight: 700; font-size: 10px; }\n.t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }\n.t-status.closed { background: #b9f9cf; color: #001a00; border: 1px solid #bbf7d0; }\n.t-risk { font-weight: 800; text-transform: uppercase; font-size: 10px; }\n.t-risk.high { background: #dc2626; color: white; padding: 4px 8px; border-radius: 6px; } .t-risk.medium { color: #d97706; } .t-risk.normal { color: #2563eb; }\n\n.load-more-btn { background: #ffffff; color: #475569; border: 1px solid #e2e8f0; padding: 8px 24px; border-radius: 50px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); letter-spacing: 0.5px; text-transform: uppercase; }\n.load-more-btn:hover { background: #f8fafc; border-color: #cbd5e1; color: #1e293b; transform: translateY(-1px); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); }\n.load-more-btn:active { transform: translateY(0); box-shadow: none; background: #f1f5f9; }\n\n@media (max-width: 768px) {\n .db-header { flex-direction: column; align-items: flex-start; }\n .master-capsule-container { width: 100%; justify-content: flex-start; flex-wrap: wrap; }\n .stats-grid { grid-template-columns: 1fr; }\n}\n.compact-stat-card.active-card { border-color: #2563eb; background: #eff6ff; transform: translateY(-2px); box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.1); }\n\n.drilldown-bar { background: #ffffff; padding: 15px; border-radius: 14px; border: 1px solid #e2e8f0; margin-bottom: 20px; }\n.filter-group { margin-bottom: 12px; }\n.filter-group-label { font-size: 10px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 8px; }\n.checkbox-row { display: flex; flex-wrap: wrap; gap: 15px; }\n.check-item { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 600; color: #475569; cursor: pointer; }\n" } ] \ No newline at end of file From 1755991acc73403ec910a01dbb72e57e5e925b57 Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Fri, 5 Jun 2026 13:12:28 +0530 Subject: [PATCH 3/6] fix(backend): separate counter logic from drilldown filters to ensure static card counts --- .../audit_management/dashboard.py | 200 +++++++++--------- 1 file changed, 102 insertions(+), 98 deletions(-) diff --git a/audit_management/audit_management/dashboard.py b/audit_management/audit_management/dashboard.py index 216bf7f..76e3c76 100644 --- a/audit_management/audit_management/dashboard.py +++ b/audit_management/audit_management/dashboard.py @@ -47,41 +47,59 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, elif isinstance(time_filter, list): time_filter_list = time_filter try: - # Get All User's Assigned Stages + # 1. 🟢 BASE FILTERS (Permissions) + base_filters = {} + if is_admin: pass + elif is_manager: + if allowed_divisions: base_filters["emp_division"] = ["in", allowed_divisions] + else: base_filters["emp_division"] = "None" + elif is_member: + base_filters["owner"] = user + # Note: stage_user (else) does NOT get base_filters by default to preserve assignment-based access + + # 2. 🟢 COUNTER FILTERS (For all top 6 cards: Permissions + Risk ONLY) + counter_filters = base_filters.copy() + if risk_list: + if "Normal" in risk_list: counter_filters["risk"] = ["in", risk_list + [None, ""]] + else: counter_filters["risk"] = ["in", risk_list] + + # 3. 🟢 LIST FILTERS (For record lists and drilldown stats: Counter + Query Type) + list_filters = counter_filters.copy() + if query_type_list: + list_filters["query_type"] = ["in", query_type_list] + + # 4. 🟢 STAGE USER LOGIC + # Get All User's Assigned Stages (No division restriction for assigned items) assigned_stages = frappe.get_all("Audit Items", filters={"user_id": user, "status": ["in", ["Pending", "No Response", "Responded"]]}, fields=["stage_name"], pluck="stage_name") user_stages = list(set(assigned_stages)) if assigned_stages else ["BM"] - - # 1. 🟢 FETCH PENDING FOR ME (From Child Table - Stage User Logic) - pending_items_query = """ - SELECT DISTINCT parent - FROM `tabAudit Items` - WHERE status = 'Pending' - AND (user_id = %s OR email = %s) - AND stage_name IN %s - """ - responded_items_query = """ - SELECT DISTINCT parent - FROM `tabAudit Items` - WHERE status = 'Responded' - AND (user_id = %s OR email = %s) - AND stage_name IN %s - """ - not_responded_items_query = """ - SELECT DISTINCT parent - FROM `tabAudit Items` - WHERE status = 'No Response' - AND (user_id = %s OR email = %s) - AND stage_name IN %s - """ - - # Ensure user_stages is a tuple for SQL IN clause stages_tuple = tuple(user_stages) if len(user_stages) > 1 else (user_stages[0],) + + # Count Queries for Stage User (Static - respect Risk but ignore Query Type) + # We still need to respect base_filters if they apply (e.g. if stage user is also a member/manager, but role_type logic handles priority) + # For a pure stage_user, we allow all assignments. + + # Helper to get allowed parent IDs based on counter_filters + allowed_parents_for_counts = frappe.get_all("My Audits", filters=counter_filters, fields=["name"], pluck="name") + # If pure stage_user and no risk filter, we might not want to restrict parents at all. + # But if they select Risk=High, we SHOULD restrict counts. - pending_records = frappe.db.sql(pending_items_query, (user, user, stages_tuple), as_dict=True) - responded_records = frappe.db.sql(responded_items_query, (user, user, stages_tuple), as_dict=True) - not_responded_records = frappe.db.sql(not_responded_items_query, (user, user, stages_tuple), as_dict=True) + # If no risk filter and pure stage user, allow all. + if not risk_list and not (is_admin or is_manager or is_member): + parent_limit_sql = "" + parent_params = [] + else: + parent_limit_sql = " AND parent IN %s" + parent_params = [tuple(allowed_parents_for_counts) if allowed_parents_for_counts else ("None",)] + + pending_items_query = f"SELECT DISTINCT parent FROM `tabAudit Items` WHERE status = 'Pending' AND (user_id = %s OR email = %s) AND stage_name IN %s {parent_limit_sql}" + responded_items_query = f"SELECT DISTINCT parent FROM `tabAudit Items` WHERE status = 'Responded' AND (user_id = %s OR email = %s) AND stage_name IN %s {parent_limit_sql}" + not_responded_items_query = f"SELECT DISTINCT parent FROM `tabAudit Items` WHERE status = 'No Response' AND (user_id = %s OR email = %s) AND stage_name IN %s {parent_limit_sql}" + + pending_records = frappe.db.sql(pending_items_query, [user, user, stages_tuple] + parent_params, as_dict=True) + responded_records = frappe.db.sql(responded_items_query, [user, user, stages_tuple] + parent_params, as_dict=True) + not_responded_records = frappe.db.sql(not_responded_items_query, [user, user, stages_tuple] + parent_params, as_dict=True) pending_for_me_count = len(pending_records) responded_by_me_count = len(responded_records) @@ -90,39 +108,33 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, pending_for_me_list = [] has_more_pending = False - # Identify which parent records to fetch for the stage user UI list + # For list fetching, respect all filters (list_filters) + contextual_parents = frappe.get_all("My Audits", filters=list_filters, fields=["name"], pluck="name") + contextual_parents_set = set(contextual_parents) if contextual_parents else set() + selected_parents_stage_user = [] target_child_status = None + if 'Responded' in status_list: - selected_parents_stage_user = [r.parent for r in responded_records] + selected_parents_stage_user = [r.parent for r in responded_records if r.parent in contextual_parents_set] target_child_status = 'Responded' elif 'No Response' in status_list: - selected_parents_stage_user = [r.parent for r in not_responded_records] + selected_parents_stage_user = [r.parent for r in not_responded_records if r.parent in contextual_parents_set] target_child_status = 'No Response' elif 'Pending' in status_list: - selected_parents_stage_user = [r.parent for r in pending_records] + selected_parents_stage_user = [r.parent for r in pending_records if r.parent in contextual_parents_set] target_child_status = 'Pending' else: selected_parents_stage_user = list(set( - [r.parent for r in pending_records] + - [r.parent for r in responded_records] + - [r.parent for r in not_responded_records] + [r.parent for r in pending_records if r.parent in contextual_parents_set] + + [r.parent for r in responded_records if r.parent in contextual_parents_set] + + [r.parent for r in not_responded_records if r.parent in contextual_parents_set] )) if selected_parents_stage_user: - p_filters = {"name": ["in", selected_parents_stage_user]} - # ... parent filters ... (status filter removed for stage_user to show based on child) - - if risk_list: - if "Normal" in risk_list: p_filters["risk"] = ["in", risk_list + [None, ""]] - else: p_filters["risk"] = ["in", risk_list] - - if query_type_list: - p_filters["query_type"] = ["in", query_type_list] - pending_for_me_list = frappe.get_all( "My Audits", - filters=p_filters, + filters={"name": ["in", selected_parents_stage_user]}, fields=["name", "audit_query_subject_box", "risk", "status", "emp_branch", "emp_division", "aging", "creation"], order_by="creation desc", limit_start=pending_start, @@ -133,91 +145,74 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, has_more_pending = True pending_for_me_list = pending_for_me_list[:page_length] - # Override parent status with child status for stage_user + # Override status with child status for idx, item in enumerate(pending_for_me_list, start=pending_start + 1): item["sr_no"] = idx if target_child_status: item["status"] = target_child_status else: - # Fetch actual child status for the user's stage(s) item["status"] = frappe.db.get_value("Audit Items", {"parent": item.name, "user_id": user, "stage_name": ["in", user_stages]}, "status") or item.status - # 2. 🔵 FETCH GLOBAL/ROLE STATS (Manager/Admin/Member) + # 5. 🔵 MANAGER/ADMIN/MEMBER LOGIC from audit_management.audit_management.utils import get_user_allowed_divisions allowed_divisions = get_user_allowed_divisions(user) - filters = {} - if is_admin: - pass - elif is_manager: - if allowed_divisions: filters["emp_division"] = ["in", allowed_divisions] - else: filters["emp_division"] = "None" - elif is_member: - filters["owner"] = user - else: - if allowed_divisions: filters["emp_division"] = ["in", allowed_divisions] - else: filters["emp_division"] = "None" - - # Apply Global query_type filter - if query_type_list: - filters["query_type"] = ["in", query_type_list] - - # Calculate Counters for Manager/Member - total_pending = frappe.db.count("My Audits", {**filters, "status": "Pending"}) - closed_count = frappe.db.count("My Audits", {**filters, "status": "Closed"}) - draft_count = frappe.db.count("My Audits", {**filters, "status": "Draft"}) - total_count = frappe.db.count("My Audits", filters) - - # New: Responded and Not Responded for Managers/Members from Child Table - manager_parents_responded = frappe.get_all("My Audits", filters={**filters}, fields=["name"], as_list=True) - manager_parents_names = [p[0] for p in manager_parents_responded] if manager_parents_responded else [] + # Top 4 Card Counts (Static - Counter Filters ignore Query Type) + total_pending = frappe.db.count("My Audits", {**counter_filters, "status": "Pending"}) + closed_count = frappe.db.count("My Audits", {**counter_filters, "status": "Closed"}) + draft_count = frappe.db.count("My Audits", {**counter_filters, "status": "Draft"}) + total_count = frappe.db.count("My Audits", counter_filters) + # Responded/Not Responded Counts (Static - Counter Filters ignore Query Type) + allowed_parents_for_global_counts = frappe.get_all("My Audits", filters=counter_filters, fields=["name"], pluck="name") responded_count_manager = 0 not_responded_count_manager = 0 - stage_counts = {} - time_counts = {"Today": 0, "Yesterday": 0, "Last Week": 0, "All Time": 0} - - if manager_parents_names: + if allowed_parents_for_global_counts: + global_tuple = tuple(allowed_parents_for_global_counts) resp_sql = "SELECT COUNT(DISTINCT parent) FROM `tabAudit Items` WHERE status = 'Responded' AND parent IN %s" nr_sql = "SELECT COUNT(DISTINCT parent) FROM `tabAudit Items` WHERE status = 'No Response' AND parent IN %s" - responded_count_manager = frappe.db.sql(resp_sql, (tuple(manager_parents_names),))[0][0] if manager_parents_names else 0 - not_responded_count_manager = frappe.db.sql(nr_sql, (tuple(manager_parents_names),))[0][0] if manager_parents_names else 0 + responded_count_manager = frappe.db.sql(resp_sql, (global_tuple,))[0][0] + not_responded_count_manager = frappe.db.sql(nr_sql, (global_tuple,))[0][0] - # Calculate Stage and Time Counts for Drilldown + # Contextual Drilldown Stats (Dynamic - respect all list_filters) + manager_parents_contextual = frappe.get_all("My Audits", filters=list_filters, fields=["name"], pluck="name") + manager_parents_contextual_tuple = tuple(manager_parents_contextual) if manager_parents_contextual else ("None",) + + stage_counts = {} + time_counts = {"Today": 0, "Yesterday": 0, "Last Week": 0, "All Time": 0} + + if manager_parents_contextual: child_status = None if 'Responded' in status_list: child_status = 'Responded' elif 'No Response' in status_list: child_status = 'No Response' if child_status: stg_sql = f"SELECT stage_name, COUNT(DISTINCT parent) as count FROM `tabAudit Items` WHERE status = %s AND parent IN %s GROUP BY stage_name" - stg_data = frappe.db.sql(stg_sql, (child_status, tuple(manager_parents_names)), as_dict=True) + stg_data = frappe.db.sql(stg_sql, (child_status, manager_parents_contextual_tuple), as_dict=True) stage_counts = {d.stage_name: d.count for d in stg_data} field = "response_time" if child_status == 'Responded' else "pending_time" - time_counts["Today"] = frappe.db.sql(f"SELECT COUNT(DISTINCT parent) FROM `tabAudit Items` WHERE status = %s AND parent IN %s AND DATE({field}) = CURDATE()", (child_status, tuple(manager_parents_names)))[0][0] or 0 - time_counts["Yesterday"] = frappe.db.sql(f"SELECT COUNT(DISTINCT parent) FROM `tabAudit Items` WHERE status = %s AND parent IN %s AND DATE({field}) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)", (child_status, tuple(manager_parents_names)))[0][0] or 0 - time_counts["Last Week"] = frappe.db.sql(f"SELECT COUNT(DISTINCT parent) FROM `tabAudit Items` WHERE status = %s AND parent IN %s AND DATE({field}) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)", (child_status, tuple(manager_parents_names)))[0][0] or 0 - time_counts["All Time"] = responded_count_manager if child_status == 'Responded' else not_responded_count_manager + time_counts["Today"] = frappe.db.sql(f"SELECT COUNT(DISTINCT parent) FROM `tabAudit Items` WHERE status = %s AND parent IN %s AND DATE({field}) = CURDATE()", (child_status, manager_parents_contextual_tuple))[0][0] or 0 + time_counts["Yesterday"] = frappe.db.sql(f"SELECT COUNT(DISTINCT parent) FROM `tabAudit Items` WHERE status = %s AND parent IN %s AND DATE({field}) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)", (child_status, manager_parents_contextual_tuple))[0][0] or 0 + time_counts["Last Week"] = frappe.db.sql(f"SELECT COUNT(DISTINCT parent) FROM `tabAudit Items` WHERE status = %s AND parent IN %s AND DATE({field}) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)", (child_status, manager_parents_contextual_tuple))[0][0] or 0 + time_counts["All Time"] = len(frappe.db.sql(f"SELECT DISTINCT parent FROM `tabAudit Items` WHERE status = %s AND parent IN %s", (child_status, manager_parents_contextual_tuple))) recent_list = [] has_more_recent = False if is_admin or is_manager or is_member: - r_filters = filters.copy() - + r_filters = list_filters.copy() + # (Rest of child filtering logic remains the same as it correctly applies drilldown to the list) # If Responded or No Response is selected, we must filter parents based on child items child_parent_ids = [] if 'Responded' in status_list or 'No Response' in status_list or item_stage_list or time_filter_list: child_conds = [] params = [] - if 'Responded' in status_list: child_conds.append("status = 'Responded'") elif 'No Response' in status_list: child_conds.append("status = 'No Response'") - if item_stage_list: child_conds.append("stage_name IN %s") params.append(tuple(item_stage_list)) - if time_filter_list: time_conds = [] for t in time_filter_list: @@ -226,25 +221,34 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, elif t == "Yesterday": time_conds.append(f"DATE({field}) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)") elif t == "Last Week": time_conds.append(f"DATE({field}) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)") if time_conds: child_conds.append("(" + " OR ".join(time_conds) + ")") - if child_conds: child_sql = f"SELECT DISTINCT parent FROM `tabAudit Items` WHERE {' AND '.join(child_conds)}" child_records = frappe.db.sql(child_sql, tuple(params), as_dict=True) child_parent_ids = [r.parent for r in child_records] - if child_parent_ids: r_filters["name"] = ["in", child_parent_ids] else: if 'Responded' in status_list or 'No Response' in status_list or item_stage_list or time_filter_list: r_filters["name"] = "None" - # Standard Parent Status Filter if status_list and not ('Responded' in status_list or 'No Response' in status_list): actual_statuses = [s for s in status_list if s in ['Draft', 'Pending', 'Closed']] if actual_statuses: r_filters["status"] = ["in", actual_statuses] - if risk_list: - if "Normal" in risk_list: r_filters["risk"] = ["in", risk_list + [None, ""]] - else: r_filters["risk"] = ["in", risk_list] + recent_list = frappe.get_all( + "My Audits", + filters=r_filters, + fields=["name", "audit_query_subject_box", "risk", "status", "emp_branch", "emp_division", "aging", "creation"], + order_by="creation desc", + limit_start=recent_start, + limit_page_length=page_length + 1 + ) + + if len(recent_list) > page_length: + has_more_recent = True + recent_list = recent_list[:page_length] + + for idx, item in enumerate(recent_list, start=recent_start + 1): + item["sr_no"] = idx recent_list = frappe.get_all( "My Audits", From e4b0e72493f9caa065e48e434c9a1d7e8babbbea Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Fri, 5 Jun 2026 13:24:53 +0530 Subject: [PATCH 4/6] feat(backend): calculate contextual query type counts for drilldown filters --- .../audit_management/dashboard.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/audit_management/audit_management/dashboard.py b/audit_management/audit_management/dashboard.py index 76e3c76..35f4b85 100644 --- a/audit_management/audit_management/dashboard.py +++ b/audit_management/audit_management/dashboard.py @@ -180,6 +180,7 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, manager_parents_contextual_tuple = tuple(manager_parents_contextual) if manager_parents_contextual else ("None",) stage_counts = {} + query_type_counts = {} time_counts = {"Today": 0, "Yesterday": 0, "Last Week": 0, "All Time": 0} if manager_parents_contextual: @@ -188,10 +189,37 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, elif 'No Response' in status_list: child_status = 'No Response' if child_status: + # 1. Stage Counts stg_sql = f"SELECT stage_name, COUNT(DISTINCT parent) as count FROM `tabAudit Items` WHERE status = %s AND parent IN %s GROUP BY stage_name" stg_data = frappe.db.sql(stg_sql, (child_status, manager_parents_contextual_tuple), as_dict=True) stage_counts = {d.stage_name: d.count for d in stg_data} + # 2. Query Type Counts (Contextual to status, stage, time) + qt_parents_filters = counter_filters.copy() + child_qt_conds = [f"status = '{child_status}'"] + qt_params = [] + if item_stage_list: + child_qt_conds.append("stage_name IN %s") + qt_params.append(tuple(item_stage_list)) + if time_filter_list: + t_conds = [] + fld = "response_time" if child_status == 'Responded' else "pending_time" + for t in time_filter_list: + if t == "Today": t_conds.append(f"DATE({fld}) = CURDATE()") + elif t == "Yesterday": t_conds.append(f"DATE({fld}) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)") + elif t == "Last Week": t_conds.append(f"DATE({fld}) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)") + if t_conds: child_qt_conds.append("(" + " OR ".join(t_conds) + ")") + + qt_child_sql = f"SELECT DISTINCT parent FROM `tabAudit Items` WHERE {' AND '.join(child_qt_conds)}" + qt_child_parents_sql = frappe.db.sql(qt_child_sql, tuple(qt_params)) + qt_child_parents = [r[0] for r in qt_child_parents_sql] if qt_child_parents_sql else [] + + if qt_child_parents: + qt_parents_filters["name"] = ["in", qt_child_parents] + qt_data = frappe.db.get_all("My Audits", filters=qt_parents_filters, fields=["query_type", "count(name) as count"], group_by="query_type") + query_type_counts = {d.query_type: d.count for d in qt_data if d.query_type} + + # 3. Time Counts field = "response_time" if child_status == 'Responded' else "pending_time" time_counts["Today"] = frappe.db.sql(f"SELECT COUNT(DISTINCT parent) FROM `tabAudit Items` WHERE status = %s AND parent IN %s AND DATE({field}) = CURDATE()", (child_status, manager_parents_contextual_tuple))[0][0] or 0 time_counts["Yesterday"] = frappe.db.sql(f"SELECT COUNT(DISTINCT parent) FROM `tabAudit Items` WHERE status = %s AND parent IN %s AND DATE({field}) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)", (child_status, manager_parents_contextual_tuple))[0][0] or 0 @@ -297,6 +325,7 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, "closed_count": closed_count, "draft_count": draft_count, "stage_counts": stage_counts, + "query_type_counts": query_type_counts, "time_counts": time_counts, "pending_list": pending_for_me_list, "recent_list": recent_list, From 2de935425b2c028a5fdbc99a9ba86e91d2789894 Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Fri, 5 Jun 2026 13:24:53 +0530 Subject: [PATCH 5/6] feat(frontend): display query type counts in drilldown section --- audit_management/fixtures/custom_html_block.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audit_management/fixtures/custom_html_block.json b/audit_management/fixtures/custom_html_block.json index b74557c..446ac01 100644 --- a/audit_management/fixtures/custom_html_block.json +++ b/audit_management/fixtures/custom_html_block.json @@ -7,7 +7,7 @@ "name": "Audit Management", "private": 0, "roles": [], - "script": "(function () {\n const root = root_element || document;\n let userRole = '';\n let userStages = [];\n let currentStatusFilter = [];\n let currentRiskFilter = [];\n let pendingStart = 0;\n let recentStart = 0;\n let currentItemStages = [];\n let currentTimeFilter = [];\n let currentQueryTypeFilter = [];\n\n const STORAGE_KEY = `audit_dashboard_settings_${frappe.session.user}`;\n\n const saveFilters = () => {\n const data = {\n status: currentStatusFilter,\n risk: currentRiskFilter,\n stages: currentItemStages,\n time: currentTimeFilter,\n query_type: currentQueryTypeFilter\n };\n localStorage.setItem(STORAGE_KEY, JSON.stringify(data));\n };\n\n const loadFilters = () => {\n const saved = localStorage.getItem(STORAGE_KEY);\n if (saved) {\n try {\n const data = JSON.parse(saved);\n currentStatusFilter = data.status || [];\n currentRiskFilter = data.risk || [];\n currentItemStages = data.stages || [];\n currentTimeFilter = data.time || [];\n currentQueryTypeFilter = data.query_type || [];\n } catch (e) { console.error('Error loading filters', e); }\n }\n };\n\n loadFilters();\n\n const upd = (id, val) => { \n const el = root.querySelector('#' + id); \n if (el) el.innerText = val ?? 0; \n };\n\n window.toggle_dropdown = (e) => {\n e.stopPropagation();\n const dd = root.querySelector('#actions-dropdown');\n if (dd) dd.style.display = dd.style.display === 'none' ? 'block' : 'none';\n };\n\n window.toggle_filter_dropdown = (e, type) => {\n e.stopPropagation();\n root.querySelectorAll('.multiselect-list').forEach(d => {\n if (d.id !== 'filter-dropdown-' + type) d.style.display = 'none';\n });\n const dd = root.querySelector('#filter-dropdown-' + type);\n if (dd) dd.style.display = dd.style.display === 'none' ? 'block' : 'none';\n };\n\n window.addEventListener('click', () => {\n root.querySelectorAll('.custom-dropdown, .multiselect-list').forEach(d => d.style.display = 'none');\n });\n\n const resetChildFilters = () => {\n currentItemStages = [];\n currentTimeFilter = [];\n currentQueryTypeFilter = [];\n root.querySelectorAll('.stage-item-checkbox, .time-checkbox, .query-type-item-checkbox').forEach(cb => cb.checked = false);\n };\n\n window.handle_checkbox_change = (type) => {\n const checkboxes = root.querySelectorAll('.' + type + '-checkbox:checked');\n const selected = Array.from(checkboxes).map(cb => cb.value);\n if (type === 'status') currentStatusFilter = selected; else currentRiskFilter = selected;\n \n const label = root.querySelector('#selected-' + type + '-label');\n const defaultLabel = type.charAt(0).toUpperCase() + type.slice(1);\n \n if (selected.length === 0) label.innerText = defaultLabel;\n else if (selected.length === 1) label.innerText = selected[0];\n else label.innerText = selected.length + ' Selected';\n\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (clearBtn) clearBtn.style.display = (currentStatusFilter.length > 0 || currentRiskFilter.length > 0) ? 'flex' : 'none';\n \n resetChildFilters();\n saveFilters();\n refresh();\n };\n\n window.clear_filters = () => {\n root.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = false);\n currentStatusFilter = []; currentRiskFilter = []; currentItemStages = []; currentTimeFilter = []; currentQueryTypeFilter = [];\n root.querySelector('#selected-status-label').innerText = 'Status';\n root.querySelector('#selected-risk-label').innerText = 'Risk';\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (clearBtn) clearBtn.style.display = 'none';\n saveFilters();\n refresh();\n };\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name.split('-').pop()}\n ${i.emp_branch || '---'}\n ${i.audit_query_subject_box || '---'}\n ${i.emp_division || '---'}\n ${i.status || '---'}\n ${i.risk || 'Normal'}\n ${i.aging || 0}\n ${frappe.datetime.str_to_user(i.creation).split(' ')[0]}${frappe.datetime.comment_when(i.creation)}\n `).join('');\n\n window.load_more_data = (type) => {\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: {\n pending_start: type === 'pending' ? pendingStart + 10 : pendingStart,\n recent_start: type === 'recent' ? recentStart + 10 : recentStart,\n status: currentStatusFilter.join(','),\n risk: currentRiskFilter.join(','),\n item_stages: currentItemStages.join(','),\n time_filter: currentTimeFilter.join(','),\n query_type: currentQueryTypeFilter.join(',')\n },\n callback: function (r) {\n if (!r.message || !r.message.success) return;\n const d = r.message;\n if (type === 'pending') {\n pendingStart += 10;\n root.querySelector('#stage-items').insertAdjacentHTML('beforeend', renderRows(d.pending_list));\n const btn = root.querySelector('#pending-more-btn');\n if (btn) btn.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n recentStart += 10;\n root.querySelector('#activity-body').insertAdjacentHTML('beforeend', renderRows(d.recent_list));\n const btn = root.querySelector('#recent-more-btn');\n if (btn) btn.style.display = d.has_more_recent ? 'flex' : 'none';\n }\n }\n });\n };\n\n function refresh() {\n pendingStart = 0; recentStart = 0;\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: { \n pending_start: 0, recent_start: 0, \n status: currentStatusFilter.join(','), \n risk: currentRiskFilter.join(','),\n item_stages: currentItemStages.join(','),\n time_filter: currentTimeFilter.join(','),\n query_type: currentQueryTypeFilter.join(',')\n },\n callback: function (r) {\n if (!r.message || !r.message.success) return;\n const d = r.message; userRole = d.role_type; userStages = d.user_stages || [];\n console.log('Dashboard Refresh:', { role: userRole, status: currentStatusFilter, time_counts: d.time_counts });\n\n const statusDropdown = root.querySelector('#filter-dropdown-status');\n const riskDropdown = root.querySelector('#filter-dropdown-risk');\n \n if (statusDropdown && !statusDropdown.hasChildNodes()) {\n const statusOps = userRole === 'stage_user' ? ['Pending', 'Responded', 'No Response'] : ['Draft', 'Pending', 'Closed'];\n statusDropdown.innerHTML = statusOps.map(opt => `
${opt}
`).join('');\n const riskOps = ['High', 'Medium', 'Normal'];\n riskDropdown.innerHTML = riskOps.map(opt => `
${opt}
`).join('');\n \n if (currentStatusFilter.length > 0) {\n const label = root.querySelector('#selected-status-label');\n if (label) label.innerText = currentStatusFilter.length === 1 ? currentStatusFilter[0] : currentStatusFilter.length + ' Selected';\n }\n if (currentRiskFilter.length > 0) {\n const label = root.querySelector('#selected-risk-label');\n if (label) label.innerText = currentRiskFilter.length === 1 ? currentRiskFilter[0] : currentRiskFilter.length + ' Selected';\n }\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (clearBtn) clearBtn.style.display = (currentStatusFilter.length > 0 || currentRiskFilter.length > 0) ? 'flex' : 'none';\n }\n\n // Sync Timeframe Checkboxes (Always run)\n root.querySelectorAll('.time-checkbox').forEach(cb => {\n cb.checked = currentTimeFilter.includes(cb.value);\n });\n\n const stageView = root.querySelector('#stage-view');\n const managerView = root.querySelector('#manager-view');\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n const totalCard = root.querySelector('#total-card');\n const nrCard = root.querySelector('#nr-card');\n const respCard = root.querySelector('#resp-card');\n const closedCard = root.querySelector('#c-card');\n const sReportBtn = root.querySelector('#stage-report-btn');\n \n root.querySelectorAll('.compact-stat-card').forEach(c => c.classList.remove('active-card'));\n\n if (userRole === 'stage_user') {\n if (stageView) stageView.style.display = 'block';\n if (managerView) managerView.style.display = 'none';\n if (masterCapsules) {\n masterCapsules.style.display = 'flex';\n root.querySelectorAll('.multiselect-container, .dropdown-wrapper, .create-btn-capsule').forEach(el => el.style.display = 'none');\n if (sReportBtn) sReportBtn.style.display = 'flex';\n }\n if (draftCard) draftCard.style.display = 'none';\n if (totalCard) totalCard.style.display = 'none';\n if (respCard) respCard.style.display = 'none';\n if (nrCard) nrCard.style.display = 'block';\n \n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-nr', d.not_responded_count);\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n\n if (currentStatusFilter.length > 0) {\n const activeStatus = currentStatusFilter[0];\n if (activeStatus === 'Pending') root.querySelector('#p-card')?.classList.add('active-card');\n else if (activeStatus === 'No Response') root.querySelector('#nr-card')?.classList.add('active-card');\n else if (activeStatus === 'Responded') root.querySelector('#c-card')?.classList.add('active-card');\n }\n\n const stageItems = root.querySelector('#stage-items');\n const pendingMore = root.querySelector('#pending-more-btn');\n if (d.pending_list && d.pending_list.length > 0) {\n stageItems.innerHTML = renderRows(d.pending_list);\n if (pendingMore) pendingMore.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n stageItems.innerHTML = 'No records found';\n if (pendingMore) pendingMore.style.display = 'none';\n }\n\n } else {\n if (stageView) stageView.style.display = 'none';\n if (managerView) managerView.style.display = 'block';\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (sReportBtn) sReportBtn.style.display = 'none';\n if (draftCard) draftCard.style.display = 'block';\n if (totalCard) totalCard.style.display = 'block';\n if (nrCard) nrCard.style.display = 'block';\n if (respCard) respCard.style.display = 'block';\n if (closedCard) closedCard.style.display = 'block';\n \n upd('val-draft', d.draft_count);\n upd('val-total', (d.draft_count || 0) + (d.total_pending || 0) + (d.closed_count || 0));\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n upd('val-nr', d.not_responded_count);\n upd('val-resp', d.responded_by_me);\n\n if (currentStatusFilter.length > 0) {\n const activeStatus = currentStatusFilter[0];\n if (activeStatus === 'Draft') root.querySelector('#draft-card')?.classList.add('active-card');\n else if (activeStatus === 'Pending') root.querySelector('#p-card')?.classList.add('active-card');\n else if (activeStatus === 'Closed') root.querySelector('#c-card')?.classList.add('active-card');\n else if (activeStatus === 'Responded') root.querySelector('#resp-card')?.classList.add('active-card');\n else if (activeStatus === 'No Response') root.querySelector('#nr-card')?.classList.add('active-card');\n } else if (currentRiskFilter.length === 0) {\n root.querySelector('#total-card')?.classList.add('active-card');\n }\n\n const drilldown = root.querySelector('#drilldown-section');\n if (drilldown) {\n const showDrill = (currentStatusFilter.includes('Responded') || currentStatusFilter.includes('No Response'));\n drilldown.style.display = showDrill ? 'block' : 'none';\n \n if (showDrill) {\n const tc = d.time_counts || {}; \n const setTC = (id, val) => { const el = root.querySelector(id); if (el) el.innerText = '(' + (val || 0) + ')'; };\n setTC('#count-today', tc.Today);\n setTC('#count-yest', tc.Yesterday);\n setTC('#count-week', tc['Last Week']);\n setTC('#count-all', tc['All Time']);\n\n const stageList = root.querySelector('#stage-checkbox-list');\n const queryTypeList = root.querySelector('#query-type-checkbox-list');\n \n if (stageList) {\n frappe.call({\n method: 'frappe.client.get_list',\n args: { doctype: 'Audit Stage', fields: ['name'], order_by: 'name asc' },\n callback: (r) => {\n if (r.message) {\n const counts = d.stage_counts || {};\n stageList.innerHTML = r.message.map(s => {\n const count = counts[s.name] || 0;\n const isChecked = currentItemStages.includes(s.name) ? 'checked' : '';\n return ``;\n }).join('');\n }\n }\n });\n }\n if (queryTypeList) {\n frappe.call({\n method: 'frappe.client.get_list',\n args: { doctype: 'Audit Query Type', fields: ['name'], order_by: 'name asc' },\n callback: (r_qt) => {\n if (r_qt.message) {\n queryTypeList.innerHTML = r_qt.message.map(qt => {\n const isChecked = currentQueryTypeFilter.includes(qt.name) ? 'checked' : '';\n return ``;\n }).join('');\n }\n }\n });\n }\n }\n }\n\n const activityBody = root.querySelector('#activity-body');\n const recentMore = root.querySelector('#recent-more-btn');\n if (d.recent_list && d.recent_list.length > 0) {\n activityBody.innerHTML = renderRows(d.recent_list);\n if (recentMore) recentMore.style.display = d.has_more_recent ? 'flex' : 'none';\n } else {\n activityBody.innerHTML = 'No recent activity';\n if (recentMore) recentMore.style.display = 'none';\n }\n }\n }\n });\n }\n\n window.show_reports_selection = () => {\n const reports = ['My Audits Report', 'Pending Audit Queries Aging Report', 'Process Technical Improvement Commitment Report', 'Recurring Operational Reports'];\n let html = `
\n ${reports.map(r => ``).join('')}\n
`;\n let d = new frappe.ui.Dialog({ title: 'Select Report', fields: [{ fieldtype: 'HTML', fieldname: 'reports_html', options: html }] });\n d.show();\n };\n\n window.handle_total_click = () => { \n frappe.set_route('List', 'My Audits', { status: '' });\n };\n\n window.handle_draft_click = () => {\n frappe.set_route('List', 'My Audits', { status: 'Draft' });\n };\n\n window.handle_responded_click = () => {\n if (userRole === 'stage_user') {\n frappe.set_route('List', 'My Audits', { 'Audit Items.status': 'Responded', 'Audit Items.stage_name': ['in', userStages] });\n } else {\n currentStatusFilter = ['Responded']; currentItemStages = []; currentTimeFilter = ['Today']; currentQueryTypeFilter = [];\n root.querySelectorAll('.time-checkbox').forEach(cb => cb.checked = (cb.value === 'Today'));\n saveFilters();\n refresh();\n }\n };\n\n window.handle_not_responded_click = () => {\n if (userRole === 'stage_user') {\n frappe.set_route('List', 'My Audits', { 'Audit Items.status': 'No Response', 'Audit Items.stage_name': ['in', userStages] });\n } else {\n currentStatusFilter = ['No Response']; currentItemStages = []; currentTimeFilter = ['Today']; currentQueryTypeFilter = [];\n root.querySelectorAll('.time-checkbox').forEach(cb => cb.checked = (cb.value === 'Today'));\n saveFilters();\n refresh();\n }\n };\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') {\n frappe.set_route('List', 'My Audits', { 'Audit Items.status': 'Pending', 'Audit Items.stage_name': ['in', userStages] });\n } else {\n frappe.set_route('List', 'My Audits', { status: 'Pending' });\n }\n };\n\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') {\n frappe.set_route('List', 'My Audits', { 'Audit Items.status': 'Responded', 'Audit Items.stage_name': ['in', userStages] });\n } else {\n frappe.set_route('List', 'My Audits', { status: 'Closed' });\n }\n };\n\n window.handle_item_stage_change = () => {\n currentItemStages = Array.from(root.querySelectorAll('.stage-item-checkbox:checked')).map(cb => cb.value);\n saveFilters();\n refresh();\n };\n\n window.handle_query_type_change = () => {\n currentQueryTypeFilter = Array.from(root.querySelectorAll('.query-type-item-checkbox:checked')).map(cb => cb.value);\n saveFilters();\n refresh();\n };\n\n window.handle_time_filter_change = () => {\n currentTimeFilter = Array.from(root.querySelectorAll('.time-checkbox:checked')).map(cb => cb.value);\n saveFilters();\n refresh();\n };\n\n $(document).on('workspace_render', refresh); refresh();\n})();\n", + "script": "(function () {\n const root = root_element || document;\n let userRole = '';\n let userStages = [];\n let currentStatusFilter = [];\n let currentRiskFilter = [];\n let pendingStart = 0;\n let recentStart = 0;\n let currentItemStages = [];\n let currentTimeFilter = [];\n let currentQueryTypeFilter = [];\n\n const STORAGE_KEY = `audit_dashboard_settings_${frappe.session.user}`;\n\n const saveFilters = () => {\n const data = {\n status: currentStatusFilter,\n risk: currentRiskFilter,\n stages: currentItemStages,\n time: currentTimeFilter,\n query_type: currentQueryTypeFilter\n };\n localStorage.setItem(STORAGE_KEY, JSON.stringify(data));\n };\n\n const loadFilters = () => {\n const saved = localStorage.getItem(STORAGE_KEY);\n if (saved) {\n try {\n const data = JSON.parse(saved);\n currentStatusFilter = data.status || [];\n currentRiskFilter = data.risk || [];\n currentItemStages = data.stages || [];\n currentTimeFilter = data.time || [];\n currentQueryTypeFilter = data.query_type || [];\n } catch (e) { console.error('Error loading filters', e); }\n }\n };\n\n loadFilters();\n\n const upd = (id, val) => { \n const el = root.querySelector('#' + id); \n if (el) el.innerText = val ?? 0; \n };\n\n window.toggle_dropdown = (e) => {\n e.stopPropagation();\n const dd = root.querySelector('#actions-dropdown');\n if (dd) dd.style.display = dd.style.display === 'none' ? 'block' : 'none';\n };\n\n window.toggle_filter_dropdown = (e, type) => {\n e.stopPropagation();\n root.querySelectorAll('.multiselect-list').forEach(d => {\n if (d.id !== 'filter-dropdown-' + type) d.style.display = 'none';\n });\n const dd = root.querySelector('#filter-dropdown-' + type);\n if (dd) dd.style.display = dd.style.display === 'none' ? 'block' : 'none';\n };\n\n window.addEventListener('click', () => {\n root.querySelectorAll('.custom-dropdown, .multiselect-list').forEach(d => d.style.display = 'none');\n });\n\n const resetChildFilters = () => {\n currentItemStages = [];\n currentTimeFilter = [];\n currentQueryTypeFilter = [];\n root.querySelectorAll('.stage-item-checkbox, .time-checkbox, .query-type-item-checkbox').forEach(cb => cb.checked = false);\n };\n\n window.handle_checkbox_change = (type) => {\n const checkboxes = root.querySelectorAll('.' + type + '-checkbox:checked');\n const selected = Array.from(checkboxes).map(cb => cb.value);\n if (type === 'status') currentStatusFilter = selected; else currentRiskFilter = selected;\n \n const label = root.querySelector('#selected-' + type + '-label');\n const defaultLabel = type.charAt(0).toUpperCase() + type.slice(1);\n \n if (selected.length === 0) label.innerText = defaultLabel;\n else if (selected.length === 1) label.innerText = selected[0];\n else label.innerText = selected.length + ' Selected';\n\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (clearBtn) clearBtn.style.display = (currentStatusFilter.length > 0 || currentRiskFilter.length > 0) ? 'flex' : 'none';\n \n resetChildFilters();\n saveFilters();\n refresh();\n };\n\n window.clear_filters = () => {\n root.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = false);\n currentStatusFilter = []; currentRiskFilter = []; currentItemStages = []; currentTimeFilter = []; currentQueryTypeFilter = [];\n root.querySelector('#selected-status-label').innerText = 'Status';\n root.querySelector('#selected-risk-label').innerText = 'Risk';\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (clearBtn) clearBtn.style.display = 'none';\n saveFilters();\n refresh();\n };\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name.split('-').pop()}\n ${i.emp_branch || '---'}\n ${i.audit_query_subject_box || '---'}\n ${i.emp_division || '---'}\n ${i.status || '---'}\n ${i.risk || 'Normal'}\n ${i.aging || 0}\n ${frappe.datetime.str_to_user(i.creation).split(' ')[0]}${frappe.datetime.comment_when(i.creation)}\n `).join('');\n\n window.load_more_data = (type) => {\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: {\n pending_start: type === 'pending' ? pendingStart + 10 : pendingStart,\n recent_start: type === 'recent' ? recentStart + 10 : recentStart,\n status: currentStatusFilter.join(','),\n risk: currentRiskFilter.join(','),\n item_stages: currentItemStages.join(','),\n time_filter: currentTimeFilter.join(','),\n query_type: currentQueryTypeFilter.join(',')\n },\n callback: function (r) {\n if (!r.message || !r.message.success) return;\n const d = r.message;\n if (type === 'pending') {\n pendingStart += 10;\n root.querySelector('#stage-items').insertAdjacentHTML('beforeend', renderRows(d.pending_list));\n const btn = root.querySelector('#pending-more-btn');\n if (btn) btn.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n recentStart += 10;\n root.querySelector('#activity-body').insertAdjacentHTML('beforeend', renderRows(d.recent_list));\n const btn = root.querySelector('#recent-more-btn');\n if (btn) btn.style.display = d.has_more_recent ? 'flex' : 'none';\n }\n }\n });\n };\n\n function refresh() {\n pendingStart = 0; recentStart = 0;\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: { \n pending_start: 0, recent_start: 0, \n status: currentStatusFilter.join(','), \n risk: currentRiskFilter.join(','),\n item_stages: currentItemStages.join(','),\n time_filter: currentTimeFilter.join(','),\n query_type: currentQueryTypeFilter.join(',')\n },\n callback: function (r) {\n if (!r.message || !r.message.success) return;\n const d = r.message; userRole = d.role_type; userStages = d.user_stages || [];\n console.log('Dashboard Refresh:', { role: userRole, status: currentStatusFilter, time_counts: d.time_counts });\n\n const statusDropdown = root.querySelector('#filter-dropdown-status');\n const riskDropdown = root.querySelector('#filter-dropdown-risk');\n \n if (statusDropdown && !statusDropdown.hasChildNodes()) {\n const statusOps = userRole === 'stage_user' ? ['Pending', 'Responded', 'No Response'] : ['Draft', 'Pending', 'Closed'];\n statusDropdown.innerHTML = statusOps.map(opt => `
${opt}
`).join('');\n const riskOps = ['High', 'Medium', 'Normal'];\n riskDropdown.innerHTML = riskOps.map(opt => `
${opt}
`).join('');\n \n if (currentStatusFilter.length > 0) {\n const label = root.querySelector('#selected-status-label');\n if (label) label.innerText = currentStatusFilter.length === 1 ? currentStatusFilter[0] : currentStatusFilter.length + ' Selected';\n }\n if (currentRiskFilter.length > 0) {\n const label = root.querySelector('#selected-risk-label');\n if (label) label.innerText = currentRiskFilter.length === 1 ? currentRiskFilter[0] : currentRiskFilter.length + ' Selected';\n }\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (clearBtn) clearBtn.style.display = (currentStatusFilter.length > 0 || currentRiskFilter.length > 0) ? 'flex' : 'none';\n }\n\n // Sync Timeframe Checkboxes (Always run)\n root.querySelectorAll('.time-checkbox').forEach(cb => {\n cb.checked = currentTimeFilter.includes(cb.value);\n });\n\n const stageView = root.querySelector('#stage-view');\n const managerView = root.querySelector('#manager-view');\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n const totalCard = root.querySelector('#total-card');\n const nrCard = root.querySelector('#nr-card');\n const respCard = root.querySelector('#resp-card');\n const closedCard = root.querySelector('#c-card');\n const sReportBtn = root.querySelector('#stage-report-btn');\n \n root.querySelectorAll('.compact-stat-card').forEach(c => c.classList.remove('active-card'));\n\n if (userRole === 'stage_user') {\n if (stageView) stageView.style.display = 'block';\n if (managerView) managerView.style.display = 'none';\n if (masterCapsules) {\n masterCapsules.style.display = 'flex';\n root.querySelectorAll('.multiselect-container, .dropdown-wrapper, .create-btn-capsule').forEach(el => el.style.display = 'none');\n if (sReportBtn) sReportBtn.style.display = 'flex';\n }\n if (draftCard) draftCard.style.display = 'none';\n if (totalCard) totalCard.style.display = 'none';\n if (respCard) respCard.style.display = 'none';\n if (nrCard) nrCard.style.display = 'block';\n \n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-nr', d.not_responded_count);\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n\n if (currentStatusFilter.length > 0) {\n const activeStatus = currentStatusFilter[0];\n if (activeStatus === 'Pending') root.querySelector('#p-card')?.classList.add('active-card');\n else if (activeStatus === 'No Response') root.querySelector('#nr-card')?.classList.add('active-card');\n else if (activeStatus === 'Responded') root.querySelector('#c-card')?.classList.add('active-card');\n }\n\n const stageItems = root.querySelector('#stage-items');\n const pendingMore = root.querySelector('#pending-more-btn');\n if (d.pending_list && d.pending_list.length > 0) {\n stageItems.innerHTML = renderRows(d.pending_list);\n if (pendingMore) pendingMore.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n stageItems.innerHTML = 'No records found';\n if (pendingMore) pendingMore.style.display = 'none';\n }\n\n } else {\n if (stageView) stageView.style.display = 'none';\n if (managerView) managerView.style.display = 'block';\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (sReportBtn) sReportBtn.style.display = 'none';\n if (draftCard) draftCard.style.display = 'block';\n if (totalCard) totalCard.style.display = 'block';\n if (nrCard) nrCard.style.display = 'block';\n if (respCard) respCard.style.display = 'block';\n if (closedCard) closedCard.style.display = 'block';\n \n upd('val-draft', d.draft_count);\n upd('val-total', (d.draft_count || 0) + (d.total_pending || 0) + (d.closed_count || 0));\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n upd('val-nr', d.not_responded_count);\n upd('val-resp', d.responded_by_me);\n\n if (currentStatusFilter.length > 0) {\n const activeStatus = currentStatusFilter[0];\n if (activeStatus === 'Draft') root.querySelector('#draft-card')?.classList.add('active-card');\n else if (activeStatus === 'Pending') root.querySelector('#p-card')?.classList.add('active-card');\n else if (activeStatus === 'Closed') root.querySelector('#c-card')?.classList.add('active-card');\n else if (activeStatus === 'Responded') root.querySelector('#resp-card')?.classList.add('active-card');\n else if (activeStatus === 'No Response') root.querySelector('#nr-card')?.classList.add('active-card');\n } else if (currentRiskFilter.length === 0) {\n root.querySelector('#total-card')?.classList.add('active-card');\n }\n\n const drilldown = root.querySelector('#drilldown-section');\n if (drilldown) {\n const showDrill = (currentStatusFilter.includes('Responded') || currentStatusFilter.includes('No Response'));\n drilldown.style.display = showDrill ? 'block' : 'none';\n \n if (showDrill) {\n const tc = d.time_counts || {}; \n const setTC = (id, val) => { const el = root.querySelector(id); if (el) el.innerText = '(' + (val || 0) + ')'; };\n setTC('#count-today', tc.Today);\n setTC('#count-yest', tc.Yesterday);\n setTC('#count-week', tc['Last Week']);\n setTC('#count-all', tc['All Time']);\n\n const stageList = root.querySelector('#stage-checkbox-list');\n const queryTypeList = root.querySelector('#query-type-checkbox-list');\n \n if (stageList) {\n frappe.call({\n method: 'frappe.client.get_list',\n args: { doctype: 'Audit Stage', fields: ['name'], order_by: 'name asc' },\n callback: (r) => {\n if (r.message) {\n const counts = d.stage_counts || {};\n stageList.innerHTML = r.message.map(s => {\n const count = counts[s.name] || 0;\n const isChecked = currentItemStages.includes(s.name) ? 'checked' : '';\n return ``;\n }).join('');\n }\n }\n });\n }\n if (queryTypeList) {\n frappe.call({\n method: 'frappe.client.get_list',\n args: { doctype: 'Audit Query Type', fields: ['name'], order_by: 'name asc' },\n callback: (r_qt) => {\n if (r_qt.message) {\n const qtCounts = d.query_type_counts || {};\n queryTypeList.innerHTML = r_qt.message.map(qt => {\n const count = qtCounts[qt.name] || 0;\n const isChecked = currentQueryTypeFilter.includes(qt.name) ? 'checked' : '';\n return ``;\n }).join('');\n }\n }\n });\n }\n }\n }\n\n const activityBody = root.querySelector('#activity-body');\n const recentMore = root.querySelector('#recent-more-btn');\n if (d.recent_list && d.recent_list.length > 0) {\n activityBody.innerHTML = renderRows(d.recent_list);\n if (recentMore) recentMore.style.display = d.has_more_recent ? 'flex' : 'none';\n } else {\n activityBody.innerHTML = 'No recent activity';\n if (recentMore) recentMore.style.display = 'none';\n }\n }\n }\n });\n }\n\n window.show_reports_selection = () => {\n const reports = ['My Audits Report', 'Pending Audit Queries Aging Report', 'Process Technical Improvement Commitment Report', 'Recurring Operational Reports'];\n let html = `
\n ${reports.map(r => ``).join('')}\n
`;\n let d = new frappe.ui.Dialog({ title: 'Select Report', fields: [{ fieldtype: 'HTML', fieldname: 'reports_html', options: html }] });\n d.show();\n };\n\n window.handle_total_click = () => { \n frappe.set_route('List', 'My Audits', { status: '' });\n };\n\n window.handle_draft_click = () => {\n frappe.set_route('List', 'My Audits', { status: 'Draft' });\n };\n\n window.handle_responded_click = () => {\n if (userRole === 'stage_user') {\n frappe.set_route('List', 'My Audits', { 'Audit Items.status': 'Responded', 'Audit Items.stage_name': ['in', userStages] });\n } else {\n currentStatusFilter = ['Responded']; currentItemStages = []; currentTimeFilter = ['Today']; currentQueryTypeFilter = [];\n root.querySelectorAll('.time-checkbox').forEach(cb => cb.checked = (cb.value === 'Today'));\n saveFilters();\n refresh();\n }\n };\n\n window.handle_not_responded_click = () => {\n if (userRole === 'stage_user') {\n frappe.set_route('List', 'My Audits', { 'Audit Items.status': 'No Response', 'Audit Items.stage_name': ['in', userStages] });\n } else {\n currentStatusFilter = ['No Response']; currentItemStages = []; currentTimeFilter = ['Today']; currentQueryTypeFilter = [];\n root.querySelectorAll('.time-checkbox').forEach(cb => cb.checked = (cb.value === 'Today'));\n saveFilters();\n refresh();\n }\n };\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') {\n frappe.set_route('List', 'My Audits', { 'Audit Items.status': 'Pending', 'Audit Items.stage_name': ['in', userStages] });\n } else {\n frappe.set_route('List', 'My Audits', { status: 'Pending' });\n }\n };\n\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') {\n frappe.set_route('List', 'My Audits', { 'Audit Items.status': 'Responded', 'Audit Items.stage_name': ['in', userStages] });\n } else {\n frappe.set_route('List', 'My Audits', { status: 'Closed' });\n }\n };\n\n window.handle_item_stage_change = () => {\n currentItemStages = Array.from(root.querySelectorAll('.stage-item-checkbox:checked')).map(cb => cb.value);\n saveFilters();\n refresh();\n };\n\n window.handle_query_type_change = () => {\n currentQueryTypeFilter = Array.from(root.querySelectorAll('.query-type-item-checkbox:checked')).map(cb => cb.value);\n saveFilters();\n refresh();\n };\n\n window.handle_time_filter_change = () => {\n currentTimeFilter = Array.from(root.querySelectorAll('.time-checkbox:checked')).map(cb => cb.value);\n saveFilters();\n refresh();\n };\n\n $(document).on('workspace_render', refresh); refresh();\n})();\n", "style": ".audit-dashboard-light { background: #f1f5f9; color: #1e293b; padding: 20px; border-radius: 16px; font-family: 'Inter', sans-serif; border: 1px solid #e2e8f0; }\n.db-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; flex-wrap: nowrap; gap: 15px; }\n.title-wrap { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }\n.db-title { font-size: 20px; font-weight: 800; color: #0f172a; margin: 0; white-space: nowrap; }\n.live-dot { width: 8px; height: 8px; background: #22c55e; border-radius: 50%; box-shadow: 0 0 10px rgba(34, 197, 94, 0.4); }\n\n.master-capsule-container { display: flex; gap: 6px; flex-wrap: nowrap; align-items: center; justify-content: flex-end; flex-grow: 1; }\n.master-capsule { background: #ffffff; padding: 4px 10px; border-radius: 50px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 6px; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); height: 30px; box-sizing: border-box; }\n.master-capsule:hover { border-color: #3b82f6; transform: translateY(-1px); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); }\n.master-capsule i { font-size: 13px; flex-shrink: 0; }\n.master-capsule span { font-size: 10px; font-weight: 700; color: #475569; white-space: nowrap; }\n.label-text { min-width: 40px; text-align: center; }\n\n.create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; }\n.create-btn-capsule i, .create-btn-capsule span { color: white !important; }\n.create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.02); }\n\n.dropdown-wrapper { position: relative; }\n.custom-dropdown { position: absolute; top: 110%; right: 0; background: white; border: 1px solid #e2e8f0; border-radius: 12px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); z-index: 1000; min-width: 180px; padding: 8px 0; }\n.dropdown-item { padding: 10px 16px; display: flex; align-items: center; gap: 12px; cursor: pointer; transition: 0.2s; font-size: 12px; font-weight: 600; color: #475569; }\n.dropdown-item:hover { background: #f1f5f9; color: #1e293b; }\n\n.grid-compact { display: grid; gap: 10px; margin-bottom: 24px; }\n.stats-grid { grid-template-columns: repeat(6, 1fr); }\n\n.blue-txt { color: #2563eb; } .purple-txt { color: #7c3aed; } .orange-txt { color: #ea580c; } .green-txt { color: #16a34a; } .red-txt { color: #dc2626; }\n\n.compact-stat-card { background: #ffffff; padding: 10px 5px; border-radius: 12px; border: 1px solid #e2e8f0; position: relative; overflow: hidden; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.02); display: flex; flex-direction: column; align-items: center; text-align: center; gap: 4px; }\n.stat-label-small { font-size: 10px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.4px; }\n.stat-val-small { font-size: 18px; font-weight: 800; color: #0f172a; }\n.icon-stat { font-size: 18px; opacity: 0.9; margin-bottom: 2px; }\n.blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; } .orange-bg { background: #f59e0b; } .red-bg { background: #ef4444; }\n\n.multiselect-container { position: relative; }\n.multiselect-list { position: absolute; top: 110%; left: 0; background: white; border: 1px solid #e2e8f0; border-radius: 12px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); z-index: 1000; min-width: 160px; padding: 8px 0; display: none; }\n.multiselect-item { padding: 8px 16px; display: flex; align-items: center; gap: 10px; cursor: pointer; transition: 0.2s; font-size: 12px; font-weight: 600; color: #475569; }\n.multiselect-item:hover { background: #f1f5f9; }\n.multiselect-item input[type='checkbox'] { cursor: pointer; width: 14px; height: 14px; }\n\n.mini-table { width: 100%; border-collapse: collapse; font-size: 12px; }\n.mini-table th { background: #f1f5f9; color: #475569; text-align: left; padding: 12px 12px; border-bottom: 2px solid #e2e8f0; font-weight: 700; text-transform: uppercase; font-size: 10px; }\n.mini-table td { padding: 14px 16px; color: #334155; border-bottom: 1px solid #f1f5f9; }\n.mini-table tr:hover { background: #f9fafb; cursor: pointer; }\n.t-id { font-weight: 800; color: #2563eb; }\n.t-status { background: #f1f5f9; padding: 4px 8px; border-radius: 6px; color: #475569; font-weight: 700; font-size: 10px; }\n.t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }\n.t-status.closed { background: #b9f9cf; color: #001a00; border: 1px solid #bbf7d0; }\n.t-risk { font-weight: 800; text-transform: uppercase; font-size: 10px; }\n.t-risk.high { background: #dc2626; color: white; padding: 4px 8px; border-radius: 6px; } .t-risk.medium { color: #d97706; } .t-risk.normal { color: #2563eb; }\n\n.load-more-btn { background: #ffffff; color: #475569; border: 1px solid #e2e8f0; padding: 8px 24px; border-radius: 50px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); letter-spacing: 0.5px; text-transform: uppercase; }\n.load-more-btn:hover { background: #f8fafc; border-color: #cbd5e1; color: #1e293b; transform: translateY(-1px); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); }\n.load-more-btn:active { transform: translateY(0); box-shadow: none; background: #f1f5f9; }\n\n@media (max-width: 768px) {\n .db-header { flex-direction: column; align-items: flex-start; }\n .master-capsule-container { width: 100%; justify-content: flex-start; flex-wrap: wrap; }\n .stats-grid { grid-template-columns: 1fr; }\n}\n.compact-stat-card.active-card { border-color: #2563eb; background: #eff6ff; transform: translateY(-2px); box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.1); }\n\n.drilldown-bar { background: #ffffff; padding: 15px; border-radius: 14px; border: 1px solid #e2e8f0; margin-bottom: 20px; }\n.filter-group { margin-bottom: 12px; }\n.filter-group-label { font-size: 10px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 8px; }\n.checkbox-row { display: flex; flex-wrap: wrap; gap: 15px; }\n.check-item { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 600; color: #475569; cursor: pointer; }\n" } ] \ No newline at end of file From e3e096bb726cbd27fda8c900890b9d287e80b5b1 Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Fri, 5 Jun 2026 14:01:10 +0530 Subject: [PATCH 6/6] fix(backend): resolve dashboard crash and implement contextual query type counts --- .../audit_management/dashboard.py | 49 ++++++------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/audit_management/audit_management/dashboard.py b/audit_management/audit_management/dashboard.py index 35f4b85..8d92bb3 100644 --- a/audit_management/audit_management/dashboard.py +++ b/audit_management/audit_management/dashboard.py @@ -47,6 +47,9 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, elif isinstance(time_filter, list): time_filter_list = time_filter try: + from audit_management.audit_management.utils import get_user_allowed_divisions + allowed_divisions = get_user_allowed_divisions(user) + # 1. 🟢 BASE FILTERS (Permissions) base_filters = {} if is_admin: pass @@ -55,7 +58,9 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, else: base_filters["emp_division"] = "None" elif is_member: base_filters["owner"] = user - # Note: stage_user (else) does NOT get base_filters by default to preserve assignment-based access + else: + if allowed_divisions: base_filters["emp_division"] = ["in", allowed_divisions] + else: base_filters["emp_division"] = "None" # 2. 🟢 COUNTER FILTERS (For all top 6 cards: Permissions + Risk ONLY) counter_filters = base_filters.copy() @@ -68,6 +73,10 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, if query_type_list: list_filters["query_type"] = ["in", query_type_list] + # Get Allowed Parents for counts (Global within segment) + allowed_parents_global = frappe.get_all("My Audits", filters=counter_filters, fields=["name"], pluck="name") + allowed_parents_global_tuple = tuple(allowed_parents_global) if allowed_parents_global else ("None",) + # 4. 🟢 STAGE USER LOGIC # Get All User's Assigned Stages (No division restriction for assigned items) assigned_stages = frappe.get_all("Audit Items", @@ -77,21 +86,12 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, stages_tuple = tuple(user_stages) if len(user_stages) > 1 else (user_stages[0],) # Count Queries for Stage User (Static - respect Risk but ignore Query Type) - # We still need to respect base_filters if they apply (e.g. if stage user is also a member/manager, but role_type logic handles priority) - # For a pure stage_user, we allow all assignments. - - # Helper to get allowed parent IDs based on counter_filters - allowed_parents_for_counts = frappe.get_all("My Audits", filters=counter_filters, fields=["name"], pluck="name") - # If pure stage_user and no risk filter, we might not want to restrict parents at all. - # But if they select Risk=High, we SHOULD restrict counts. - - # If no risk filter and pure stage user, allow all. if not risk_list and not (is_admin or is_manager or is_member): parent_limit_sql = "" parent_params = [] else: parent_limit_sql = " AND parent IN %s" - parent_params = [tuple(allowed_parents_for_counts) if allowed_parents_for_counts else ("None",)] + parent_params = [allowed_parents_global_tuple] pending_items_query = f"SELECT DISTINCT parent FROM `tabAudit Items` WHERE status = 'Pending' AND (user_id = %s OR email = %s) AND stage_name IN %s {parent_limit_sql}" responded_items_query = f"SELECT DISTINCT parent FROM `tabAudit Items` WHERE status = 'Responded' AND (user_id = %s OR email = %s) AND stage_name IN %s {parent_limit_sql}" @@ -155,9 +155,6 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, {"parent": item.name, "user_id": user, "stage_name": ["in", user_stages]}, "status") or item.status # 5. 🔵 MANAGER/ADMIN/MEMBER LOGIC - from audit_management.audit_management.utils import get_user_allowed_divisions - allowed_divisions = get_user_allowed_divisions(user) - # Top 4 Card Counts (Static - Counter Filters ignore Query Type) total_pending = frappe.db.count("My Audits", {**counter_filters, "status": "Pending"}) closed_count = frappe.db.count("My Audits", {**counter_filters, "status": "Closed"}) @@ -165,15 +162,13 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, total_count = frappe.db.count("My Audits", counter_filters) # Responded/Not Responded Counts (Static - Counter Filters ignore Query Type) - allowed_parents_for_global_counts = frappe.get_all("My Audits", filters=counter_filters, fields=["name"], pluck="name") responded_count_manager = 0 not_responded_count_manager = 0 - if allowed_parents_for_global_counts: - global_tuple = tuple(allowed_parents_for_global_counts) + if allowed_parents_global: resp_sql = "SELECT COUNT(DISTINCT parent) FROM `tabAudit Items` WHERE status = 'Responded' AND parent IN %s" nr_sql = "SELECT COUNT(DISTINCT parent) FROM `tabAudit Items` WHERE status = 'No Response' AND parent IN %s" - responded_count_manager = frappe.db.sql(resp_sql, (global_tuple,))[0][0] - not_responded_count_manager = frappe.db.sql(nr_sql, (global_tuple,))[0][0] + responded_count_manager = frappe.db.sql(resp_sql, (allowed_parents_global_tuple,))[0][0] + not_responded_count_manager = frappe.db.sql(nr_sql, (allowed_parents_global_tuple,))[0][0] # Contextual Drilldown Stats (Dynamic - respect all list_filters) manager_parents_contextual = frappe.get_all("My Audits", filters=list_filters, fields=["name"], pluck="name") @@ -278,22 +273,6 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, for idx, item in enumerate(recent_list, start=recent_start + 1): item["sr_no"] = idx - recent_list = frappe.get_all( - "My Audits", - filters=r_filters, - fields=["name", "audit_query_subject_box", "risk", "status", "emp_branch", "emp_division", "aging", "creation"], - order_by="creation desc", - limit_start=recent_start, - limit_page_length=page_length + 1 - ) - - if len(recent_list) > page_length: - has_more_recent = True - recent_list = recent_list[:page_length] - - for idx, item in enumerate(recent_list, start=recent_start + 1): - item["sr_no"] = idx - # 3. 🟣 ENHANCE BRANCH COLUMN (Existing Logic) all_lists = pending_for_me_list + recent_list if all_lists: