From c5b047ef65a1dc3995768e8a8c2bf29a89886df1 Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Wed, 20 May 2026 14:08:03 +0530 Subject: [PATCH 1/7] feat: remove time from creation date in audit dashboard --- 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 c5fd066..ed58513 100644 --- a/audit_management/fixtures/custom_html_block.json +++ b/audit_management/fixtures/custom_html_block.json @@ -7,6 +7,6 @@ "name": "Audit Management", "private": 0, "roles": [], - "script": "(function () {\n const root = root_element || document;\n let userRole = '';\n let pendingStart = 0;\n let recentStart = 0;\n let currentStatusFilter = '';\n\n const upd = (id, val) => { \n const el = root.querySelector(`#${id}`); \n if (el) el.innerText = val ?? 0; \n };\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name}\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)}\n `).join('');\n\n window.load_more_data = (type) => {\n const args = {\n pending_start: type === 'pending' ? pendingStart + 10 : pendingStart,\n recent_start: type === 'recent' ? recentStart + 10 : recentStart,\n status: currentStatusFilter\n };\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: args,\n callback: function (r) {\n if (!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 window.handle_status_filter = (val) => {\n currentStatusFilter = val;\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (val) clearBtn.style.display = 'flex';\n else clearBtn.style.display = 'none';\n refresh(val);\n };\n\n window.clear_filters = () => {\n const select = root.querySelector('#status-filter');\n if (select) select.value = '';\n currentStatusFilter = '';\n root.querySelector('#clear-filter-btn').style.display = 'none';\n refresh('');\n };\n\n function refresh(status = '') {\n pendingStart = 0; recentStart = 0;\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: { pending_start: 0, recent_start: 0, status: status },\n callback: function (r) {\n if (!r.message?.success) return;\n const d = r.message; \n \n userRole = d.role_type;\n\n // Visibility Control\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n const statusFilter = root.querySelector('#status-filter');\n \n if (userRole === 'stage_user') {\n if (masterCapsules) masterCapsules.style.display = 'none';\n if (draftCard) draftCard.style.display = 'none';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n } else {\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (draftCard) draftCard.style.display = 'block';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n }\n\n\n if (userRole === 'stage_user') {\n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n } else {\n upd('val-draft', d.draft_count);\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n }\n\n const stageView = root.querySelector('#stage-view');\n const stageItems = root.querySelector('#stage-items');\n const pendingMore = root.querySelector('#pending-more-btn');\n\n if (d.pending_list && d.pending_list.length > 0) {\n stageView.style.display = 'block';\n stageItems.innerHTML = renderRows(d.pending_list);\n if (pendingMore) pendingMore.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n stageView.style.display = 'none';\n }\n\n const managerView = root.querySelector('#manager-view');\n const activityBody = root.querySelector('#activity-body');\n const recentMore = root.querySelector('#recent-more-btn');\n\n if (userRole !== 'stage_user' && d.recent_list && d.recent_list.length > 0) {\n managerView.style.display = 'block';\n activityBody.innerHTML = renderRows(d.recent_list);\n if (recentMore) recentMore.style.display = d.has_more_recent ? 'flex' : 'none';\n } else {\n managerView.style.display = 'none';\n }\n }\n });\n }\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_pending_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Pending' });\n };\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_responded_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Close' });\n };\n\n $(document).on('workspace_render', () => refresh(currentStatusFilter)); refresh(currentStatusFilter);\n})();", + "script": "(function () {\n const root = root_element || document;\n let userRole = '';\n let pendingStart = 0;\n let recentStart = 0;\n let currentStatusFilter = '';\n\n const upd = (id, val) => { \n const el = root.querySelector(`#${id}`); \n if (el) el.innerText = val ?? 0; \n };\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name}\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]}\n `).join('');\n\n window.load_more_data = (type) => {\n const args = {\n pending_start: type === 'pending' ? pendingStart + 10 : pendingStart,\n recent_start: type === 'recent' ? recentStart + 10 : recentStart,\n status: currentStatusFilter\n };\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: args,\n callback: function (r) {\n if (!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 window.handle_status_filter = (val) => {\n currentStatusFilter = val;\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (val) clearBtn.style.display = 'flex';\n else clearBtn.style.display = 'none';\n refresh(val);\n };\n\n window.clear_filters = () => {\n const select = root.querySelector('#status-filter');\n if (select) select.value = '';\n currentStatusFilter = '';\n root.querySelector('#clear-filter-btn').style.display = 'none';\n refresh('');\n };\n\n function refresh(status = '') {\n pendingStart = 0; recentStart = 0;\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: { pending_start: 0, recent_start: 0, status: status },\n callback: function (r) {\n if (!r.message?.success) return;\n const d = r.message; \n \n userRole = d.role_type;\n\n // Visibility Control\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n const statusFilter = root.querySelector('#status-filter');\n \n if (userRole === 'stage_user') {\n if (masterCapsules) masterCapsules.style.display = 'none';\n if (draftCard) draftCard.style.display = 'none';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n } else {\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (draftCard) draftCard.style.display = 'block';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n }\n\n\n if (userRole === 'stage_user') {\n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n } else {\n upd('val-draft', d.draft_count);\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n }\n\n const stageView = root.querySelector('#stage-view');\n const stageItems = root.querySelector('#stage-items');\n const pendingMore = root.querySelector('#pending-more-btn');\n\n if (d.pending_list && d.pending_list.length > 0) {\n stageView.style.display = 'block';\n stageItems.innerHTML = renderRows(d.pending_list);\n if (pendingMore) pendingMore.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n stageView.style.display = 'none';\n }\n\n const managerView = root.querySelector('#manager-view');\n const activityBody = root.querySelector('#activity-body');\n const recentMore = root.querySelector('#recent-more-btn');\n\n if (userRole !== 'stage_user' && d.recent_list && d.recent_list.length > 0) {\n managerView.style.display = 'block';\n activityBody.innerHTML = renderRows(d.recent_list);\n if (recentMore) recentMore.style.display = d.has_more_recent ? 'flex' : 'none';\n } else {\n managerView.style.display = 'none';\n }\n }\n });\n }\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_pending_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Pending' });\n };\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_responded_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Close' });\n };\n\n $(document).on('workspace_render', () => refresh(currentStatusFilter)); refresh(currentStatusFilter);\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: wrap; gap: 15px; }\n.title-wrap { display: flex; align-items: center; gap: 10px; }\n.db-title { font-size: 22px; font-weight: 800; color: #0f172a; margin: 0; }\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: 8px; flex-wrap: wrap; }\n.master-capsule { background: #ffffff; padding: 6px 14px; border-radius: 50px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }\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: 14px; }\n.master-capsule span { font-size: 11px; font-weight: 700; color: #475569; white-space: nowrap; }\n\n.create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; }\n.create-btn-capsule i { color: white !important; }\n.create-btn-capsule span { color: white !important; }\n.create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.05); }\n\n.grid-compact { display: grid; gap: 12px; margin-bottom: 24px; }\n.stats-grid { grid-template-columns: repeat(3, 1fr); }\n\n.blue-txt { color: #2563eb; } .purple-txt { color: #7c3aed; } .orange-txt { color: #ea580c; } .green-txt { color: #16a34a; }\n\n.compact-stat-card { background: #ffffff; padding: 12px 15px; border-radius: 14px; border: 1px solid #e2e8f0; position: relative; overflow: hidden; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.compact-stat-card:hover { border-color: #cbd5e1; box-shadow: 0 8px 10px -1px rgba(0,0,0,0.05); }\n.stat-label-small { font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.6px; }\n.stat-row { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }\n.stat-val-small { font-size: 22px; font-weight: 800; color: #0f172a; }\n.icon-stat { font-size: 20px; opacity: 0.8; }\n\n.accent-bar { position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; }\n.blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; }\n\n.filter-row-container { display: flex; align-items: center; justify-content: space-between; background: #ffffff; padding: 12px 20px; border-radius: 12px; border: 1px solid #e2e8f0; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.filter-group { display: flex; align-items: center; gap: 15px; }\n.filter-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 800; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; }\n.filter-label i { color: #3b82f6; font-size: 14px; }\n.select-wrapper { position: relative; display: flex; align-items: center; }\n.status-select { appearance: none; background: #f8fafc; border: 1px solid #e2e8f0; padding: 6px 35px 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 700; color: #1e293b; cursor: pointer; transition: 0.2s; min-width: 140px; }\n.status-select:hover { border-color: #3b82f6; background: #ffffff; }\n.status-select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }\n.select-arrow { position: absolute; right: 12px; pointer-events: none; font-size: 10px; color: #64748b; }\n.clear-btn { display: flex; align-items: center; gap: 6px; background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; }\n.clear-btn:hover { background: #fecaca; transform: translateY(-1px); }\n\n.load-more-btn { background: #ffffff; color: #475569; border: 1px solid #e2e8f0; padding: 8px 24px; border-radius: 50px; cursor: pointer; font-size: 12px; 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.list-section { margin-top: 20px; }\n.list-header { font-size: 12px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 12px; letter-spacing: 0.8px; }\n.table-light-wrap { background: #ffffff; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\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; letter-spacing: 0.5px; }\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; pointer-events: none; }\n.t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }\n.t-status.close { 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.mini-table th:first-child, .mini-table td:first-child { width: 60px; text-align: center; font-weight: 700; color: #64748b; }\n\n@media (max-width: 768px) {\n .db-header { flex-direction: column; align-items: flex-start; }\n .master-capsule-container { width: 100%; }\n .stats-grid { grid-template-columns: 1fr; }\n .filter-row-container { flex-direction: column; gap: 12px; align-items: flex-start; }\n .filter-group { width: 100%; }\n .select-wrapper { width: 100%; }\n .status-select { width: 100%; }\n}" } ] \ No newline at end of file From 7a5b62bf7e259980b9600e144c6cedac4a59a85c Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Wed, 20 May 2026 14:12:00 +0530 Subject: [PATCH 2/7] feat: add Total Records card to audit dashboard --- audit_management/fixtures/custom_html_block.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/audit_management/fixtures/custom_html_block.json b/audit_management/fixtures/custom_html_block.json index ed58513..f553439 100644 --- a/audit_management/fixtures/custom_html_block.json +++ b/audit_management/fixtures/custom_html_block.json @@ -2,11 +2,11 @@ { "docstatus": 0, "doctype": "Custom HTML Block", - "html": "
\n
\n
\n

Audit Management

\n
\n
\n
\n
\n \n Audit Levels\n
\n
\n \n Query Types\n
\n
\n \n Settings\n
\n
\n \n Reports\n
\n
\n \n Create Audit\n
\n
\n
\n\n \n
\n
\n
Draft
\n
\n
-
\n \n
\n
\n
\n
\n
Pending
\n
\n
-
\n \n
\n
\n
\n
\n
Closed
\n
\n
-
\n \n
\n
\n
\n
\n\n \n
\n
\n
\n \n Filter by Status\n
\n
\n \n \n
\n
\n \n
\n\n \n \n\n \n
", + "html": "
\n
\n
\n

Audit Management

\n
\n
\n
\n
\n \n Audit Levels\n
\n
\n \n Query Types\n
\n
\n \n Settings\n
\n
\n \n Reports\n
\n
\n \n Create Audit\n
\n
\n
\n\n \n
\n
\n
Total Records
\n
\n
-
\n \n
\n
\n
\n
\n
Draft
\n
\n
-
\n \n
\n
\n
\n
\n
Pending
\n
\n
-
\n \n
\n
\n
\n
\n
Closed
\n
\n
-
\n \n
\n
\n
\n
\n\n \n
\n
\n
\n \n Filter by Status\n
\n
\n \n \n
\n
\n \n
\n\n \n \n\n \n
", "modified": "2026-05-19 12:00:00.000000", "name": "Audit Management", "private": 0, "roles": [], - "script": "(function () {\n const root = root_element || document;\n let userRole = '';\n let pendingStart = 0;\n let recentStart = 0;\n let currentStatusFilter = '';\n\n const upd = (id, val) => { \n const el = root.querySelector(`#${id}`); \n if (el) el.innerText = val ?? 0; \n };\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name}\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]}\n `).join('');\n\n window.load_more_data = (type) => {\n const args = {\n pending_start: type === 'pending' ? pendingStart + 10 : pendingStart,\n recent_start: type === 'recent' ? recentStart + 10 : recentStart,\n status: currentStatusFilter\n };\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: args,\n callback: function (r) {\n if (!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 window.handle_status_filter = (val) => {\n currentStatusFilter = val;\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (val) clearBtn.style.display = 'flex';\n else clearBtn.style.display = 'none';\n refresh(val);\n };\n\n window.clear_filters = () => {\n const select = root.querySelector('#status-filter');\n if (select) select.value = '';\n currentStatusFilter = '';\n root.querySelector('#clear-filter-btn').style.display = 'none';\n refresh('');\n };\n\n function refresh(status = '') {\n pendingStart = 0; recentStart = 0;\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: { pending_start: 0, recent_start: 0, status: status },\n callback: function (r) {\n if (!r.message?.success) return;\n const d = r.message; \n \n userRole = d.role_type;\n\n // Visibility Control\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n const statusFilter = root.querySelector('#status-filter');\n \n if (userRole === 'stage_user') {\n if (masterCapsules) masterCapsules.style.display = 'none';\n if (draftCard) draftCard.style.display = 'none';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n } else {\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (draftCard) draftCard.style.display = 'block';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n }\n\n\n if (userRole === 'stage_user') {\n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n } else {\n upd('val-draft', d.draft_count);\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n }\n\n const stageView = root.querySelector('#stage-view');\n const stageItems = root.querySelector('#stage-items');\n const pendingMore = root.querySelector('#pending-more-btn');\n\n if (d.pending_list && d.pending_list.length > 0) {\n stageView.style.display = 'block';\n stageItems.innerHTML = renderRows(d.pending_list);\n if (pendingMore) pendingMore.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n stageView.style.display = 'none';\n }\n\n const managerView = root.querySelector('#manager-view');\n const activityBody = root.querySelector('#activity-body');\n const recentMore = root.querySelector('#recent-more-btn');\n\n if (userRole !== 'stage_user' && d.recent_list && d.recent_list.length > 0) {\n managerView.style.display = 'block';\n activityBody.innerHTML = renderRows(d.recent_list);\n if (recentMore) recentMore.style.display = d.has_more_recent ? 'flex' : 'none';\n } else {\n managerView.style.display = 'none';\n }\n }\n });\n }\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_pending_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Pending' });\n };\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_responded_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Close' });\n };\n\n $(document).on('workspace_render', () => refresh(currentStatusFilter)); refresh(currentStatusFilter);\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: wrap; gap: 15px; }\n.title-wrap { display: flex; align-items: center; gap: 10px; }\n.db-title { font-size: 22px; font-weight: 800; color: #0f172a; margin: 0; }\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: 8px; flex-wrap: wrap; }\n.master-capsule { background: #ffffff; padding: 6px 14px; border-radius: 50px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }\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: 14px; }\n.master-capsule span { font-size: 11px; font-weight: 700; color: #475569; white-space: nowrap; }\n\n.create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; }\n.create-btn-capsule i { color: white !important; }\n.create-btn-capsule span { color: white !important; }\n.create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.05); }\n\n.grid-compact { display: grid; gap: 12px; margin-bottom: 24px; }\n.stats-grid { grid-template-columns: repeat(3, 1fr); }\n\n.blue-txt { color: #2563eb; } .purple-txt { color: #7c3aed; } .orange-txt { color: #ea580c; } .green-txt { color: #16a34a; }\n\n.compact-stat-card { background: #ffffff; padding: 12px 15px; border-radius: 14px; border: 1px solid #e2e8f0; position: relative; overflow: hidden; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.compact-stat-card:hover { border-color: #cbd5e1; box-shadow: 0 8px 10px -1px rgba(0,0,0,0.05); }\n.stat-label-small { font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.6px; }\n.stat-row { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }\n.stat-val-small { font-size: 22px; font-weight: 800; color: #0f172a; }\n.icon-stat { font-size: 20px; opacity: 0.8; }\n\n.accent-bar { position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; }\n.blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; }\n\n.filter-row-container { display: flex; align-items: center; justify-content: space-between; background: #ffffff; padding: 12px 20px; border-radius: 12px; border: 1px solid #e2e8f0; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.filter-group { display: flex; align-items: center; gap: 15px; }\n.filter-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 800; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; }\n.filter-label i { color: #3b82f6; font-size: 14px; }\n.select-wrapper { position: relative; display: flex; align-items: center; }\n.status-select { appearance: none; background: #f8fafc; border: 1px solid #e2e8f0; padding: 6px 35px 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 700; color: #1e293b; cursor: pointer; transition: 0.2s; min-width: 140px; }\n.status-select:hover { border-color: #3b82f6; background: #ffffff; }\n.status-select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }\n.select-arrow { position: absolute; right: 12px; pointer-events: none; font-size: 10px; color: #64748b; }\n.clear-btn { display: flex; align-items: center; gap: 6px; background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; }\n.clear-btn:hover { background: #fecaca; transform: translateY(-1px); }\n\n.load-more-btn { background: #ffffff; color: #475569; border: 1px solid #e2e8f0; padding: 8px 24px; border-radius: 50px; cursor: pointer; font-size: 12px; 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.list-section { margin-top: 20px; }\n.list-header { font-size: 12px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 12px; letter-spacing: 0.8px; }\n.table-light-wrap { background: #ffffff; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\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; letter-spacing: 0.5px; }\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; pointer-events: none; }\n.t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }\n.t-status.close { 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.mini-table th:first-child, .mini-table td:first-child { width: 60px; text-align: center; font-weight: 700; color: #64748b; }\n\n@media (max-width: 768px) {\n .db-header { flex-direction: column; align-items: flex-start; }\n .master-capsule-container { width: 100%; }\n .stats-grid { grid-template-columns: 1fr; }\n .filter-row-container { flex-direction: column; gap: 12px; align-items: flex-start; }\n .filter-group { width: 100%; }\n .select-wrapper { width: 100%; }\n .status-select { width: 100%; }\n}" } + "script": "(function () {\n const root = root_element || document;\n let userRole = '';\n let pendingStart = 0;\n let recentStart = 0;\n let currentStatusFilter = '';\n\n const upd = (id, val) => { \n const el = root.querySelector(`#${id}`); \n if (el) el.innerText = val ?? 0; \n };\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name}\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]}\n `).join('');\n\n window.load_more_data = (type) => {\n const args = {\n pending_start: type === 'pending' ? pendingStart + 10 : pendingStart,\n recent_start: type === 'recent' ? recentStart + 10 : recentStart,\n status: currentStatusFilter\n };\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: args,\n callback: function (r) {\n if (!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 window.handle_status_filter = (val) => {\n currentStatusFilter = val;\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (val) clearBtn.style.display = 'flex';\n else clearBtn.style.display = 'none';\n refresh(val);\n };\n\n window.clear_filters = () => {\n const select = root.querySelector('#status-filter');\n if (select) select.value = '';\n currentStatusFilter = '';\n root.querySelector('#clear-filter-btn').style.display = 'none';\n refresh('');\n };\n\n function refresh(status = '') {\n pendingStart = 0; recentStart = 0;\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: { pending_start: 0, recent_start: 0, status: status },\n callback: function (r) {\n if (!r.message?.success) return;\n const d = r.message; \n \n userRole = d.role_type;\n\n // Visibility Control\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n const statusFilter = root.querySelector('#status-filter');\n \n if (userRole === 'stage_user') {\n if (masterCapsules) masterCapsules.style.display = 'none';\n if (draftCard) draftCard.style.display = 'none';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n } else {\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (draftCard) draftCard.style.display = 'block';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n }\n\n\n if (userRole === 'stage_user') {\n upd('val-total', (d.pending_for_me || 0) + (d.responded_by_me || 0));\n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n } else {\n upd('val-total', (d.draft_count || 0) + (d.total_pending || 0) + (d.closed_count || 0));\n upd('val-draft', d.draft_count);\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n }\n\n const stageView = root.querySelector('#stage-view');\n const stageItems = root.querySelector('#stage-items');\n const pendingMore = root.querySelector('#pending-more-btn');\n\n if (d.pending_list && d.pending_list.length > 0) {\n stageView.style.display = 'block';\n stageItems.innerHTML = renderRows(d.pending_list);\n if (pendingMore) pendingMore.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n stageView.style.display = 'none';\n }\n\n const managerView = root.querySelector('#manager-view');\n const activityBody = root.querySelector('#activity-body');\n const recentMore = root.querySelector('#recent-more-btn');\n\n if (userRole !== 'stage_user' && d.recent_list && d.recent_list.length > 0) {\n managerView.style.display = 'block';\n activityBody.innerHTML = renderRows(d.recent_list);\n if (recentMore) recentMore.style.display = d.has_more_recent ? 'flex' : 'none';\n } else {\n managerView.style.display = 'none';\n }\n }\n });\n }\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_pending_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Pending' });\n };\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_responded_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Close' });\n };\n\n $(document).on('workspace_render', () => refresh(currentStatusFilter)); refresh(currentStatusFilter);\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: wrap; gap: 15px; }\n.title-wrap { display: flex; align-items: center; gap: 10px; }\n.db-title { font-size: 22px; font-weight: 800; color: #0f172a; margin: 0; }\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: 8px; flex-wrap: wrap; }\n.master-capsule { background: #ffffff; padding: 6px 14px; border-radius: 50px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }\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: 14px; }\n.master-capsule span { font-size: 11px; font-weight: 700; color: #475569; white-space: nowrap; }\n\n.create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; }\n.create-btn-capsule i { color: white !important; }\n.create-btn-capsule span { color: white !important; }\n.create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.05); }\n\n.grid-compact { display: grid; gap: 12px; margin-bottom: 24px; }\n.stats-grid { grid-template-columns: repeat(4, 1fr); }\n\n.blue-txt { color: #2563eb; } .purple-txt { color: #7c3aed; } .orange-txt { color: #ea580c; } .green-txt { color: #16a34a; }\n\n.compact-stat-card { background: #ffffff; padding: 12px 15px; border-radius: 14px; border: 1px solid #e2e8f0; position: relative; overflow: hidden; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.compact-stat-card:hover { border-color: #cbd5e1; box-shadow: 0 8px 10px -1px rgba(0,0,0,0.05); }\n.stat-label-small { font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.6px; }\n.stat-row { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }\n.stat-val-small { font-size: 22px; font-weight: 800; color: #0f172a; }\n.icon-stat { font-size: 20px; opacity: 0.8; }\n\n.accent-bar { position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; }\n.blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; }\n\n.filter-row-container { display: flex; align-items: center; justify-content: space-between; background: #ffffff; padding: 12px 20px; border-radius: 12px; border: 1px solid #e2e8f0; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.filter-group { display: flex; align-items: center; gap: 15px; }\n.filter-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 800; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; }\n.filter-label i { color: #3b82f6; font-size: 14px; }\n.select-wrapper { position: relative; display: flex; align-items: center; }\n.status-select { appearance: none; background: #f8fafc; border: 1px solid #e2e8f0; padding: 6px 35px 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 700; color: #1e293b; cursor: pointer; transition: 0.2s; min-width: 140px; }\n.status-select:hover { border-color: #3b82f6; background: #ffffff; }\n.status-select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }\n.select-arrow { position: absolute; right: 12px; pointer-events: none; font-size: 10px; color: #64748b; }\n.clear-btn { display: flex; align-items: center; gap: 6px; background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; }\n.clear-btn:hover { background: #fecaca; transform: translateY(-1px); }\n\n.load-more-btn { background: #ffffff; color: #475569; border: 1px solid #e2e8f0; padding: 8px 24px; border-radius: 50px; cursor: pointer; font-size: 12px; 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.list-section { margin-top: 20px; }\n.list-header { font-size: 12px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 12px; letter-spacing: 0.8px; }\n.table-light-wrap { background: #ffffff; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\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; letter-spacing: 0.5px; }\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; pointer-events: none; }\n.t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }\n.t-status.close { 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.mini-table th:first-child, .mini-table td:first-child { width: 60px; text-align: center; font-weight: 700; color: #64748b; }\n\n@media (max-width: 768px) {\n .db-header { flex-direction: column; align-items: flex-start; }\n .master-capsule-container { width: 100%; }\n .stats-grid { grid-template-columns: 1fr; }\n .filter-row-container { flex-direction: column; gap: 12px; align-items: flex-start; }\n .filter-group { width: 100%; }\n .select-wrapper { width: 100%; }\n .status-select { width: 100%; }\n}" } ] \ No newline at end of file From a1ab3d0692a4e605631610197b2988f002427a16 Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Wed, 20 May 2026 15:25:17 +0530 Subject: [PATCH 3/7] feat: color Pending card as red in audit dashboard --- audit_management/fixtures/custom_html_block.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/audit_management/fixtures/custom_html_block.json b/audit_management/fixtures/custom_html_block.json index f553439..d61025b 100644 --- a/audit_management/fixtures/custom_html_block.json +++ b/audit_management/fixtures/custom_html_block.json @@ -2,11 +2,11 @@ { "docstatus": 0, "doctype": "Custom HTML Block", - "html": "
\n
\n
\n

Audit Management

\n
\n
\n
\n
\n \n Audit Levels\n
\n
\n \n Query Types\n
\n
\n \n Settings\n
\n
\n \n Reports\n
\n
\n \n Create Audit\n
\n
\n
\n\n \n
\n
\n
Total Records
\n
\n
-
\n \n
\n
\n
\n
\n
Draft
\n
\n
-
\n \n
\n
\n
\n
\n
Pending
\n
\n
-
\n \n
\n
\n
\n
\n
Closed
\n
\n
-
\n \n
\n
\n
\n
\n\n \n
\n
\n
\n \n Filter by Status\n
\n
\n \n \n
\n
\n \n
\n\n \n \n\n \n
", + "html": "
\n
\n
\n

Audit Management

\n
\n
\n
\n
\n \n Audit Levels\n
\n
\n \n Query Types\n
\n
\n \n Settings\n
\n
\n \n Reports\n
\n
\n \n Create Audit\n
\n
\n
\n\n \n
\n
\n
Total Records
\n
\n
-
\n \n
\n
\n
\n
\n
Draft
\n
\n
-
\n \n
\n
\n
\n
\n
Pending
\n
\n
-
\n \n
\n
\n
\n
\n
Closed
\n
\n
-
\n \n
\n
\n
\n
\n\n \n
\n
\n
\n \n Filter by Status\n
\n
\n \n \n
\n
\n \n
\n\n \n \n\n \n
", "modified": "2026-05-19 12:00:00.000000", "name": "Audit Management", "private": 0, "roles": [], "script": "(function () {\n const root = root_element || document;\n let userRole = '';\n let pendingStart = 0;\n let recentStart = 0;\n let currentStatusFilter = '';\n\n const upd = (id, val) => { \n const el = root.querySelector(`#${id}`); \n if (el) el.innerText = val ?? 0; \n };\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name}\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]}\n `).join('');\n\n window.load_more_data = (type) => {\n const args = {\n pending_start: type === 'pending' ? pendingStart + 10 : pendingStart,\n recent_start: type === 'recent' ? recentStart + 10 : recentStart,\n status: currentStatusFilter\n };\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: args,\n callback: function (r) {\n if (!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 window.handle_status_filter = (val) => {\n currentStatusFilter = val;\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (val) clearBtn.style.display = 'flex';\n else clearBtn.style.display = 'none';\n refresh(val);\n };\n\n window.clear_filters = () => {\n const select = root.querySelector('#status-filter');\n if (select) select.value = '';\n currentStatusFilter = '';\n root.querySelector('#clear-filter-btn').style.display = 'none';\n refresh('');\n };\n\n function refresh(status = '') {\n pendingStart = 0; recentStart = 0;\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: { pending_start: 0, recent_start: 0, status: status },\n callback: function (r) {\n if (!r.message?.success) return;\n const d = r.message; \n \n userRole = d.role_type;\n\n // Visibility Control\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n const statusFilter = root.querySelector('#status-filter');\n \n if (userRole === 'stage_user') {\n if (masterCapsules) masterCapsules.style.display = 'none';\n if (draftCard) draftCard.style.display = 'none';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n } else {\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (draftCard) draftCard.style.display = 'block';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n }\n\n\n if (userRole === 'stage_user') {\n upd('val-total', (d.pending_for_me || 0) + (d.responded_by_me || 0));\n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n } else {\n upd('val-total', (d.draft_count || 0) + (d.total_pending || 0) + (d.closed_count || 0));\n upd('val-draft', d.draft_count);\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n }\n\n const stageView = root.querySelector('#stage-view');\n const stageItems = root.querySelector('#stage-items');\n const pendingMore = root.querySelector('#pending-more-btn');\n\n if (d.pending_list && d.pending_list.length > 0) {\n stageView.style.display = 'block';\n stageItems.innerHTML = renderRows(d.pending_list);\n if (pendingMore) pendingMore.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n stageView.style.display = 'none';\n }\n\n const managerView = root.querySelector('#manager-view');\n const activityBody = root.querySelector('#activity-body');\n const recentMore = root.querySelector('#recent-more-btn');\n\n if (userRole !== 'stage_user' && d.recent_list && d.recent_list.length > 0) {\n managerView.style.display = 'block';\n activityBody.innerHTML = renderRows(d.recent_list);\n if (recentMore) recentMore.style.display = d.has_more_recent ? 'flex' : 'none';\n } else {\n managerView.style.display = 'none';\n }\n }\n });\n }\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_pending_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Pending' });\n };\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_responded_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Close' });\n };\n\n $(document).on('workspace_render', () => refresh(currentStatusFilter)); refresh(currentStatusFilter);\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: wrap; gap: 15px; }\n.title-wrap { display: flex; align-items: center; gap: 10px; }\n.db-title { font-size: 22px; font-weight: 800; color: #0f172a; margin: 0; }\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: 8px; flex-wrap: wrap; }\n.master-capsule { background: #ffffff; padding: 6px 14px; border-radius: 50px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }\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: 14px; }\n.master-capsule span { font-size: 11px; font-weight: 700; color: #475569; white-space: nowrap; }\n\n.create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; }\n.create-btn-capsule i { color: white !important; }\n.create-btn-capsule span { color: white !important; }\n.create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.05); }\n\n.grid-compact { display: grid; gap: 12px; margin-bottom: 24px; }\n.stats-grid { grid-template-columns: repeat(4, 1fr); }\n\n.blue-txt { color: #2563eb; } .purple-txt { color: #7c3aed; } .orange-txt { color: #ea580c; } .green-txt { color: #16a34a; }\n\n.compact-stat-card { background: #ffffff; padding: 12px 15px; border-radius: 14px; border: 1px solid #e2e8f0; position: relative; overflow: hidden; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.compact-stat-card:hover { border-color: #cbd5e1; box-shadow: 0 8px 10px -1px rgba(0,0,0,0.05); }\n.stat-label-small { font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.6px; }\n.stat-row { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }\n.stat-val-small { font-size: 22px; font-weight: 800; color: #0f172a; }\n.icon-stat { font-size: 20px; opacity: 0.8; }\n\n.accent-bar { position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; }\n.blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; }\n\n.filter-row-container { display: flex; align-items: center; justify-content: space-between; background: #ffffff; padding: 12px 20px; border-radius: 12px; border: 1px solid #e2e8f0; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.filter-group { display: flex; align-items: center; gap: 15px; }\n.filter-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 800; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; }\n.filter-label i { color: #3b82f6; font-size: 14px; }\n.select-wrapper { position: relative; display: flex; align-items: center; }\n.status-select { appearance: none; background: #f8fafc; border: 1px solid #e2e8f0; padding: 6px 35px 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 700; color: #1e293b; cursor: pointer; transition: 0.2s; min-width: 140px; }\n.status-select:hover { border-color: #3b82f6; background: #ffffff; }\n.status-select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }\n.select-arrow { position: absolute; right: 12px; pointer-events: none; font-size: 10px; color: #64748b; }\n.clear-btn { display: flex; align-items: center; gap: 6px; background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; }\n.clear-btn:hover { background: #fecaca; transform: translateY(-1px); }\n\n.load-more-btn { background: #ffffff; color: #475569; border: 1px solid #e2e8f0; padding: 8px 24px; border-radius: 50px; cursor: pointer; font-size: 12px; 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.list-section { margin-top: 20px; }\n.list-header { font-size: 12px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 12px; letter-spacing: 0.8px; }\n.table-light-wrap { background: #ffffff; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\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; letter-spacing: 0.5px; }\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; pointer-events: none; }\n.t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }\n.t-status.close { 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.mini-table th:first-child, .mini-table td:first-child { width: 60px; text-align: center; font-weight: 700; color: #64748b; }\n\n@media (max-width: 768px) {\n .db-header { flex-direction: column; align-items: flex-start; }\n .master-capsule-container { width: 100%; }\n .stats-grid { grid-template-columns: 1fr; }\n .filter-row-container { flex-direction: column; gap: 12px; align-items: flex-start; }\n .filter-group { width: 100%; }\n .select-wrapper { width: 100%; }\n .status-select { width: 100%; }\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: wrap; gap: 15px; }\n.title-wrap { display: flex; align-items: center; gap: 10px; }\n.db-title { font-size: 22px; font-weight: 800; color: #0f172a; margin: 0; }\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: 8px; flex-wrap: wrap; }\n.master-capsule { background: #ffffff; padding: 6px 14px; border-radius: 50px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }\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: 14px; }\n.master-capsule span { font-size: 11px; font-weight: 700; color: #475569; white-space: nowrap; }\n\n.create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; }\n.create-btn-capsule i { color: white !important; }\n.create-btn-capsule span { color: white !important; }\n.create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.05); }\n\n.grid-compact { display: grid; gap: 12px; margin-bottom: 24px; }\n.stats-grid { grid-template-columns: repeat(4, 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: 12px 15px; border-radius: 14px; border: 1px solid #e2e8f0; position: relative; overflow: hidden; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.compact-stat-card:hover { border-color: #cbd5e1; box-shadow: 0 8px 10px -1px rgba(0,0,0,0.05); }\n.stat-label-small { font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.6px; }\n.stat-row { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }\n.stat-val-small { font-size: 22px; font-weight: 800; color: #0f172a; }\n.icon-stat { font-size: 20px; opacity: 0.8; }\n\n.accent-bar { position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; }\n.blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; } .red-bg { background: #ef4444; }\n\n.filter-row-container { display: flex; align-items: center; justify-content: space-between; background: #ffffff; padding: 12px 20px; border-radius: 12px; border: 1px solid #e2e8f0; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.filter-group { display: flex; align-items: center; gap: 15px; }\n.filter-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 800; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; }\n.filter-label i { color: #3b82f6; font-size: 14px; }\n.select-wrapper { position: relative; display: flex; align-items: center; }\n.status-select { appearance: none; background: #f8fafc; border: 1px solid #e2e8f0; padding: 6px 35px 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 700; color: #1e293b; cursor: pointer; transition: 0.2s; min-width: 140px; }\n.status-select:hover { border-color: #3b82f6; background: #ffffff; }\n.status-select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }\n.select-arrow { position: absolute; right: 12px; pointer-events: none; font-size: 10px; color: #64748b; }\n.clear-btn { display: flex; align-items: center; gap: 6px; background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; }\n.clear-btn:hover { background: #fecaca; transform: translateY(-1px); }\n\n.load-more-btn { background: #ffffff; color: #475569; border: 1px solid #e2e8f0; padding: 8px 24px; border-radius: 50px; cursor: pointer; font-size: 12px; 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.list-section { margin-top: 20px; }\n.list-header { font-size: 12px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 12px; letter-spacing: 0.8px; }\n.table-light-wrap { background: #ffffff; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\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; letter-spacing: 0.5px; }\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; pointer-events: none; }\n.t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }\n.t-status.close { 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.mini-table th:first-child, .mini-table td:first-child { width: 60px; text-align: center; font-weight: 700; color: #64748b; }\n\n@media (max-width: 768px) {\n .db-header { flex-direction: column; align-items: flex-start; }\n .master-capsule-container { width: 100%; }\n .stats-grid { grid-template-columns: 1fr; }\n .filter-row-container { flex-direction: column; gap: 12px; align-items: flex-start; }\n .filter-group { width: 100%; }\n .select-wrapper { width: 100%; }\n .status-select { width: 100%; }\n}" } ] \ No newline at end of file From 6c185a45e919df9605cc36e12129f15f6b5375e5 Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Wed, 20 May 2026 15:35:36 +0530 Subject: [PATCH 4/7] feat: refactor dashboard header navigation into an Actions dropdown --- audit_management/fixtures/custom_html_block.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/audit_management/fixtures/custom_html_block.json b/audit_management/fixtures/custom_html_block.json index d61025b..5ed3a45 100644 --- a/audit_management/fixtures/custom_html_block.json +++ b/audit_management/fixtures/custom_html_block.json @@ -2,11 +2,11 @@ { "docstatus": 0, "doctype": "Custom HTML Block", - "html": "
\n
\n
\n

Audit Management

\n
\n
\n
\n
\n \n Audit Levels\n
\n
\n \n Query Types\n
\n
\n \n Settings\n
\n
\n \n Reports\n
\n
\n \n Create Audit\n
\n
\n
\n\n \n
\n
\n
Total Records
\n
\n
-
\n \n
\n
\n
\n
\n
Draft
\n
\n
-
\n \n
\n
\n
\n
\n
Pending
\n
\n
-
\n \n
\n
\n
\n
\n
Closed
\n
\n
-
\n \n
\n
\n
\n
\n\n \n
\n
\n
\n \n Filter by Status\n
\n
\n \n \n
\n
\n \n
\n\n \n \n\n \n
", + "html": "
\n
\n
\n

Audit Management

\n
\n
\n
\n \n
\n \n Create Audit\n
\n
\n
\n\n \n
\n
\n
Total Records
\n
\n
-
\n \n
\n
\n
\n
\n
Draft
\n
\n
-
\n \n
\n
\n
\n
\n
Pending
\n
\n
-
\n \n
\n
\n
\n
\n
Closed
\n
\n
-
\n \n
\n
\n
\n
\n\n \n
\n
\n
\n \n Filter by Status\n
\n
\n \n \n
\n
\n \n
\n\n \n \n\n \n
", "modified": "2026-05-19 12:00:00.000000", "name": "Audit Management", "private": 0, "roles": [], - "script": "(function () {\n const root = root_element || document;\n let userRole = '';\n let pendingStart = 0;\n let recentStart = 0;\n let currentStatusFilter = '';\n\n const upd = (id, val) => { \n const el = root.querySelector(`#${id}`); \n if (el) el.innerText = val ?? 0; \n };\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name}\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]}\n `).join('');\n\n window.load_more_data = (type) => {\n const args = {\n pending_start: type === 'pending' ? pendingStart + 10 : pendingStart,\n recent_start: type === 'recent' ? recentStart + 10 : recentStart,\n status: currentStatusFilter\n };\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: args,\n callback: function (r) {\n if (!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 window.handle_status_filter = (val) => {\n currentStatusFilter = val;\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (val) clearBtn.style.display = 'flex';\n else clearBtn.style.display = 'none';\n refresh(val);\n };\n\n window.clear_filters = () => {\n const select = root.querySelector('#status-filter');\n if (select) select.value = '';\n currentStatusFilter = '';\n root.querySelector('#clear-filter-btn').style.display = 'none';\n refresh('');\n };\n\n function refresh(status = '') {\n pendingStart = 0; recentStart = 0;\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: { pending_start: 0, recent_start: 0, status: status },\n callback: function (r) {\n if (!r.message?.success) return;\n const d = r.message; \n \n userRole = d.role_type;\n\n // Visibility Control\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n const statusFilter = root.querySelector('#status-filter');\n \n if (userRole === 'stage_user') {\n if (masterCapsules) masterCapsules.style.display = 'none';\n if (draftCard) draftCard.style.display = 'none';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n } else {\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (draftCard) draftCard.style.display = 'block';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n }\n\n\n if (userRole === 'stage_user') {\n upd('val-total', (d.pending_for_me || 0) + (d.responded_by_me || 0));\n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n } else {\n upd('val-total', (d.draft_count || 0) + (d.total_pending || 0) + (d.closed_count || 0));\n upd('val-draft', d.draft_count);\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n }\n\n const stageView = root.querySelector('#stage-view');\n const stageItems = root.querySelector('#stage-items');\n const pendingMore = root.querySelector('#pending-more-btn');\n\n if (d.pending_list && d.pending_list.length > 0) {\n stageView.style.display = 'block';\n stageItems.innerHTML = renderRows(d.pending_list);\n if (pendingMore) pendingMore.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n stageView.style.display = 'none';\n }\n\n const managerView = root.querySelector('#manager-view');\n const activityBody = root.querySelector('#activity-body');\n const recentMore = root.querySelector('#recent-more-btn');\n\n if (userRole !== 'stage_user' && d.recent_list && d.recent_list.length > 0) {\n managerView.style.display = 'block';\n activityBody.innerHTML = renderRows(d.recent_list);\n if (recentMore) recentMore.style.display = d.has_more_recent ? 'flex' : 'none';\n } else {\n managerView.style.display = 'none';\n }\n }\n });\n }\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_pending_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Pending' });\n };\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_responded_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Close' });\n };\n\n $(document).on('workspace_render', () => refresh(currentStatusFilter)); refresh(currentStatusFilter);\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: wrap; gap: 15px; }\n.title-wrap { display: flex; align-items: center; gap: 10px; }\n.db-title { font-size: 22px; font-weight: 800; color: #0f172a; margin: 0; }\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: 8px; flex-wrap: wrap; }\n.master-capsule { background: #ffffff; padding: 6px 14px; border-radius: 50px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }\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: 14px; }\n.master-capsule span { font-size: 11px; font-weight: 700; color: #475569; white-space: nowrap; }\n\n.create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; }\n.create-btn-capsule i { color: white !important; }\n.create-btn-capsule span { color: white !important; }\n.create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.05); }\n\n.grid-compact { display: grid; gap: 12px; margin-bottom: 24px; }\n.stats-grid { grid-template-columns: repeat(4, 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: 12px 15px; border-radius: 14px; border: 1px solid #e2e8f0; position: relative; overflow: hidden; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.compact-stat-card:hover { border-color: #cbd5e1; box-shadow: 0 8px 10px -1px rgba(0,0,0,0.05); }\n.stat-label-small { font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.6px; }\n.stat-row { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }\n.stat-val-small { font-size: 22px; font-weight: 800; color: #0f172a; }\n.icon-stat { font-size: 20px; opacity: 0.8; }\n\n.accent-bar { position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; }\n.blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; } .red-bg { background: #ef4444; }\n\n.filter-row-container { display: flex; align-items: center; justify-content: space-between; background: #ffffff; padding: 12px 20px; border-radius: 12px; border: 1px solid #e2e8f0; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.filter-group { display: flex; align-items: center; gap: 15px; }\n.filter-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 800; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; }\n.filter-label i { color: #3b82f6; font-size: 14px; }\n.select-wrapper { position: relative; display: flex; align-items: center; }\n.status-select { appearance: none; background: #f8fafc; border: 1px solid #e2e8f0; padding: 6px 35px 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 700; color: #1e293b; cursor: pointer; transition: 0.2s; min-width: 140px; }\n.status-select:hover { border-color: #3b82f6; background: #ffffff; }\n.status-select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }\n.select-arrow { position: absolute; right: 12px; pointer-events: none; font-size: 10px; color: #64748b; }\n.clear-btn { display: flex; align-items: center; gap: 6px; background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; }\n.clear-btn:hover { background: #fecaca; transform: translateY(-1px); }\n\n.load-more-btn { background: #ffffff; color: #475569; border: 1px solid #e2e8f0; padding: 8px 24px; border-radius: 50px; cursor: pointer; font-size: 12px; 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.list-section { margin-top: 20px; }\n.list-header { font-size: 12px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 12px; letter-spacing: 0.8px; }\n.table-light-wrap { background: #ffffff; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\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; letter-spacing: 0.5px; }\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; pointer-events: none; }\n.t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }\n.t-status.close { 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.mini-table th:first-child, .mini-table td:first-child { width: 60px; text-align: center; font-weight: 700; color: #64748b; }\n\n@media (max-width: 768px) {\n .db-header { flex-direction: column; align-items: flex-start; }\n .master-capsule-container { width: 100%; }\n .stats-grid { grid-template-columns: 1fr; }\n .filter-row-container { flex-direction: column; gap: 12px; align-items: flex-start; }\n .filter-group { width: 100%; }\n .select-wrapper { width: 100%; }\n .status-select { width: 100%; }\n}" } + "script": "(function () {\n const root = root_element || document;\n let userRole = '';\n let pendingStart = 0;\n let recentStart = 0;\n let currentStatusFilter = '';\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.addEventListener('click', () => {\n const dd = root.querySelector('#actions-dropdown');\n if (dd) dd.style.display = 'none';\n });\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name}\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]}\n `).join('');\n\n window.load_more_data = (type) => {\n const args = {\n pending_start: type === 'pending' ? pendingStart + 10 : pendingStart,\n recent_start: type === 'recent' ? recentStart + 10 : recentStart,\n status: currentStatusFilter\n };\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: args,\n callback: function (r) {\n if (!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 window.handle_status_filter = (val) => {\n currentStatusFilter = val;\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (val) clearBtn.style.display = 'flex';\n else clearBtn.style.display = 'none';\n refresh(val);\n };\n\n window.clear_filters = () => {\n const select = root.querySelector('#status-filter');\n if (select) select.value = '';\n currentStatusFilter = '';\n root.querySelector('#clear-filter-btn').style.display = 'none';\n refresh('');\n };\n\n function refresh(status = '') {\n pendingStart = 0; recentStart = 0;\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: { pending_start: 0, recent_start: 0, status: status },\n callback: function (r) {\n if (!r.message?.success) return;\n const d = r.message; \n \n userRole = d.role_type;\n\n // Visibility Control\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n const statusFilter = root.querySelector('#status-filter');\n \n if (userRole === 'stage_user') {\n if (masterCapsules) masterCapsules.style.display = 'none';\n if (draftCard) draftCard.style.display = 'none';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n } else {\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (draftCard) draftCard.style.display = 'block';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n }\n\n\n if (userRole === 'stage_user') {\n upd('val-total', (d.pending_for_me || 0) + (d.responded_by_me || 0));\n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n } else {\n upd('val-total', (d.draft_count || 0) + (d.total_pending || 0) + (d.closed_count || 0));\n upd('val-draft', d.draft_count);\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n }\n\n const stageView = root.querySelector('#stage-view');\n const stageItems = root.querySelector('#stage-items');\n const pendingMore = root.querySelector('#pending-more-btn');\n\n if (d.pending_list && d.pending_list.length > 0) {\n stageView.style.display = 'block';\n stageItems.innerHTML = renderRows(d.pending_list);\n if (pendingMore) pendingMore.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n stageView.style.display = 'none';\n }\n\n const managerView = root.querySelector('#manager-view');\n const activityBody = root.querySelector('#activity-body');\n const recentMore = root.querySelector('#recent-more-btn');\n\n if (userRole !== 'stage_user' && d.recent_list && d.recent_list.length > 0) {\n managerView.style.display = 'block';\n activityBody.innerHTML = renderRows(d.recent_list);\n if (recentMore) recentMore.style.display = d.has_more_recent ? 'flex' : 'none';\n } else {\n managerView.style.display = 'none';\n }\n }\n });\n }\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_pending_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Pending' });\n };\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_responded_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Close' });\n };\n\n $(document).on('workspace_render', () => refresh(currentStatusFilter)); refresh(currentStatusFilter);\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: wrap; gap: 15px; }\n.title-wrap { display: flex; align-items: center; gap: 10px; }\n.db-title { font-size: 22px; font-weight: 800; color: #0f172a; margin: 0; }\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: 8px; flex-wrap: wrap; }\n.master-capsule { background: #ffffff; padding: 6px 14px; border-radius: 50px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }\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: 14px; }\n.master-capsule span { font-size: 11px; font-weight: 700; color: #475569; white-space: nowrap; }\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.dropdown-item i { width: 16px; text-align: center; }\n\n.create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; }\n.create-btn-capsule i { color: white !important; }\n.create-btn-capsule span { color: white !important; }\n.create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.05); }\n\n.grid-compact { display: grid; gap: 12px; margin-bottom: 24px; }\n.stats-grid { grid-template-columns: repeat(4, 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: 12px 15px; border-radius: 14px; border: 1px solid #e2e8f0; position: relative; overflow: hidden; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.compact-stat-card:hover { border-color: #cbd5e1; box-shadow: 0 8px 10px -1px rgba(0,0,0,0.05); }\n.stat-label-small { font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.6px; }\n.stat-row { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }\n.stat-val-small { font-size: 22px; font-weight: 800; color: #0f172a; }\n.icon-stat { font-size: 20px; opacity: 0.8; }\n\n.accent-bar { position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; }\n.blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; } .red-bg { background: #ef4444; }\n\n.filter-row-container { display: flex; align-items: center; justify-content: space-between; background: #ffffff; padding: 12px 20px; border-radius: 12px; border: 1px solid #e2e8f0; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.filter-group { display: flex; align-items: center; gap: 15px; }\n.filter-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 800; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; }\n.filter-label i { color: #3b82f6; font-size: 14px; }\n.select-wrapper { position: relative; display: flex; align-items: center; }\n.status-select { appearance: none; background: #f8fafc; border: 1px solid #e2e8f0; padding: 6px 35px 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 700; color: #1e293b; cursor: pointer; transition: 0.2s; min-width: 140px; }\n.status-select:hover { border-color: #3b82f6; background: #ffffff; }\n.status-select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }\n.select-arrow { position: absolute; right: 12px; pointer-events: none; font-size: 10px; color: #64748b; }\n.clear-btn { display: flex; align-items: center; gap: 6px; background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; }\n.clear-btn:hover { background: #fecaca; transform: translateY(-1px); }\n\n.load-more-btn { background: #ffffff; color: #475569; border: 1px solid #e2e8f0; padding: 8px 24px; border-radius: 50px; cursor: pointer; font-size: 12px; 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.list-section { margin-top: 20px; }\n.list-header { font-size: 12px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 12px; letter-spacing: 0.8px; }\n.table-light-wrap { background: #ffffff; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\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; letter-spacing: 0.5px; }\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; pointer-events: none; }\n.t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }\n.t-status.close { 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.mini-table th:first-child, .mini-table td:first-child { width: 60px; text-align: center; font-weight: 700; color: #64748b; }\n\n@media (max-width: 768px) {\n .db-header { flex-direction: column; align-items: flex-start; }\n .master-capsule-container { width: 100%; }\n .stats-grid { grid-template-columns: 1fr; }\n .filter-row-container { flex-direction: column; gap: 12px; align-items: flex-start; }\n .filter-group { width: 100%; }\n .select-wrapper { width: 100%; }\n .status-select { width: 100%; }\n}" } ] \ No newline at end of file From 8b1e374e53ec2ae747ca25a78b4a48d6414cc76d Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Wed, 20 May 2026 17:10:14 +0530 Subject: [PATCH 5/7] feat: implement multi-select status and risk filtering in audit dashboard --- .../audit_management/dashboard.py | 261 +++--------------- .../fixtures/custom_html_block.json | 7 +- 2 files changed, 41 insertions(+), 227 deletions(-) diff --git a/audit_management/audit_management/dashboard.py b/audit_management/audit_management/dashboard.py index 164d8d0..252ff0a 100644 --- a/audit_management/audit_management/dashboard.py +++ b/audit_management/audit_management/dashboard.py @@ -1,11 +1,9 @@ import frappe import json - -import frappe from frappe import _ @frappe.whitelist() -def get_dashboard_stats(pending_start=0, recent_start=0, status=None): +def get_dashboard_stats(pending_start=0, recent_start=0, status=None, risk=None): user = frappe.session.user roles = frappe.get_roles(user) @@ -13,14 +11,25 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None): recent_start = int(recent_start) page_length = 10 - # ✅ Role flags + # Role flags is_admin = "Administrator" in roles or "System Manager" in roles is_manager = "Audit Manager" in roles is_member = "Audit Member" in roles + # Handle multiple statuses + status_list = [] + if status: + if isinstance(status, str): status_list = [s.strip() for s in status.split(',') if s.strip()] + elif isinstance(status, list): status_list = status + + # Handle multiple risks + risk_list = [] + if risk: + if isinstance(risk, str): risk_list = [r.strip() for r in risk.split(',') if r.strip()] + elif isinstance(risk, list): risk_list = risk + try: # 1. 🟢 FETCH PENDING FOR ME (From Child Table) - # -------------------------------------------- pending_items_query = """ SELECT DISTINCT parent FROM `tabAudit Items` @@ -43,20 +52,24 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None): pending_for_me_list = [] has_more_pending = False - # Use responded records if status is 'Responded', otherwise use pending + # Use responded records if status contains 'Responded', otherwise use pending active_records = pending_records - if status == 'Responded': + if 'Responded' in status_list: active_records = responded_records if active_records: parent_names = [r.parent for r in active_records] - # Apply status filter if provided + # Apply filters p_filters = {"name": ["in", parent_names]} - # If stage user selects 'Pending', we filter the parent by 'Pending' too. - # If they select 'Responded', we already have the parents from 'responded_records'. - if status and status != 'Responded': - p_filters["status"] = status + + if status_list: + actual_statuses = [s for s in status_list if s != 'Responded'] + if actual_statuses: + p_filters["status"] = ["in", actual_statuses] + + if risk_list: + p_filters["risk"] = ["in", risk_list] pending_for_me_list = frappe.get_all( "My Audits", @@ -75,16 +88,13 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None): item["sr_no"] = idx # 2. 🔵 FETCH GLOBAL/ROLE STATS - # -------------------------------------------- from audit_management.audit_management.utils import get_user_allowed_divisions allowed_divisions = get_user_allowed_divisions(user) filters = {} if is_admin: - # ✅ Administrator / System Manager sees EVERYTHING pass elif is_manager: - # ✅ RESTRICT BY DIVISION FOR AUDIT MANAGER if allowed_divisions: filters["emp_division"] = ["in", allowed_divisions] else: @@ -92,24 +102,23 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None): elif is_member: filters["owner"] = user else: - # For Stage Users (no manager/member role) if allowed_divisions: filters["emp_division"] = ["in", allowed_divisions] else: filters["emp_division"] = "None" total_pending = frappe.db.count("My Audits", {**filters, "status": "Pending"}) - high_risk = frappe.db.count("My Audits", {**filters, "risk": "High"}) closed_count = frappe.db.count("My Audits", {**filters, "status": "Close"}) draft_count = frappe.db.count("My Audits", {**filters, "status": "Draft"}) recent_list = [] has_more_recent = False if is_manager or is_member: - # Apply status filter if provided r_filters = filters.copy() - if status: - r_filters["status"] = status + if status_list: + r_filters["status"] = ["in", status_list] + if risk_list: + r_filters["risk"] = ["in", risk_list] recent_list = frappe.get_all( "My Audits", @@ -128,8 +137,7 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None): for idx, item in enumerate(recent_list, start=recent_start + 1): item["sr_no"] = idx - # 3. 🟣 ENHANCE BRANCH COLUMN WITH SOL ID - # -------------------------------------------- + # 3. 🟣 ENHANCE BRANCH COLUMN all_lists = pending_for_me_list + recent_list if all_lists: audit_levels = list(set([i.emp_branch for i in all_lists if i.emp_branch])) @@ -156,7 +164,6 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None): "pending_for_me": pending_for_me_count, "responded_by_me": responded_by_me_count, "total_pending": total_pending, - "high_risk": high_risk, "closed_count": closed_count, "draft_count": draft_count, "pending_list": pending_for_me_list, @@ -170,214 +177,20 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None): frappe.log_error(frappe.get_traceback(), "Dashboard Stats Error") return {"success": False} - except Exception: - frappe.log_error(frappe.get_traceback(), "Dashboard Error") - return {"success": False} - @frappe.whitelist() def get_my_responded_records(): user = frappe.session.user - - records = frappe.db.sql(""" - SELECT DISTINCT parent - FROM `tabAudit Items` - WHERE status = 'Responded' - AND (user_id = %s OR email = %s) - """, (user, user), as_dict=True) - + records = frappe.db.sql("SELECT DISTINCT parent FROM `tabAudit Items` WHERE status = 'Responded' AND (user_id = %s OR email = %s)", (user, user), as_dict=True) return [r.parent for r in records] @frappe.whitelist() def get_my_pending_records(): user = frappe.session.user - - records = frappe.db.sql(""" - SELECT DISTINCT parent - FROM `tabAudit Items` - WHERE status = 'Pending' - AND (user_id = %s OR email = %s) - """, (user, user), as_dict=True) - + records = frappe.db.sql("SELECT DISTINCT parent FROM `tabAudit Items` WHERE status = 'Pending' AND (user_id = %s OR email = %s)", (user, user), as_dict=True) return [r.parent for r in records] - -def update_custom_block(): - new_html = """ -
-
-
-

Audit Overview

-

Real-time tracking and operational metrics

-
-
- -
-
-
-
- - - Pending For Me -
-
- -
-
-
- - - Total Pending -
-
- -
-
-
- - - High Risk -
-
- -
', 0]})\"> -
-
- - - TAT Breached -
-
-
- - -
- -
- - - -
-
-
- """ - - new_script = """ - (function() { - console.log("Audit Dashboard Script Initialized"); - - function refresh_stats() { - frappe.call({ - method: 'audit_management.audit_management.dashboard.get_dashboard_stats', - callback: (r) => { - if (r.message && r.message.success) { - const m = r.message; - const update_val = (id, val) => { - const el = document.getElementById(id); - if (el) el.innerText = val; - }; - update_val('stat-pending', m.pending_for_me); - update_val('stat-total', m.total_pending); - update_val('stat-high', m.high_risk); - update_val('stat-tat', m.tat_breached); - - // Highlight Pending Items - const pendingSection = document.getElementById('pending-action-section'); - const pendingList = document.getElementById('pending-items-list'); - - if (m.pending_list && m.pending_list.length > 0) { - pendingSection.style.display = 'block'; - pendingList.innerHTML = m.pending_list.map(item => ` -
-
- ${item.name} - ${item.audit_query_subject_box || 'No Subject'} -
-
- ${item.risk} Risk - -
-
- `).join(''); - } else { - pendingSection.style.display = 'none'; - } - } - } - }); - } - - refresh_stats(); - setTimeout(refresh_stats, 1500); - - $(document).on('workspace_render', function() { - refresh_stats(); - }); - })(); - """ - - new_style = """ - .modern-audit-dashboard { font-family: 'Inter', -apple-system, sans-serif; padding: 20px; background: #fbfcfe; border-radius: 16px; border: 1px solid #e2e8f0; } - .header-section { margin-bottom: 25px; } - .main-title { font-size: 22px; font-weight: 800; color: #1a202c; margin: 0; } - .sub-title { font-size: 14px; color: #718096; margin-top: 4px; } - - .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; margin-bottom: 35px; } - .stat-card { display: flex; align-items: center; gap: 18px; padding: 24px; border-radius: 16px; color: white; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } - .stat-card:hover { transform: translateY(-5px); box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); } - - .stat-card.blue { background: linear-gradient(135deg, #3b82f6, #2563eb); } - .stat-card.purple { background: linear-gradient(135deg, #8b5cf6, #6d28d9); } - .stat-card.red { background: linear-gradient(135deg, #ef4444, #dc2626); } - .stat-card.orange { background: linear-gradient(135deg, #f59e0b, #d97706); } - - .icon-box { font-size: 32px; opacity: 0.8; } - .stat-info .val { display: block; font-size: 30px; font-weight: 800; line-height: 1; margin-bottom: 4px; } - .stat-info .lbl { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.9; } - - .section-label { font-size: 14px; font-weight: 700; color: #4a5568; text-transform: uppercase; margin-bottom: 16px; letter-spacing: 1px; } - .actions-flex { display: flex; gap: 12px; flex-wrap: wrap; } - - .btn-modern { display: flex; align-items: center; gap: 10px; padding: 12px 24px; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; border: none; } - .btn-modern.primary { background: #1a202c; color: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } - .btn-modern.primary:hover { background: #2d3748; transform: scale(1.02); } - .btn-modern.secondary { background: white; color: #4a5568; border: 1px solid #e2e8f0; } - .btn-modern.secondary:hover { background: #edf2f7; border-color: #cbd5e0; } - - .pending-items-container { display: flex; flex-direction: column; gap: 10px; } - .pending-item-card { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; background: white; border-radius: 12px; border-left: 5px solid #cbd5e0; box-shadow: 0 1px 3px rgba(0,0,0,0.1); cursor: pointer; transition: 0.2s; } - .pending-item-card:hover { transform: translateX(5px); box-shadow: 0 4px 6px rgba(0,0,0,0.05); } - .pending-item-card.high { border-left-color: #ef4444; background: #fff5f5; } - .pending-item-card.medium { border-left-color: #f59e0b; } - .pending-item-card.normal { border-left-color: #3b82f6; } - - .item-info { display: flex; flex-direction: column; } - .item-name { font-weight: 700; font-size: 14px; color: #2d3748; } - .item-subject { font-size: 13px; color: #718096; } - - .item-meta { display: flex; align-items: center; gap: 15px; } - .risk-badge { font-size: 11px; font-weight: 700; padding: 4px 8px; border-radius: 20px; text-transform: uppercase; } - .high .risk-badge { background: #fee2e2; color: #b91c1c; } - .medium .risk-badge { background: #fef3c7; color: #92400e; } - .normal .risk-badge { background: #dbeafe; color: #1e40af; } - - @media (max-width: 768px) { - .stats-grid { grid-template-columns: 1fr 1fr; } - .btn-modern { width: 100%; justify-content: center; } - } - """ - - if frappe.db.exists("Custom HTML Block", "Audit Management"): - doc = frappe.get_doc("Custom HTML Block", "Audit Management") - if doc.html != new_html or doc.script != new_script: - doc.html = new_html - doc.script = new_script - doc.style = new_style - doc.save(ignore_permissions=True) - frappe.db.commit() +def update_custom_block(): + # Helper to force update the Custom HTML Block from code + doc = frappe.get_doc("Custom HTML Block", "Audit Management") + # This function will be called manually or via patch if needed + pass diff --git a/audit_management/fixtures/custom_html_block.json b/audit_management/fixtures/custom_html_block.json index 5ed3a45..e2d1df8 100644 --- a/audit_management/fixtures/custom_html_block.json +++ b/audit_management/fixtures/custom_html_block.json @@ -2,11 +2,12 @@ { "docstatus": 0, "doctype": "Custom HTML Block", - "html": "
\n
\n
\n

Audit Management

\n
\n
\n
\n \n
\n \n Create Audit\n
\n
\n
\n\n \n
\n
\n
Total Records
\n
\n
-
\n \n
\n
\n
\n
\n
Draft
\n
\n
-
\n \n
\n
\n
\n
\n
Pending
\n
\n
-
\n \n
\n
\n
\n
\n
Closed
\n
\n
-
\n \n
\n
\n
\n
\n\n \n
\n
\n
\n \n Filter by Status\n
\n
\n \n \n
\n
\n \n
\n\n \n \n\n \n
", + "html": "
\n
\n
\n

Audit Management

\n
\n
\n
\n \n
\n \n Create Audit\n
\n
\n
\n\n \n
\n
\n
Total Records
\n
\n
-
\n \n
\n
\n
\n
\n
Draft
\n
\n
-
\n \n
\n
\n
\n
\n
Pending
\n
\n
-
\n \n
\n
\n
\n
\n
Closed
\n
\n
-
\n \n
\n
\n
\n
\n\n \n
\n
\n
\n \n Status\n
\n
\n
\n All\n \n
\n
\n
\n
\n
\n
\n \n Risk\n
\n
\n
\n All\n \n
\n
\n
\n
\n \n
\n\n \n \n\n \n
", "modified": "2026-05-19 12:00:00.000000", "name": "Audit Management", "private": 0, "roles": [], - "script": "(function () {\n const root = root_element || document;\n let userRole = '';\n let pendingStart = 0;\n let recentStart = 0;\n let currentStatusFilter = '';\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.addEventListener('click', () => {\n const dd = root.querySelector('#actions-dropdown');\n if (dd) dd.style.display = 'none';\n });\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name}\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]}\n `).join('');\n\n window.load_more_data = (type) => {\n const args = {\n pending_start: type === 'pending' ? pendingStart + 10 : pendingStart,\n recent_start: type === 'recent' ? recentStart + 10 : recentStart,\n status: currentStatusFilter\n };\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: args,\n callback: function (r) {\n if (!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 window.handle_status_filter = (val) => {\n currentStatusFilter = val;\n const clearBtn = root.querySelector('#clear-filter-btn');\n if (val) clearBtn.style.display = 'flex';\n else clearBtn.style.display = 'none';\n refresh(val);\n };\n\n window.clear_filters = () => {\n const select = root.querySelector('#status-filter');\n if (select) select.value = '';\n currentStatusFilter = '';\n root.querySelector('#clear-filter-btn').style.display = 'none';\n refresh('');\n };\n\n function refresh(status = '') {\n pendingStart = 0; recentStart = 0;\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: { pending_start: 0, recent_start: 0, status: status },\n callback: function (r) {\n if (!r.message?.success) return;\n const d = r.message; \n \n userRole = d.role_type;\n\n // Visibility Control\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n const statusFilter = root.querySelector('#status-filter');\n \n if (userRole === 'stage_user') {\n if (masterCapsules) masterCapsules.style.display = 'none';\n if (draftCard) draftCard.style.display = 'none';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n } else {\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (draftCard) draftCard.style.display = 'block';\n if (statusFilter) {\n statusFilter.innerHTML = `\n \n \n \n \n `;\n statusFilter.value = currentStatusFilter;\n }\n }\n\n\n if (userRole === 'stage_user') {\n upd('val-total', (d.pending_for_me || 0) + (d.responded_by_me || 0));\n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n } else {\n upd('val-total', (d.draft_count || 0) + (d.total_pending || 0) + (d.closed_count || 0));\n upd('val-draft', d.draft_count);\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n }\n\n const stageView = root.querySelector('#stage-view');\n const stageItems = root.querySelector('#stage-items');\n const pendingMore = root.querySelector('#pending-more-btn');\n\n if (d.pending_list && d.pending_list.length > 0) {\n stageView.style.display = 'block';\n stageItems.innerHTML = renderRows(d.pending_list);\n if (pendingMore) pendingMore.style.display = d.has_more_pending ? 'flex' : 'none';\n } else {\n stageView.style.display = 'none';\n }\n\n const managerView = root.querySelector('#manager-view');\n const activityBody = root.querySelector('#activity-body');\n const recentMore = root.querySelector('#recent-more-btn');\n\n if (userRole !== 'stage_user' && d.recent_list && d.recent_list.length > 0) {\n managerView.style.display = 'block';\n activityBody.innerHTML = renderRows(d.recent_list);\n if (recentMore) recentMore.style.display = d.has_more_recent ? 'flex' : 'none';\n } else {\n managerView.style.display = 'none';\n }\n }\n });\n }\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_pending_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Pending' });\n };\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_responded_records', callback: r => r.message?.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Close' });\n };\n\n $(document).on('workspace_render', () => refresh(currentStatusFilter)); refresh(currentStatusFilter);\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: wrap; gap: 15px; }\n.title-wrap { display: flex; align-items: center; gap: 10px; }\n.db-title { font-size: 22px; font-weight: 800; color: #0f172a; margin: 0; }\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: 8px; flex-wrap: wrap; }\n.master-capsule { background: #ffffff; padding: 6px 14px; border-radius: 50px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }\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: 14px; }\n.master-capsule span { font-size: 11px; font-weight: 700; color: #475569; white-space: nowrap; }\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.dropdown-item i { width: 16px; text-align: center; }\n\n.create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; }\n.create-btn-capsule i { color: white !important; }\n.create-btn-capsule span { color: white !important; }\n.create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.05); }\n\n.grid-compact { display: grid; gap: 12px; margin-bottom: 24px; }\n.stats-grid { grid-template-columns: repeat(4, 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: 12px 15px; border-radius: 14px; border: 1px solid #e2e8f0; position: relative; overflow: hidden; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.compact-stat-card:hover { border-color: #cbd5e1; box-shadow: 0 8px 10px -1px rgba(0,0,0,0.05); }\n.stat-label-small { font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.6px; }\n.stat-row { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }\n.stat-val-small { font-size: 22px; font-weight: 800; color: #0f172a; }\n.icon-stat { font-size: 20px; opacity: 0.8; }\n\n.accent-bar { position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; }\n.blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; } .red-bg { background: #ef4444; }\n\n.filter-row-container { display: flex; align-items: center; justify-content: space-between; background: #ffffff; padding: 12px 20px; border-radius: 12px; border: 1px solid #e2e8f0; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.filter-group { display: flex; align-items: center; gap: 15px; }\n.filter-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 800; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; }\n.filter-label i { color: #3b82f6; font-size: 14px; }\n.select-wrapper { position: relative; display: flex; align-items: center; }\n.status-select { appearance: none; background: #f8fafc; border: 1px solid #e2e8f0; padding: 6px 35px 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 700; color: #1e293b; cursor: pointer; transition: 0.2s; min-width: 140px; }\n.status-select:hover { border-color: #3b82f6; background: #ffffff; }\n.status-select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }\n.select-arrow { position: absolute; right: 12px; pointer-events: none; font-size: 10px; color: #64748b; }\n.clear-btn { display: flex; align-items: center; gap: 6px; background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; }\n.clear-btn:hover { background: #fecaca; transform: translateY(-1px); }\n\n.load-more-btn { background: #ffffff; color: #475569; border: 1px solid #e2e8f0; padding: 8px 24px; border-radius: 50px; cursor: pointer; font-size: 12px; 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.list-section { margin-top: 20px; }\n.list-header { font-size: 12px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 12px; letter-spacing: 0.8px; }\n.table-light-wrap { background: #ffffff; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\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; letter-spacing: 0.5px; }\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; pointer-events: none; }\n.t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }\n.t-status.close { 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.mini-table th:first-child, .mini-table td:first-child { width: 60px; text-align: center; font-weight: 700; color: #64748b; }\n\n@media (max-width: 768px) {\n .db-header { flex-direction: column; align-items: flex-start; }\n .master-capsule-container { width: 100%; }\n .stats-grid { grid-template-columns: 1fr; }\n .filter-row-container { flex-direction: column; gap: 12px; align-items: flex-start; }\n .filter-group { width: 100%; }\n .select-wrapper { width: 100%; }\n .status-select { width: 100%; }\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\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 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 if (selected.length === 0) label.innerText = 'All';\n else if (selected.length === 1) label.innerText = selected[0] === 'Close' ? 'Closed' : selected[0];\n else label.innerText = selected.length + ' Selected';\n\n root.querySelector('#clear-filter-btn').style.display = (currentStatusFilter.length > 0 || currentRiskFilter.length > 0) ? 'flex' : 'none';\n refresh();\n };\n\n window.clear_filters = () => {\n root.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = false);\n currentStatusFilter = []; currentRiskFilter = [];\n root.querySelector('#selected-status-label').innerText = 'All';\n root.querySelector('#selected-risk-label').innerText = 'All';\n root.querySelector('#clear-filter-btn').style.display = 'none';\n refresh();\n };\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name}\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]}\n `).join('');\n\n window.load_more_data = (type) => {\n const 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 };\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: args,\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: { pending_start: 0, recent_start: 0, status: currentStatusFilter.join(','), risk: currentRiskFilter.join(',') },\n callback: function (r) {\n if (!r.message || !r.message.success) return;\n const d = r.message; userRole = d.role_type;\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'] : ['Draft', 'Pending', 'Close'];\n statusDropdown.innerHTML = statusOps.map(opt => `
${opt === 'Close' ? 'Closed' : opt}
`).join('');\n \n const riskOps = ['High', 'Medium', 'Normal'];\n riskDropdown.innerHTML = riskOps.map(opt => `
${opt}
`).join('');\n }\n\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n \n if (userRole === 'stage_user') {\n if (masterCapsules) masterCapsules.style.display = 'none';\n if (draftCard) draftCard.style.display = 'none';\n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n } else {\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (draftCard) draftCard.style.display = 'block';\n upd('val-draft', d.draft_count);\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n }\n upd('val-total', (d.draft_count || 0) + (d.total_pending || 0) + (d.closed_count || 0));\n\n const stageView = root.querySelector('#stage-view');\n const stageItems = root.querySelector('#stage-items');\n if (d.pending_list && d.pending_list.length > 0) {\n stageView.style.display = 'block';\n stageItems.innerHTML = renderRows(d.pending_list);\n } else stageView.style.display = 'none';\n\n const managerView = root.querySelector('#manager-view');\n const activityBody = root.querySelector('#activity-body');\n if (userRole !== 'stage_user' && d.recent_list && d.recent_list.length > 0) {\n managerView.style.display = 'block';\n activityBody.innerHTML = renderRows(d.recent_list);\n } else managerView.style.display = 'none';\n }\n });\n }\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_pending_records', callback: r => r.message && r.message.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Pending' });\n };\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_responded_records', callback: r => r.message && r.message.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Close' });\n };\n\n $(document).on('workspace_render', refresh); refresh();\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: wrap; gap: 15px; }\n.title-wrap { display: flex; align-items: center; gap: 10px; }\n.db-title { font-size: 22px; font-weight: 800; color: #0f172a; margin: 0; }\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: 8px; flex-wrap: wrap; }\n.master-capsule { background: #ffffff; padding: 6px 14px; border-radius: 50px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }\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: 14px; }\n.master-capsule span { font-size: 11px; font-weight: 700; color: #475569; white-space: nowrap; }\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.dropdown-item i { width: 16px; text-align: center; }\n\n.create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; }\n.create-btn-capsule i { color: white !important; }\n.create-btn-capsule span { color: white !important; }\n.create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.05); }\n\n.grid-compact { display: grid; gap: 12px; margin-bottom: 24px; }\n.stats-grid { grid-template-columns: repeat(4, 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: 12px 15px; border-radius: 14px; border: 1px solid #e2e8f0; position: relative; overflow: hidden; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.compact-stat-card:hover { border-color: #cbd5e1; box-shadow: 0 8px 10px -1px rgba(0,0,0,0.05); }\n.stat-label-small { font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.6px; }\n.stat-row { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }\n.stat-val-small { font-size: 22px; font-weight: 800; color: #0f172a; }\n.icon-stat { font-size: 20px; opacity: 0.8; }\n\n.accent-bar { position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; }\n.blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; } .red-bg { background: #ef4444; }\n\n.filter-row-container { display: flex; align-items: center; justify-content: space-between; background: #ffffff; padding: 12px 20px; border-radius: 12px; border: 1px solid #e2e8f0; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.filter-group { display: flex; align-items: center; gap: 15px; }\n.filter-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 800; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; }\n.filter-label i { color: #3b82f6; font-size: 14px; }\n\n.multiselect-container { position: relative; min-width: 160px; }\n.multiselect-btn { background: #f8fafc; border: 1px solid #e2e8f0; padding: 8px 12px; border-radius: 8px; font-size: 12px; font-weight: 700; color: #1e293b; cursor: pointer; display: flex; justify-content: space-between; align-items: center; width: 100%; transition: 0.2s; }\n.multiselect-btn:hover { border-color: #3b82f6; background: #ffffff; }\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; width: 100%; 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.clear-btn { display: flex; align-items: center; gap: 6px; background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; }\n.clear-btn:hover { background: #fecaca; transform: translateY(-1px); }\n\n.load-more-btn { background: #ffffff; color: #475569; border: 1px solid #e2e8f0; padding: 8px 24px; border-radius: 50px; cursor: pointer; font-size: 12px; 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.list-section { margin-top: 20px; }\n.list-header { font-size: 12px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 12px; letter-spacing: 0.8px; }\n.table-light-wrap { background: #ffffff; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\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; letter-spacing: 0.5px; }\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; pointer-events: none; }\n.t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }\n.t-status.close { 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.mini-table th:first-child, .mini-table td:first-child { width: 60px; text-align: center; font-weight: 700; color: #64748b; }\n\n@media (max-width: 768px) {\n .db-header { flex-direction: column; align-items: flex-start; }\n .master-capsule-container { width: 100%; }\n .stats-grid { grid-template-columns: 1fr; }\n .filter-row-container { flex-direction: column; gap: 12px; align-items: flex-start; }\n .filter-group { width: 100%; }\n}" + } ] \ No newline at end of file From 4436c7a5c85c134600dcdf7958e8cad24bc82f84 Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Wed, 20 May 2026 18:00:26 +0530 Subject: [PATCH 6/7] style: relocate dashboard filters to the header master-capsule container --- 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 e2d1df8..155ee80 100644 --- a/audit_management/fixtures/custom_html_block.json +++ b/audit_management/fixtures/custom_html_block.json @@ -2,7 +2,7 @@ { "docstatus": 0, "doctype": "Custom HTML Block", - "html": "
\n
\n
\n

Audit Management

\n
\n
\n
\n \n
\n \n Create Audit\n
\n
\n
\n\n \n
\n
\n
Total Records
\n
\n
-
\n \n
\n
\n
\n
\n
Draft
\n
\n
-
\n \n
\n
\n
\n
\n
Pending
\n
\n
-
\n \n
\n
\n
\n
\n
Closed
\n
\n
-
\n \n
\n
\n
\n
\n\n \n
\n
\n
\n \n Status\n
\n
\n
\n All\n \n
\n
\n
\n
\n
\n
\n \n Risk\n
\n
\n
\n All\n \n
\n
\n
\n
\n \n
\n\n \n \n\n \n
", + "html": "
\n
\n
\n

Audit Management

\n
\n
\n
\n \n
\n
\n \n Status\n \n
\n
\n
\n\n \n
\n
\n \n Risk\n \n
\n
\n
\n\n \n \n\n \n
\n \n Create Audit\n
\n
\n
\n\n \n
\n
\n
Total Records
\n
\n
-
\n \n
\n
\n
\n
\n
Draft
\n
\n
-
\n \n
\n
\n
\n
\n
Pending
\n
\n
-
\n \n
\n
\n
\n
\n
Closed
\n
\n
-
\n \n
\n
\n
\n
\n\n \n \n\n \n
", "modified": "2026-05-19 12:00:00.000000", "name": "Audit Management", "private": 0, From 02148a5450361db465240d4a168815788a99e3c0 Mon Sep 17 00:00:00 2001 From: Rishabh Rahangdale Date: Thu, 21 May 2026 10:59:01 +0530 Subject: [PATCH 7/7] feat: optimize dashboard filtering logic, header layout, and load more button styling --- audit_management/fixtures/custom_html_block.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/audit_management/fixtures/custom_html_block.json b/audit_management/fixtures/custom_html_block.json index 155ee80..64949d7 100644 --- a/audit_management/fixtures/custom_html_block.json +++ b/audit_management/fixtures/custom_html_block.json @@ -2,12 +2,12 @@ { "docstatus": 0, "doctype": "Custom HTML Block", - "html": "
\n
\n
\n

Audit Management

\n
\n
\n
\n \n
\n
\n \n Status\n \n
\n
\n
\n\n \n
\n
\n \n Risk\n \n
\n
\n
\n\n \n \n\n \n
\n \n Create Audit\n
\n
\n
\n\n \n
\n
\n
Total Records
\n
\n
-
\n \n
\n
\n
\n
\n
Draft
\n
\n
-
\n \n
\n
\n
\n
\n
Pending
\n
\n
-
\n \n
\n
\n
\n
\n
Closed
\n
\n
-
\n \n
\n
\n
\n
\n\n \n \n\n \n
", + "html": "
\n
\n
\n

Audit Management

\n
\n
\n
\n \n
\n
\n \n Status\n \n
\n
\n
\n\n \n
\n
\n \n Risk\n \n
\n
\n
\n\n \n \n\n \n
\n \n Create Audit\n
\n
\n
\n\n \n
\n
\n
Total Records
\n
\n
-
\n \n
\n
\n
\n
\n
Draft
\n
\n
-
\n \n
\n
\n
\n
\n
Pending
\n
\n
-
\n \n
\n
\n
\n
\n
Closed
\n
\n
-
\n \n
\n
\n
\n
\n\n \n \n\n \n
", "modified": "2026-05-19 12:00:00.000000", "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\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 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 if (selected.length === 0) label.innerText = 'All';\n else if (selected.length === 1) label.innerText = selected[0] === 'Close' ? 'Closed' : selected[0];\n else label.innerText = selected.length + ' Selected';\n\n root.querySelector('#clear-filter-btn').style.display = (currentStatusFilter.length > 0 || currentRiskFilter.length > 0) ? 'flex' : 'none';\n refresh();\n };\n\n window.clear_filters = () => {\n root.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = false);\n currentStatusFilter = []; currentRiskFilter = [];\n root.querySelector('#selected-status-label').innerText = 'All';\n root.querySelector('#selected-risk-label').innerText = 'All';\n root.querySelector('#clear-filter-btn').style.display = 'none';\n refresh();\n };\n\n const renderRows = (list) => list.map(i => `\n \n ${i.sr_no}\n ${i.name}\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]}\n `).join('');\n\n window.load_more_data = (type) => {\n const 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 };\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: args,\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: { pending_start: 0, recent_start: 0, status: currentStatusFilter.join(','), risk: currentRiskFilter.join(',') },\n callback: function (r) {\n if (!r.message || !r.message.success) return;\n const d = r.message; userRole = d.role_type;\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'] : ['Draft', 'Pending', 'Close'];\n statusDropdown.innerHTML = statusOps.map(opt => `
${opt === 'Close' ? 'Closed' : opt}
`).join('');\n \n const riskOps = ['High', 'Medium', 'Normal'];\n riskDropdown.innerHTML = riskOps.map(opt => `
${opt}
`).join('');\n }\n\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n \n if (userRole === 'stage_user') {\n if (masterCapsules) masterCapsules.style.display = 'none';\n if (draftCard) draftCard.style.display = 'none';\n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n } else {\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (draftCard) draftCard.style.display = 'block';\n upd('val-draft', d.draft_count);\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n }\n upd('val-total', (d.draft_count || 0) + (d.total_pending || 0) + (d.closed_count || 0));\n\n const stageView = root.querySelector('#stage-view');\n const stageItems = root.querySelector('#stage-items');\n if (d.pending_list && d.pending_list.length > 0) {\n stageView.style.display = 'block';\n stageItems.innerHTML = renderRows(d.pending_list);\n } else stageView.style.display = 'none';\n\n const managerView = root.querySelector('#manager-view');\n const activityBody = root.querySelector('#activity-body');\n if (userRole !== 'stage_user' && d.recent_list && d.recent_list.length > 0) {\n managerView.style.display = 'block';\n activityBody.innerHTML = renderRows(d.recent_list);\n } else managerView.style.display = 'none';\n }\n });\n }\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_pending_records', callback: r => r.message && r.message.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Pending' });\n };\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_responded_records', callback: r => r.message && r.message.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Close' });\n };\n\n $(document).on('workspace_render', refresh); refresh();\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: wrap; gap: 15px; }\n.title-wrap { display: flex; align-items: center; gap: 10px; }\n.db-title { font-size: 22px; font-weight: 800; color: #0f172a; margin: 0; }\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: 8px; flex-wrap: wrap; }\n.master-capsule { background: #ffffff; padding: 6px 14px; border-radius: 50px; border: 1px solid #e2e8f0; display: flex; align-items: center; gap: 8px; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }\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: 14px; }\n.master-capsule span { font-size: 11px; font-weight: 700; color: #475569; white-space: nowrap; }\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.dropdown-item i { width: 16px; text-align: center; }\n\n.create-btn-capsule { background: #2563eb !important; color: white !important; border: none !important; }\n.create-btn-capsule i { color: white !important; }\n.create-btn-capsule span { color: white !important; }\n.create-btn-capsule:hover { background: #1d4ed8 !important; transform: scale(1.05); }\n\n.grid-compact { display: grid; gap: 12px; margin-bottom: 24px; }\n.stats-grid { grid-template-columns: repeat(4, 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: 12px 15px; border-radius: 14px; border: 1px solid #e2e8f0; position: relative; overflow: hidden; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.compact-stat-card:hover { border-color: #cbd5e1; box-shadow: 0 8px 10px -1px rgba(0,0,0,0.05); }\n.stat-label-small { font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.6px; }\n.stat-row { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }\n.stat-val-small { font-size: 22px; font-weight: 800; color: #0f172a; }\n.icon-stat { font-size: 20px; opacity: 0.8; }\n\n.accent-bar { position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; }\n.blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; } .red-bg { background: #ef4444; }\n\n.filter-row-container { display: flex; align-items: center; justify-content: space-between; background: #ffffff; padding: 12px 20px; border-radius: 12px; border: 1px solid #e2e8f0; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.filter-group { display: flex; align-items: center; gap: 15px; }\n.filter-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 800; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; }\n.filter-label i { color: #3b82f6; font-size: 14px; }\n\n.multiselect-container { position: relative; min-width: 160px; }\n.multiselect-btn { background: #f8fafc; border: 1px solid #e2e8f0; padding: 8px 12px; border-radius: 8px; font-size: 12px; font-weight: 700; color: #1e293b; cursor: pointer; display: flex; justify-content: space-between; align-items: center; width: 100%; transition: 0.2s; }\n.multiselect-btn:hover { border-color: #3b82f6; background: #ffffff; }\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; width: 100%; 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.clear-btn { display: flex; align-items: center; gap: 6px; background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 11px; font-weight: 800; transition: 0.2s; }\n.clear-btn:hover { background: #fecaca; transform: translateY(-1px); }\n\n.load-more-btn { background: #ffffff; color: #475569; border: 1px solid #e2e8f0; padding: 8px 24px; border-radius: 50px; cursor: pointer; font-size: 12px; 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.list-section { margin-top: 20px; }\n.list-header { font-size: 12px; font-weight: 800; color: #64748b; text-transform: uppercase; margin-bottom: 12px; letter-spacing: 0.8px; }\n.table-light-wrap { background: #ffffff; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\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; letter-spacing: 0.5px; }\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; pointer-events: none; }\n.t-status.pending { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }\n.t-status.close { 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.mini-table th:first-child, .mini-table td:first-child { width: 60px; text-align: center; font-weight: 700; color: #64748b; }\n\n@media (max-width: 768px) {\n .db-header { flex-direction: column; align-items: flex-start; }\n .master-capsule-container { width: 100%; }\n .stats-grid { grid-template-columns: 1fr; }\n .filter-row-container { flex-direction: column; gap: 12px; align-items: flex-start; }\n .filter-group { width: 100%; }\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\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 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] === 'Close' ? 'Closed' : 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 refresh();\n };\n\n window.clear_filters = () => {\n root.querySelectorAll('input[type=checkbox]').forEach(cb => cb.checked = false);\n currentStatusFilter = []; currentRiskFilter = [];\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}\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]}\n `).join('');\n\n window.load_more_data = (type) => {\n const 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 };\n frappe.call({\n method: 'audit_management.audit_management.dashboard.get_dashboard_stats',\n args: args,\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: { pending_start: 0, recent_start: 0, status: currentStatusFilter.join(','), risk: currentRiskFilter.join(',') },\n callback: function (r) {\n if (!r.message || !r.message.success) return;\n const d = r.message; userRole = d.role_type;\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'] : ['Draft', 'Pending', 'Close'];\n statusDropdown.innerHTML = statusOps.map(opt => `
${opt === 'Close' ? 'Closed' : opt}
`).join('');\n \n const riskOps = ['High', 'Medium', 'Normal'];\n riskDropdown.innerHTML = riskOps.map(opt => `
${opt}
`).join('');\n }\n\n const masterCapsules = root.querySelector('.master-capsule-container');\n const draftCard = root.querySelector('#draft-card');\n \n if (userRole === 'stage_user') {\n if (masterCapsules) masterCapsules.style.display = 'none';\n if (draftCard) draftCard.style.display = 'none';\n upd('val-pending', d.pending_for_me); upd('lbl-pending', 'Pending Me');\n upd('val-closed', d.responded_by_me); upd('lbl-closed', 'Responded');\n } else {\n if (masterCapsules) masterCapsules.style.display = 'flex';\n if (draftCard) draftCard.style.display = 'block';\n upd('val-draft', d.draft_count);\n upd('val-pending', d.total_pending); upd('lbl-pending', 'Total Pending');\n upd('val-closed', d.closed_count); upd('lbl-closed', 'Closed');\n }\n upd('val-total', (d.draft_count || 0) + (d.total_pending || 0) + (d.closed_count || 0));\n\n const stageView = root.querySelector('#stage-view');\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 stageView.style.display = 'block';\n stageItems.innerHTML = renderRows(d.pending_list);\n if (pendingMore) pendingMore.style.display = d.has_more_pending ? 'flex' : 'none';\n } else stageView.style.display = 'none';\n\n const managerView = root.querySelector('#manager-view');\n const activityBody = root.querySelector('#activity-body');\n const recentMore = root.querySelector('#recent-more-btn');\n if (userRole !== 'stage_user' && d.recent_list && d.recent_list.length > 0) {\n managerView.style.display = 'block';\n activityBody.innerHTML = renderRows(d.recent_list);\n if (recentMore) recentMore.style.display = d.has_more_recent ? 'flex' : 'none';\n } else managerView.style.display = 'none';\n }\n });\n }\n\n window.handle_pending_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_pending_records', callback: r => r.message && r.message.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Pending' });\n };\n window.handle_closed_click = () => { \n if (userRole === 'stage_user') frappe.call({ method: 'audit_management.audit_management.dashboard.get_my_responded_records', callback: r => r.message && r.message.length && frappe.set_route('List', 'My Audits', { name: ['in', r.message] }) });\n else frappe.set_route('List', 'My Audits', { status: 'Close' });\n };\n\n $(document).on('workspace_render', refresh); refresh();\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: 12px; margin-bottom: 24px; }\n.stats-grid { grid-template-columns: repeat(4, 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: 12px 15px; border-radius: 14px; border: 1px solid #e2e8f0; position: relative; overflow: hidden; cursor: pointer; transition: 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }\n.stat-label-small { font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.6px; }\n.stat-val-small { font-size: 22px; font-weight: 800; color: #0f172a; }\n.icon-stat { font-size: 20px; opacity: 0.8; }\n.accent-bar { position: absolute; bottom: 0; left: 0; height: 4px; width: 100%; }\n.blue-bg { background: #3b82f6; } .green-bg { background: #10b981; } .purple-bg { background: #8b5cf6; } .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.close { 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}" } ] \ No newline at end of file