diff --git a/audit_management/audit_management/dashboard.py b/audit_management/audit_management/dashboard.py index 4911789..7c52b87 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,13 +102,13 @@ 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"}) + closed_count = frappe.db.count("My Audits", {**filters, "status": "Close"}) high_risk = frappe.db.count("My Audits", {**filters, "risk": "High"}) closed_count = frappe.db.count("My Audits", {**filters, "status": "Closed"}) draft_count = frappe.db.count("My Audits", {**filters, "status": "Draft"}) @@ -106,10 +116,11 @@ def get_dashboard_stats(pending_start=0, recent_start=0, status=None): 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 +139,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 +166,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 +179,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 c5fd066..64949d7 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 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
\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 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})();", - "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 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