From 0f1a8f723e1972444c8deb9440f63453304e4a92 Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Mon, 25 May 2026 12:25:54 +0530 Subject: [PATCH 1/4] feat: implement audit management dashboard as a dedicated Frappe Page --- .../audit_management/dashboard.py | 2 + .../audit_management_dashboard.js | 262 ++++++++++++++++++ .../audit_management_dashboard.json | 26 ++ 3 files changed, 290 insertions(+) create mode 100644 audit_management/audit_management/page/audit_management_dashboard/audit_management_dashboard.js create mode 100644 audit_management/audit_management/page/audit_management_dashboard/audit_management_dashboard.json diff --git a/audit_management/audit_management/dashboard.py b/audit_management/audit_management/dashboard.py index dfce5e4..5aa9ea9 100644 --- a/audit_management/audit_management/dashboard.py +++ b/audit_management/audit_management/dashboard.py @@ -133,6 +133,7 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, 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) @@ -251,6 +252,7 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None, "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, + "total_count": (pending_for_me_count + responded_by_me_count + not_responded_count_me) if (is_member or is_manager or is_admin) == False else total_count, "total_pending": total_pending, "closed_count": closed_count, "draft_count": draft_count, diff --git a/audit_management/audit_management/page/audit_management_dashboard/audit_management_dashboard.js b/audit_management/audit_management/page/audit_management_dashboard/audit_management_dashboard.js new file mode 100644 index 0000000..ab2d670 --- /dev/null +++ b/audit_management/audit_management/page/audit_management_dashboard/audit_management_dashboard.js @@ -0,0 +1,262 @@ +frappe.pages['audit_management_dashboard'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'Audit Management Dashboard', + single_column: true + }); + + // CSS Injection + const style = document.createElement('style'); + style.innerHTML = ` + .audit-dashboard-light { background: #f1f5f9; color: #1e293b; padding: 20px; border-radius: 16px; font-family: 'Inter', sans-serif; border: 1px solid #e2e8f0; } + .db-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; flex-wrap: nowrap; gap: 15px; } + .title-wrap { display: flex; align-items: center; gap: 10px; flex-shrink: 0; } + .db-title { font-size: 20px; font-weight: 800; color: #0f172a; margin: 0; white-space: nowrap; } + .live-dot { width: 8px; height: 8px; background: #22c55e; border-radius: 50%; box-shadow: 0 0 10px rgba(34, 197, 94, 0.4); } + + .master-capsule-container { display: flex; gap: 6px; flex-wrap: nowrap; align-items: center; justify-content: flex-end; flex-grow: 1; } + .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; } + .master-capsule:hover { border-color: #3b82f6; transform: translateY(-1px); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); } + .master-capsule i { font-size: 13px; flex-shrink: 0; } + .master-capsule span { font-size: 10px; font-weight: 700; color: #475569; white-space: nowrap; } + .label-text { min-width: 40px; text-align: center; } + + .create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; } + .create-btn-capsule i, .create-btn-capsule span { color: white !important; } + .create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.02); } + + .dropdown-wrapper { position: relative; } + .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; } + .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; } + .dropdown-item:hover { background: #f1f5f9; color: #1e293b; } + + .grid-compact { display: grid; gap: 10px; margin-bottom: 24px; } + .stats-grid { grid-template-columns: repeat(6, 1fr); } + + .blue-txt { color: #2563eb; } .purple-txt { color: #7c3aed; } .orange-txt { color: #ea580c; } .green-txt { color: #16a34a; } .red-txt { color: #dc2626; } + + .compact-stat-card { background: #ffffff; padding: 10px 12px; border-radius: 10px; 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: row; align-items: center; gap: 8px; min-width: 140px; flex-wrap: nowrap; } + .stat-label-small { font-size: 10px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.4px; white-space: nowrap; flex-shrink: 0; } + .stat-val-small { font-size: 18px; font-weight: 800; color: #0f172a; margin-left: auto; flex-shrink: 0; } + .icon-stat { font-size: 13px; opacity: 0.9; flex-shrink: 0; } + .accent-bar { position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; } + .blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; } .orange-bg { background: #f59e0b; } .red-bg { background: #ef4444; } + + .multiselect-container { position: relative; } + .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; } + .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; } + .multiselect-item:hover { background: #f1f5f9; } + .multiselect-item input[type='checkbox'] { cursor: pointer; width: 14px; height: 14px; } + + .mini-table { width: 100%; border-collapse: collapse; font-size: 12px; } + .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; } + .mini-table td { padding: 14px 16px; color: #334155; border-bottom: 1px solid #f1f5f9; } + .mini-table tr:hover { background: #f9fafb; cursor: pointer; } + .t-id { font-weight: 800; color: #2563eb; } + .t-status { background: #f1f5f9; padding: 4px 8px; border-radius: 6px; color: #475569; font-weight: 700; font-size: 10px; } + .t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; } + .t-status.closed { background: #b9f9cf; color: #001a00; border: 1px solid #bbf7d0; } + .t-risk { font-weight: 800; text-transform: uppercase; font-size: 10px; } + .t-risk.high { background: #dc2626; color: white; padding: 4px 8px; border-radius: 6px; } .t-risk.medium { color: #d97706; } .t-risk.normal { color: #2563eb; } + + .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; } + .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); } + .load-more-btn:active { transform: translateY(0); box-shadow: none; background: #f1f5f9; } + + @media (max-width: 1024px) { .stats-grid { grid-template-columns: repeat(3, 1fr); } } + @media (max-width: 768px) { .db-header { flex-direction: column; align-items: flex-start; } .master-capsule-container { width: 100%; justify-content: flex-start; flex-wrap: wrap; } .stats-grid { grid-template-columns: 1fr; } } + .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); } + + .drilldown-bar { background: #ffffff; padding: 15px; border-radius: 14px; border: 1px solid #e2e8f0; margin-bottom: 20px; } + .filter-group { margin-bottom: 12px; } + .filter-group-label { font-size: 10px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 8px; } + .checkbox-row { display: flex; flex-wrap: wrap; gap: 15px; } + .check-item { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 600; color: #475569; cursor: pointer; } + `; + document.head.appendChild(style); + + // HTML Injection + page.main.html(` +
+
+
+

Audit Management

+
+
+
+
Status
+
Risk
+ + + +
Create Audit
+
+
+
+
Total Records
-
+
Draft
-
+
Pending
-
+
Closed
-
+ + +
+ + + +
+ `); + + // --- LOGIC --- + const $w = $(wrapper); + let userRole = ''; + let currentStatusFilter = []; + let currentRiskFilter = []; + let pendingStart = 0; + let recentStart = 0; + let currentItemStages = []; + let currentTimeFilter = []; + + const upd = (id, val) => { const $el = $w.find('#' + id); if ($el.length) $el.text(val ?? 0); }; + + const renderRows = (list) => { + return list.map(i => ` + + ${i.sr_no} + ${i.name.split('-').pop()} + ${i.emp_branch || '---'} + ${i.audit_query_subject_box || '---'} + ${i.emp_division || '---'} + ${i.status || '---'} + ${i.risk || 'Normal'} + ${i.aging || 0} + ${frappe.datetime.str_to_user(i.creation).split(' ')[0]}${frappe.datetime.comment_when(i.creation)} + `).join(''); + }; + + const refresh = () => { + pendingStart = 0; recentStart = 0; + frappe.call({ + method: 'audit_management.audit_management.dashboard.get_dashboard_stats', + args: { pending_start: 0, recent_start: 0, status: currentStatusFilter.join(','), risk: currentRiskFilter.join(','), item_stages: currentItemStages.join(','), time_filter: currentTimeFilter.join(',') }, + callback: function (r) { + if (!r.message || !r.message.success) return; + const d = r.message; userRole = d.role_type; + + const $sDD = $w.find('#filter-dropdown-status'); + const $rDD = $w.find('#filter-dropdown-risk'); + if ($sDD.is(':empty')) { + const sOps = userRole === 'stage_user' ? ['Pending', 'Responded', 'No Response'] : ['Draft', 'Pending', 'Closed']; + $sDD.html(sOps.map(o => `
${o}
`).join('')); + const rOps = ['High', 'Medium', 'Normal']; + $rDD.html(rOps.map(o => `
${o}
`).join('')); + } + + const is_stage = userRole === 'stage_user'; + $w.find('#draft-card, #total-card, #resp-card').toggle(!is_stage).css('display', !is_stage ? 'flex' : 'none'); + $w.find('#nr-card').show().css('display', 'flex'); + + if (is_stage) { + upd('val-pending', d.pending_for_me); $w.find('#lbl-pending').text('Pending Me'); + upd('val-nr', d.not_responded_count); + upd('val-closed', d.responded_by_me); $w.find('#lbl-closed').text('Responded'); + } else { + upd('val-draft', d.draft_count); upd('val-total', d.total_count); + upd('val-pending', d.total_pending); $w.find('#lbl-pending').text('Total Pending'); + upd('val-closed', d.closed_count); $w.find('#lbl-closed').text('Closed'); + upd('val-nr', d.not_responded_count); upd('val-resp', d.responded_by_me); + } + + $w.find('.compact-stat-card').removeClass('active-card'); + if (currentStatusFilter.length > 0) { + const s = currentStatusFilter[0]; + if (s === 'Draft') $w.find('#draft-card').addClass('active-card'); + else if (s === 'Pending') $w.find('#p-card').addClass('active-card'); + else if (s === 'Closed') $w.find('#c-card').addClass('active-card'); + else if (s === 'Responded') $w.find('#resp-card').addClass('active-card'); + else if (s === 'No Response') $w.find('#nr-card').addClass('active-card'); + } else if (currentRiskFilter.length === 0) { $w.find('#total-card').addClass('active-card'); } + + const showD = (currentStatusFilter.includes('Responded') || currentStatusFilter.includes('No Response')); + $w.find('#drilldown-section').toggle(showD); + if (showD) { + const $sL = $w.find('#stage-checkbox-list'); + if ($sL.is(':empty')) { + frappe.call({ method: 'frappe.client.get_list', args: { doctype: 'Audit Stage', fields: ['name'], order_by: 'name asc' }, callback: (res) => { + if (res.message) { + const counts = d.stage_counts || {}; + $sL.html(res.message.map(s => ``).join('')); + } + }}); + } + } + + $w.find('#stage-view').toggle(is_stage); $w.find('#manager-view').toggle(!is_stage); + const items = is_stage ? d.pending_list : d.recent_list; + const $b = $w.find(is_stage ? '#stage-items' : '#activity-body'); + const $m = $w.find(is_stage ? '#pending-more-btn' : '#recent-more-btn'); + const hasM = is_stage ? d.has_more_pending : d.has_more_recent; + if (items && items.length > 0) { $b.html(renderRows(items)); $m.toggle(!!hasM).css('display', hasM ? 'flex' : 'none'); } + else { $b.html('No records found'); $m.hide(); } + } + }); + }; + + const load_more = (type) => { + const is_p = type === 'pending'; + frappe.call({ + method: 'audit_management.audit_management.dashboard.get_dashboard_stats', + args: { pending_start: is_p ? pendingStart + 10 : pendingStart, recent_start: !is_p ? recentStart + 10 : recentStart, status: currentStatusFilter.join(','), risk: currentRiskFilter.join(','), item_stages: currentItemStages.join(','), time_filter: currentTimeFilter.join(',') }, + callback: function (r) { + if (!r.message || !r.message.success) return; + const d = r.message; + if (is_p) { pendingStart += 10; $w.find('#stage-items').append(renderRows(d.pending_list)); $w.find('#pending-more-btn').toggle(!!d.has_more_pending).css('display', d.has_more_pending ? 'flex' : 'none'); } + else { recentStart += 10; $w.find('#activity-body').append(renderRows(d.recent_list)); $w.find('#recent-more-btn').toggle(!!d.has_more_recent).css('display', d.has_more_recent ? 'flex' : 'none'); } + } + }); + }; + + // --- BIND EVENTS --- + $w.on('click', '#status-filter-btn', (e) => { e.stopPropagation(); $w.find('.multiselect-list').not('#filter-dropdown-status').hide(); $w.find('#filter-dropdown-status').toggle(); }); + $w.on('click', '#risk-filter-btn', (e) => { e.stopPropagation(); $w.find('.multiselect-list').not('#filter-dropdown-risk').hide(); $w.find('#filter-dropdown-risk').toggle(); }); + $w.on('click', '#actions-btn', (e) => { e.stopPropagation(); $w.find('#actions-dropdown').toggle(); }); + $w.on('click', '#clear-filter-btn', () => { $w.find('input[type=checkbox]').prop('checked', false); currentStatusFilter = []; currentRiskFilter = []; currentItemStages = []; currentTimeFilter = []; $w.find('#selected-status-label').text('Status'); $w.find('#selected-risk-label').text('Risk'); $w.find('#clear-filter-btn').hide(); refresh(); }); + + $w.on('change', '.status-checkbox', function() { + currentStatusFilter = $w.find('.status-checkbox:checked').map((i, el) => $(el).val()).get(); + $w.find('#selected-status-label').text(currentStatusFilter.length === 0 ? 'Status' : (currentStatusFilter.length === 1 ? currentStatusFilter[0] : currentStatusFilter.length + ' Selected')); + $w.find('#clear-filter-btn').show(); refresh(); + }); + + $w.on('change', '.risk-checkbox', function() { + currentRiskFilter = $w.find('.risk-checkbox:checked').map((i, el) => $(el).val()).get(); + $w.find('#selected-risk-label').text(currentRiskFilter.length === 0 ? 'Risk' : (currentRiskFilter.length === 1 ? currentRiskFilter[0] : currentRiskFilter.length + ' Selected')); + $w.find('#clear-filter-btn').show(); refresh(); + }); + + $w.on('change', '.time-checkbox', () => { currentTimeFilter = $w.find('.time-checkbox:checked').map((i, el) => $(el).val()).get(); refresh(); }); + $w.on('change', '.stage-item-checkbox', () => { currentItemStages = $w.find('.stage-item-checkbox:checked').map((i, el) => $(el).val()).get(); refresh(); }); + + $w.on('click', '#total-card', () => { currentStatusFilter = []; currentRiskFilter = []; refresh(); }); + $w.on('click', '#draft-card', () => { currentStatusFilter = ['Draft']; refresh(); }); + $w.on('click', '#p-card', () => { currentStatusFilter = ['Pending']; refresh(); }); + $w.on('click', '#c-card', () => { currentStatusFilter = ['Closed']; refresh(); }); + $w.on('click', '#resp-card', () => { currentStatusFilter = ['Responded']; refresh(); }); + $w.on('click', '#nr-card', () => { currentStatusFilter = ['No Response']; refresh(); }); + + $w.on('click', '#load-more-p-btn', () => load_more('pending')); + $w.on('click', '#load-more-r-btn', () => load_more('recent')); + + $w.on('click', '#btn-audit-levels', () => frappe.set_route('List', 'Audit Level')); + $w.on('click', '#btn-query-types', () => frappe.set_route('List', 'Audit Query Type')); + $w.on('click', '#btn-settings', () => frappe.set_route('Form', 'Audit Management Settings')); + $w.on('click', '#btn-create-audit', () => frappe.new_doc('My Audits')); + $w.on('click', '#btn-show-reports', () => { + const reports = ['My Audits Report', 'Pending Audit Queries Aging Report', 'Process Technical Improvement Commitment Report', 'Recurring Operational Reports']; + let html = `
${reports.map(r => ``).join('')}
`; + new frappe.ui.Dialog({ title: 'Select Report', fields: [{ fieldtype: 'HTML', fieldname: 'reports_html', options: html }] }).show(); + }); + + $(document).on('click', () => { $w.find('.custom-dropdown, .multiselect-list').hide(); }); + + refresh(); +}; diff --git a/audit_management/audit_management/page/audit_management_dashboard/audit_management_dashboard.json b/audit_management/audit_management/page/audit_management_dashboard/audit_management_dashboard.json new file mode 100644 index 0000000..0a12748 --- /dev/null +++ b/audit_management/audit_management/page/audit_management_dashboard/audit_management_dashboard.json @@ -0,0 +1,26 @@ +{ + "content": null, + "creation": "2026-05-25 12:00:00.000000", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2026-05-25 12:00:00.000000", + "modified_by": "Administrator", + "module": "Audit Management", + "name": "audit_management_dashboard", + "owner": "Administrator", + "page_name": "Audit Management Dashboard", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Audit Manager" + }, + { + "role": "Audit Member" + } + ], + "standard": "Yes", + "title": "Audit Management Dashboard" +} From 5e86cc81b0381b6bcb89de2e5226ca6cdad2e5d8 Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Mon, 25 May 2026 12:50:13 +0530 Subject: [PATCH 2/4] feat: implement Gmail-style UI for audit management dashboard --- .../audit_management_dashboard.js | 380 ++++++++++++------ 1 file changed, 255 insertions(+), 125 deletions(-) diff --git a/audit_management/audit_management/page/audit_management_dashboard/audit_management_dashboard.js b/audit_management/audit_management/page/audit_management_dashboard/audit_management_dashboard.js index ab2d670..d8b292f 100644 --- a/audit_management/audit_management/page/audit_management_dashboard/audit_management_dashboard.js +++ b/audit_management/audit_management/page/audit_management_dashboard/audit_management_dashboard.js @@ -1,112 +1,194 @@ frappe.pages['audit_management_dashboard'].on_page_load = function(wrapper) { var page = frappe.ui.make_app_page({ parent: wrapper, - title: 'Audit Management Dashboard', + title: 'Audit Management', single_column: true }); // CSS Injection const style = document.createElement('style'); style.innerHTML = ` - .audit-dashboard-light { background: #f1f5f9; color: #1e293b; padding: 20px; border-radius: 16px; font-family: 'Inter', sans-serif; border: 1px solid #e2e8f0; } - .db-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; flex-wrap: nowrap; gap: 15px; } - .title-wrap { display: flex; align-items: center; gap: 10px; flex-shrink: 0; } - .db-title { font-size: 20px; font-weight: 800; color: #0f172a; margin: 0; white-space: nowrap; } - .live-dot { width: 8px; height: 8px; background: #22c55e; border-radius: 50%; box-shadow: 0 0 10px rgba(34, 197, 94, 0.4); } + .audit-dashboard-gmail { display: flex; gap: 20px; background: #f6f8fc; min-height: 100vh; padding: 10px; font-family: 'Inter', sans-serif; } + + /* Sidebar Styling */ + .db-sidebar { width: 260px; display: flex; flex-direction: column; gap: 4px; flex-shrink: 0; } + .btn-compose-wrap { padding: 8px 0 16px 0; } + .btn-create-audit { background: #c2e7ff; color: #001d35; padding: 16px 24px; border-radius: 16px; display: flex; align-items: center; gap: 12px; font-weight: 600; cursor: pointer; border: none; transition: 0.2s; font-size: 14px; width: fit-content; } + .btn-create-audit:hover { box-shadow: 0 1px 3px 0 rgba(60,64,67,0.3), 0 4px 8px 3px rgba(60,64,67,0.15); background: #d3e3fd; } + .btn-create-audit i { font-size: 18px; } - .master-capsule-container { display: flex; gap: 6px; flex-wrap: nowrap; align-items: center; justify-content: flex-end; flex-grow: 1; } - .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; } - .master-capsule:hover { border-color: #3b82f6; transform: translateY(-1px); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); } - .master-capsule i { font-size: 13px; flex-shrink: 0; } - .master-capsule span { font-size: 10px; font-weight: 700; color: #475569; white-space: nowrap; } - .label-text { min-width: 40px; text-align: center; } + .nav-item { display: flex; align-items: center; padding: 0 16px 0 24px; height: 36px; border-radius: 0 20px 20px 0; cursor: pointer; color: #444746; font-size: 14px; transition: 0.1s; position: relative; margin-right: 12px; } + .nav-item:hover { background-color: #eaebef; } + .nav-item.active { background-color: #d3e3fd; color: #001d35; font-weight: 700; } + .nav-item i { width: 20px; margin-right: 18px; font-size: 16px; text-align: center; opacity: 0.8; } + .nav-count { margin-left: auto; font-size: 12px; opacity: 0.8; } + .nav-item.active .nav-count { opacity: 1; } - .create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; } - .create-btn-capsule i, .create-btn-capsule span { color: white !important; } - .create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.02); } + /* Main Content Area */ + .db-main-content { flex-grow: 1; background: #ffffff; border-radius: 16px; display: flex; flex-direction: column; overflow: hidden; border: 1px solid #e2e8f0; box-shadow: 0 1px 2px rgba(0,0,0,0.05); } + .db-inner-header { padding: 12px 16px; border-bottom: 1px solid #f1f3f4; display: flex; align-items: center; justify-content: space-between; background: #fff; position: sticky; top: 0; z-index: 10; } + .filter-section { display: flex; gap: 8px; align-items: center; } - .dropdown-wrapper { position: relative; } - .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; } - .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; } - .dropdown-item:hover { background: #f1f5f9; color: #1e293b; } - - .grid-compact { display: grid; gap: 10px; margin-bottom: 24px; } - .stats-grid { grid-template-columns: repeat(6, 1fr); } - - .blue-txt { color: #2563eb; } .purple-txt { color: #7c3aed; } .orange-txt { color: #ea580c; } .green-txt { color: #16a34a; } .red-txt { color: #dc2626; } - - .compact-stat-card { background: #ffffff; padding: 10px 12px; border-radius: 10px; 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: row; align-items: center; gap: 8px; min-width: 140px; flex-wrap: nowrap; } - .stat-label-small { font-size: 10px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.4px; white-space: nowrap; flex-shrink: 0; } - .stat-val-small { font-size: 18px; font-weight: 800; color: #0f172a; margin-left: auto; flex-shrink: 0; } - .icon-stat { font-size: 13px; opacity: 0.9; flex-shrink: 0; } - .accent-bar { position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; } - .blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; } .orange-bg { background: #f59e0b; } .red-bg { background: #ef4444; } - - .multiselect-container { position: relative; } - .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; } - .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; } + .master-capsule { background: #ffffff; padding: 4px 12px; border-radius: 50px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 6px; cursor: pointer; transition: 0.2s; height: 32px; box-sizing: border-box; font-size: 12px; font-weight: 600; color: #444746; } + .master-capsule:hover { border-color: #0b57d0; background: #f8f9fa; } + .blue-txt { color: #0b57d0; } .purple-txt { color: #7c3aed; } .orange-txt { color: #ea580c; } .green-txt { color: #16a34a; } .red-txt { color: #d93025; } + + .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: 180px; padding: 8px 0; display: none; } + .multiselect-item { padding: 8px 16px; display: flex; align-items: center; gap: 10px; cursor: pointer; transition: 0.2s; font-size: 13px; color: #444746; } .multiselect-item:hover { background: #f1f5f9; } - .multiselect-item input[type='checkbox'] { cursor: pointer; width: 14px; height: 14px; } + .multiselect-item input { cursor: pointer; } - .mini-table { width: 100%; border-collapse: collapse; font-size: 12px; } - .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; } - .mini-table td { padding: 14px 16px; color: #334155; border-bottom: 1px solid #f1f5f9; } - .mini-table tr:hover { background: #f9fafb; cursor: pointer; } - .t-id { font-weight: 800; color: #2563eb; } - .t-status { background: #f1f5f9; padding: 4px 8px; border-radius: 6px; color: #475569; font-weight: 700; font-size: 10px; } - .t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; } - .t-status.closed { background: #b9f9cf; color: #001a00; border: 1px solid #bbf7d0; } - .t-risk { font-weight: 800; text-transform: uppercase; font-size: 10px; } - .t-risk.high { background: #dc2626; color: white; padding: 4px 8px; border-radius: 6px; } .t-risk.medium { color: #d97706; } .t-risk.normal { color: #2563eb; } + /* Table Styling */ + .list-container { padding: 0; overflow-y: auto; flex-grow: 1; } + .list-header-text { padding: 16px 20px; font-size: 15px; font-weight: 700; color: #1f1f1f; border-bottom: 1px solid #f1f3f4; background: #fff; } + + .mini-table { width: 100%; border-collapse: collapse; font-size: 13px; } + .mini-table th { text-align: left; padding: 12px 16px; border-bottom: 1px solid #f1f3f4; color: #444746; font-weight: 600; background: #fafafa; white-space: nowrap; } + .mini-table td { padding: 12px 16px; border-bottom: 1px solid #f1f3f4; color: #1f1f1f; vertical-align: middle; } + .mini-table tr:hover { background: #f2f6fc; box-shadow: inset 1px 0 0 #0b57d0; cursor: pointer; } + + .t-id { font-weight: 700; color: #0b57d0; } + .t-status { padding: 3px 10px; border-radius: 6px; font-size: 11px; font-weight: 700; text-transform: uppercase; } + .t-status.pending { background: #fef2f2; color: #b91c1c; border: 1px solid #fecaca; } + .t-status.closed { background: #f0fdf4; color: #15803d; border: 1px solid #bbf7d0; } + + .load-more-btn { background: #fff; border: 1px solid #dadce0; color: #0b57d0; padding: 10px 24px; border-radius: 20px; cursor: pointer; font-size: 13px; font-weight: 600; margin: 24px auto; display: block; transition: 0.2s; } + .load-more-btn:hover { background: #f8f9fa; border-color: #0b57d0; } - .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; } - .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); } - .load-more-btn:active { transform: translateY(0); box-shadow: none; background: #f1f5f9; } + .drilldown-bar { background: #f8f9fa; padding: 16px 24px; border-bottom: 1px solid #f1f3f4; } + .filter-group-label { font-size: 11px; font-weight: 700; color: #5f6368; text-transform: uppercase; margin-bottom: 10px; letter-spacing: 0.5px; } + .checkbox-row { display: flex; flex-wrap: wrap; gap: 16px; } + .check-item { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #3c4043; cursor: pointer; font-weight: 500; } - @media (max-width: 1024px) { .stats-grid { grid-template-columns: repeat(3, 1fr); } } - @media (max-width: 768px) { .db-header { flex-direction: column; align-items: flex-start; } .master-capsule-container { width: 100%; justify-content: flex-start; flex-wrap: wrap; } .stats-grid { grid-template-columns: 1fr; } } - .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); } - - .drilldown-bar { background: #ffffff; padding: 15px; border-radius: 14px; border: 1px solid #e2e8f0; margin-bottom: 20px; } - .filter-group { margin-bottom: 12px; } - .filter-group-label { font-size: 10px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 8px; } - .checkbox-row { display: flex; flex-wrap: wrap; gap: 15px; } - .check-item { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 600; color: #475569; cursor: pointer; } + @media (max-width: 992px) { + .audit-dashboard-gmail { flex-direction: column; } + .db-sidebar { width: 100%; } + .nav-item { border-radius: 20px; margin-right: 0; } + } `; document.head.appendChild(style); - // HTML Injection + // HTML Structure page.main.html(` -
-
-
-

Audit Management

-
+
+
+
+ +
+ + -
-
Status
-
Risk
- - - -
Create Audit
+ + + + + + +
Actions
+ + + +
-
-
Total Records
-
-
Draft
-
-
Pending
-
-
Closed
-
- - + +
+
+
+
+
+ + Status + +
+
+
+
+
+ + Risk + +
+
+
+ +
+ +
+ +
+
+
+ + + +
+ + + +
- - -
`); - // --- LOGIC --- + // --- LOGIC SECTION --- const $w = $(wrapper); let userRole = ''; let currentStatusFilter = []; @@ -116,7 +198,10 @@ frappe.pages['audit_management_dashboard'].on_page_load = function(wrapper) { let currentItemStages = []; let currentTimeFilter = []; - const upd = (id, val) => { const $el = $w.find('#' + id); if ($el.length) $el.text(val ?? 0); }; + const upd = (id, val) => { + const $el = $w.find('#' + id); + if ($el.length) $el.text(val ?? 0); + }; const renderRows = (list) => { return list.map(i => ` @@ -127,9 +212,9 @@ frappe.pages['audit_management_dashboard'].on_page_load = function(wrapper) { ${i.audit_query_subject_box || '---'} ${i.emp_division || '---'} ${i.status || '---'} - ${i.risk || 'Normal'} + ${i.risk || 'Normal'} ${i.aging || 0} - ${frappe.datetime.str_to_user(i.creation).split(' ')[0]}${frappe.datetime.comment_when(i.creation)} + ${frappe.datetime.comment_when(i.creation)} `).join(''); }; @@ -137,66 +222,88 @@ frappe.pages['audit_management_dashboard'].on_page_load = function(wrapper) { pendingStart = 0; recentStart = 0; frappe.call({ method: 'audit_management.audit_management.dashboard.get_dashboard_stats', - args: { pending_start: 0, recent_start: 0, status: currentStatusFilter.join(','), risk: currentRiskFilter.join(','), item_stages: currentItemStages.join(','), time_filter: currentTimeFilter.join(',') }, + args: { + pending_start: 0, recent_start: 0, + status: currentStatusFilter.join(','), + risk: currentRiskFilter.join(','), + item_stages: currentItemStages.join(','), + time_filter: currentTimeFilter.join(',') + }, callback: function (r) { if (!r.message || !r.message.success) return; const d = r.message; userRole = d.role_type; + // Setup Filters const $sDD = $w.find('#filter-dropdown-status'); const $rDD = $w.find('#filter-dropdown-risk'); if ($sDD.is(':empty')) { const sOps = userRole === 'stage_user' ? ['Pending', 'Responded', 'No Response'] : ['Draft', 'Pending', 'Closed']; $sDD.html(sOps.map(o => `
${o}
`).join('')); - const rOps = ['High', 'Medium', 'Normal']; - $rDD.html(rOps.map(o => `
${o}
`).join('')); + $rDD.html(['High', 'Medium', 'Normal'].map(o => `
${o}
`).join('')); } + // Sync Stats & Sidebar UI const is_stage = userRole === 'stage_user'; - $w.find('#draft-card, #total-card, #resp-card').toggle(!is_stage).css('display', !is_stage ? 'flex' : 'none'); - $w.find('#nr-card').show().css('display', 'flex'); + $w.find('#nav-draft, #nav-total, #nav-responded').toggle(!is_stage); + $w.find('#nav-nr').show(); if (is_stage) { - upd('val-pending', d.pending_for_me); $w.find('#lbl-pending').text('Pending Me'); + upd('val-pending', d.pending_for_me); $w.find('#nav-pending span:nth-child(2)').text('Pending Me'); upd('val-nr', d.not_responded_count); - upd('val-closed', d.responded_by_me); $w.find('#lbl-closed').text('Responded'); + upd('val-closed', d.responded_by_me); $w.find('#nav-closed span:nth-child(2)').text('Responded'); } else { upd('val-draft', d.draft_count); upd('val-total', d.total_count); - upd('val-pending', d.total_pending); $w.find('#lbl-pending').text('Total Pending'); - upd('val-closed', d.closed_count); $w.find('#lbl-closed').text('Closed'); + upd('val-pending', d.total_pending); $w.find('#nav-pending span:nth-child(2)').text('Pending'); + upd('val-closed', d.closed_count); $w.find('#nav-closed span:nth-child(2)').text('Closed'); upd('val-nr', d.not_responded_count); upd('val-resp', d.responded_by_me); } - $w.find('.compact-stat-card').removeClass('active-card'); + // Active Nav Styling + $w.find('.nav-item').removeClass('active'); if (currentStatusFilter.length > 0) { const s = currentStatusFilter[0]; - if (s === 'Draft') $w.find('#draft-card').addClass('active-card'); - else if (s === 'Pending') $w.find('#p-card').addClass('active-card'); - else if (s === 'Closed') $w.find('#c-card').addClass('active-card'); - else if (s === 'Responded') $w.find('#resp-card').addClass('active-card'); - else if (s === 'No Response') $w.find('#nr-card').addClass('active-card'); - } else if (currentRiskFilter.length === 0) { $w.find('#total-card').addClass('active-card'); } + if (s === 'Draft') $w.find('#nav-draft').addClass('active'); + else if (s === 'Pending') $w.find('#nav-pending').addClass('active'); + else if (s === 'Closed') $w.find('#nav-closed').addClass('active'); + else if (s === 'Responded') $w.find('#nav-responded').addClass('active'); + else if (s === 'No Response') $w.find('#nav-nr').addClass('active'); + } else if (currentRiskFilter.length === 0) { + $w.find('#nav-total').addClass('active'); + } + // Drilldown handling const showD = (currentStatusFilter.includes('Responded') || currentStatusFilter.includes('No Response')); $w.find('#drilldown-section').toggle(showD); if (showD) { const $sL = $w.find('#stage-checkbox-list'); if ($sL.is(':empty')) { - frappe.call({ method: 'frappe.client.get_list', args: { doctype: 'Audit Stage', fields: ['name'], order_by: 'name asc' }, callback: (res) => { - if (res.message) { - const counts = d.stage_counts || {}; - $sL.html(res.message.map(s => ``).join('')); + frappe.call({ + method: 'frappe.client.get_list', + args: { doctype: 'Audit Stage', fields: ['name'], order_by: 'name asc' }, + callback: (res) => { + if (res.message) { + const counts = d.stage_counts || {}; + $sL.html(res.message.map(s => ``).join('')); + } } - }}); + }); } } + // List Rendering $w.find('#stage-view').toggle(is_stage); $w.find('#manager-view').toggle(!is_stage); const items = is_stage ? d.pending_list : d.recent_list; - const $b = $w.find(is_stage ? '#stage-items' : '#activity-body'); - const $m = $w.find(is_stage ? '#pending-more-btn' : '#recent-more-btn'); - const hasM = is_stage ? d.has_more_pending : d.has_more_recent; - if (items && items.length > 0) { $b.html(renderRows(items)); $m.toggle(!!hasM).css('display', hasM ? 'flex' : 'none'); } - else { $b.html('No records found'); $m.hide(); } + const $body = $w.find(is_stage ? '#stage-items' : '#activity-body'); + const $btn = $w.find(is_stage ? '#load-more-p-btn' : '#load-more-r-btn'); + const hasMore = is_stage ? d.has_more_pending : d.has_more_recent; + + if (items && items.length > 0) { + $body.html(renderRows(items)); + $btn.toggle(!!hasMore); + } else { + $body.html('No records found'); + $btn.hide(); + } } }); }; @@ -205,43 +312,65 @@ frappe.pages['audit_management_dashboard'].on_page_load = function(wrapper) { const is_p = type === 'pending'; frappe.call({ method: 'audit_management.audit_management.dashboard.get_dashboard_stats', - args: { pending_start: is_p ? pendingStart + 10 : pendingStart, recent_start: !is_p ? recentStart + 10 : recentStart, status: currentStatusFilter.join(','), risk: currentRiskFilter.join(','), item_stages: currentItemStages.join(','), time_filter: currentTimeFilter.join(',') }, + args: { + pending_start: is_p ? pendingStart + 10 : pendingStart, + recent_start: !is_p ? recentStart + 10 : recentStart, + status: currentStatusFilter.join(','), + risk: currentRiskFilter.join(','), + item_stages: currentItemStages.join(','), + time_filter: currentTimeFilter.join(',') + }, callback: function (r) { if (!r.message || !r.message.success) return; const d = r.message; - if (is_p) { pendingStart += 10; $w.find('#stage-items').append(renderRows(d.pending_list)); $w.find('#pending-more-btn').toggle(!!d.has_more_pending).css('display', d.has_more_pending ? 'flex' : 'none'); } - else { recentStart += 10; $w.find('#activity-body').append(renderRows(d.recent_list)); $w.find('#recent-more-btn').toggle(!!d.has_more_recent).css('display', d.has_more_recent ? 'flex' : 'none'); } + if (is_p) { + pendingStart += 10; + $w.find('#stage-items').append(renderRows(d.pending_list)); + $w.find('#load-more-p-btn').toggle(!!d.has_more_pending); + } else { + recentStart += 10; + $w.find('#activity-body').append(renderRows(d.recent_list)); + $w.find('#load-more-r-btn').toggle(!!d.has_more_recent); + } } }); }; - // --- BIND EVENTS --- + // --- EVENT BINDING --- $w.on('click', '#status-filter-btn', (e) => { e.stopPropagation(); $w.find('.multiselect-list').not('#filter-dropdown-status').hide(); $w.find('#filter-dropdown-status').toggle(); }); $w.on('click', '#risk-filter-btn', (e) => { e.stopPropagation(); $w.find('.multiselect-list').not('#filter-dropdown-risk').hide(); $w.find('#filter-dropdown-risk').toggle(); }); $w.on('click', '#actions-btn', (e) => { e.stopPropagation(); $w.find('#actions-dropdown').toggle(); }); - $w.on('click', '#clear-filter-btn', () => { $w.find('input[type=checkbox]').prop('checked', false); currentStatusFilter = []; currentRiskFilter = []; currentItemStages = []; currentTimeFilter = []; $w.find('#selected-status-label').text('Status'); $w.find('#selected-risk-label').text('Risk'); $w.find('#clear-filter-btn').hide(); refresh(); }); + $w.on('click', '#clear-filter-btn', () => { + $w.find('input[type=checkbox]').prop('checked', false); + currentStatusFilter = []; currentRiskFilter = []; currentItemStages = []; currentTimeFilter = []; + $w.find('#selected-status-label').text('Status'); $w.find('#selected-risk-label').text('Risk'); + $w.find('#clear-filter-btn').hide(); refresh(); + }); $w.on('change', '.status-checkbox', function() { currentStatusFilter = $w.find('.status-checkbox:checked').map((i, el) => $(el).val()).get(); $w.find('#selected-status-label').text(currentStatusFilter.length === 0 ? 'Status' : (currentStatusFilter.length === 1 ? currentStatusFilter[0] : currentStatusFilter.length + ' Selected')); - $w.find('#clear-filter-btn').show(); refresh(); + $w.find('#clear-filter-btn').toggle(currentStatusFilter.length > 0 || currentRiskFilter.length > 0); + refresh(); }); $w.on('change', '.risk-checkbox', function() { currentRiskFilter = $w.find('.risk-checkbox:checked').map((i, el) => $(el).val()).get(); $w.find('#selected-risk-label').text(currentRiskFilter.length === 0 ? 'Risk' : (currentRiskFilter.length === 1 ? currentRiskFilter[0] : currentRiskFilter.length + ' Selected')); - $w.find('#clear-filter-btn').show(); refresh(); + $w.find('#clear-filter-btn').toggle(currentStatusFilter.length > 0 || currentRiskFilter.length > 0); + refresh(); }); $w.on('change', '.time-checkbox', () => { currentTimeFilter = $w.find('.time-checkbox:checked').map((i, el) => $(el).val()).get(); refresh(); }); $w.on('change', '.stage-item-checkbox', () => { currentItemStages = $w.find('.stage-item-checkbox:checked').map((i, el) => $(el).val()).get(); refresh(); }); - $w.on('click', '#total-card', () => { currentStatusFilter = []; currentRiskFilter = []; refresh(); }); - $w.on('click', '#draft-card', () => { currentStatusFilter = ['Draft']; refresh(); }); - $w.on('click', '#p-card', () => { currentStatusFilter = ['Pending']; refresh(); }); - $w.on('click', '#c-card', () => { currentStatusFilter = ['Closed']; refresh(); }); - $w.on('click', '#resp-card', () => { currentStatusFilter = ['Responded']; refresh(); }); - $w.on('click', '#nr-card', () => { currentStatusFilter = ['No Response']; refresh(); }); + // Sidebar Nav Clicks + $w.on('click', '#nav-total', () => { currentStatusFilter = []; currentRiskFilter = []; refresh(); }); + $w.on('click', '#nav-draft', () => { currentStatusFilter = ['Draft']; refresh(); }); + $w.on('click', '#nav-pending', () => { currentStatusFilter = ['Pending']; refresh(); }); + $w.on('click', '#nav-closed', () => { currentStatusFilter = ['Closed']; refresh(); }); + $w.on('click', '#nav-responded', () => { currentStatusFilter = ['Responded']; refresh(); }); + $w.on('click', '#nav-nr', () => { currentStatusFilter = ['No Response']; refresh(); }); $w.on('click', '#load-more-p-btn', () => load_more('pending')); $w.on('click', '#load-more-r-btn', () => load_more('recent')); @@ -256,7 +385,8 @@ frappe.pages['audit_management_dashboard'].on_page_load = function(wrapper) { new frappe.ui.Dialog({ title: 'Select Report', fields: [{ fieldtype: 'HTML', fieldname: 'reports_html', options: html }] }).show(); }); - $(document).on('click', () => { $w.find('.custom-dropdown, .multiselect-list').hide(); }); + $(document).on('click', () => { $w.find('.multiselect-list').hide(); }); + // Initial Refresh refresh(); }; From c389b1459cf5b24cb7c1a281fefb3a4b04f34fb6 Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Mon, 25 May 2026 15:00:53 +0530 Subject: [PATCH 3/4] feat(dashboard): implement web dashboard with fixed icons and header reports --- .../www/audit_management_dashboard.html | 568 ++++++++++++++++++ 1 file changed, 568 insertions(+) create mode 100644 audit_management/www/audit_management_dashboard.html diff --git a/audit_management/www/audit_management_dashboard.html b/audit_management/www/audit_management_dashboard.html new file mode 100644 index 0000000..10f0ce9 --- /dev/null +++ b/audit_management/www/audit_management_dashboard.html @@ -0,0 +1,568 @@ +{% extends "templates/web.html" %} + +{% block title %}Audit Management Dashboard{% endblock %} + +{% block header %} +{% endblock %} + +{% block style %} + +{% endblock %} + +{% block content %} + + + +
+ + + + +
+
+
+ +
+
+ + Status + +
+
+
+
+
+ + Risk + +
+
+
+ +
+ +
+ +
+ Reports +
+
+
+
+ + + +
+ + + +
+
+
+{% endblock %} + +{% block script %} + +{% endblock %} \ No newline at end of file From 99380d452ff3378a91cc85c1f288c315ed1ea03c Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Tue, 26 May 2026 12:06:49 +0530 Subject: [PATCH 4/4] feat(dashboard): redirect cards to My Audits list view --- 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 4cae36b..e11a929 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\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 root.querySelectorAll('.stage-item-checkbox, .time-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 refresh();\n };\n\n window.clear_filters = () => {\n root.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = false);\n currentStatusFilter = []; currentRiskFilter = []; currentItemStages = []; currentTimeFilter = [];\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 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 },\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 },\n callback: function (r) {\n if (!r.message || !r.message.success) return;\n const d = r.message; userRole = d.role_type;\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\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 const stageList = root.querySelector('#stage-checkbox-list');\n if (showDrill && 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 const tc = d.time_counts || {}; \n root.querySelector('#count-today').innerText = '(' + (tc.Today || 0) + ')'; \n root.querySelector('#count-yest').innerText = '(' + (tc.Yesterday || 0) + ')'; \n root.querySelector('#count-week').innerText = '(' + (tc['Last Week'] || 0) + ')'; \n const upAll = root.querySelector('#count-all'); \n if (upAll) upAll.innerText = '(' + (tc['All Time'] || 0) + ')'; \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 }\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 if (userRole === 'stage_user') return;\n currentStatusFilter = []; currentRiskFilter = []; resetChildFilters();\n root.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = false);\n root.querySelector('#selected-status-label').innerText = 'Status';\n root.querySelector('#selected-risk-label').innerText = 'Risk';\n refresh();\n };\n\n window.handle_draft_click = () => {\n if (userRole === 'stage_user') return;\n currentStatusFilter = ['Draft']; resetChildFilters();\n refresh();\n };\n\n window.handle_responded_click = () => {\n currentStatusFilter = ['Responded']; currentItemStages = []; currentTimeFilter = ['Today'];\n root.querySelectorAll('.time-checkbox').forEach(cb => cb.checked = (cb.value === 'Today'));\n refresh();\n };\n\n window.handle_not_responded_click = () => {\n currentStatusFilter = ['No Response']; currentItemStages = []; currentTimeFilter = ['Today'];\n root.querySelectorAll('.time-checkbox').forEach(cb => cb.checked = (cb.value === 'Today'));\n refresh();\n };\n\n window.handle_pending_click = () => { \n currentStatusFilter = ['Pending']; currentItemStages = []; currentTimeFilter = ['All Time'];\n root.querySelectorAll('.time-checkbox').forEach(cb => cb.checked = (cb.value === 'All Time'));\n refresh();\n };\n\n window.handle_closed_click = () => { \n currentStatusFilter = ['Closed']; resetChildFilters();\n refresh();\n };\n\n window.handle_item_stage_change = () => {\n currentItemStages = Array.from(root.querySelectorAll('.stage-item-checkbox:checked')).map(cb => cb.value);\n refresh();\n };\n\n window.handle_time_filter_change = () => {\n currentTimeFilter = Array.from(root.querySelectorAll('.time-checkbox:checked')).map(cb => cb.value);\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 currentStatusFilter = [];\n let currentRiskFilter = [];\n let pendingStart = 0;\n let recentStart = 0;\n let currentItemStages = [];\n let currentTimeFilter = [];\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 root.querySelectorAll('.stage-item-checkbox, .time-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 refresh();\n };\n\n window.clear_filters = () => {\n root.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = false);\n currentStatusFilter = []; currentRiskFilter = []; currentItemStages = []; currentTimeFilter = [];\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 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 },\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 },\n callback: function (r) {\n if (!r.message || !r.message.success) return;\n const d = r.message; userRole = d.role_type;\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\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 const stageList = root.querySelector('#stage-checkbox-list');\n if (showDrill && 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 const tc = d.time_counts || {}; \n root.querySelector('#count-today').innerText = '(' + (tc.Today || 0) + ')'; \n root.querySelector('#count-yest').innerText = '(' + (tc.Yesterday || 0) + ')'; \n root.querySelector('#count-week').innerText = '(' + (tc['Last Week'] || 0) + ')'; \n const upAll = root.querySelector('#count-all'); \n if (upAll) upAll.innerText = '(' + (tc['All Time'] || 0) + ')'; \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 }\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'];\n root.querySelectorAll('.time-checkbox').forEach(cb => cb.checked = (cb.value === 'Today'));\n refresh();\n };\n\n window.handle_not_responded_click = () => {\n currentStatusFilter = ['No Response']; currentItemStages = []; currentTimeFilter = ['Today'];\n root.querySelectorAll('.time-checkbox').forEach(cb => cb.checked = (cb.value === 'Today'));\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 refresh();\n };\n\n window.handle_time_filter_change = () => {\n currentTimeFilter = Array.from(root.querySelectorAll('.time-checkbox:checked')).map(cb => cb.value);\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