diff --git a/audit_management/audit_management/doctype/my_audits/my_audits.js b/audit_management/audit_management/doctype/my_audits/my_audits.js index 093fdba..da65b02 100755 --- a/audit_management/audit_management/doctype/my_audits/my_audits.js +++ b/audit_management/audit_management/doctype/my_audits/my_audits.js @@ -79,8 +79,16 @@ frappe.ui.form.on("My Audits", { date_html += item("Closed", closed_date); } - if (frm.doc.aging !== undefined && frm.doc.aging !== null) { - date_html += item("Aging", `${frm.doc.aging} Days`); + if (frm.doc.creation) { + const created_date = frappe.datetime.str_to_user(frm.doc.creation.split(" ")[0]); + + // Calculate dynamic aging: today - creation date + const creation_date_obj = frappe.datetime.str_to_obj(frm.doc.creation); + const today = new Date(); + const time_diff = today - creation_date_obj; + const dynamic_aging = Math.floor(time_diff / (1000 * 60 * 60 * 24)); + + date_html += item("Aging", `${dynamic_aging} Days`); } date_html += ``; @@ -483,11 +491,14 @@ frappe.ui.form.on("My Audits", { args: { docname: frm.doc.name }, callback: function (r) { if (r.message) { - // Check again if it's already there to be absolutely sure if (frm.page.wrapper.find(".custom-status-tracker").length === 0) { frm.set_intro( `
${r.message}
`, ); + // Initialize Bootstrap tooltips + setTimeout(() => { + frm.page.wrapper.find('[data-toggle="tooltip"]').tooltip(); + }, 300); } } else { frm.is_intro_set = false; @@ -2327,6 +2338,7 @@ function render_interactive_tracker(frm, can_edit) { .pill-pending { background-color: #fef2f2; border: 1px solid #fecaca; color: #b91c1c; } .pill-responded { background-color: #f0fdf4; border: 1px solid #bbf7d0; color: #15803d; } .pill-skipped { background-color: #faf5ff; border: 1px solid #e9d5ff; color: #6b21a8; } + .pill-no-response { background-color: #fff7ed; border: 1px solid #fdba74; color: #c2410c; } .pill-default { background-color: #f3f4f6; border: 1px solid #e5e7eb; color: #374151; } .pill-audit-team { background-color: #eff6ff; border: 1px solid #bfdbfe; color: #1d4ed8; } @@ -2437,7 +2449,9 @@ function render_interactive_tracker(frm, can_edit) { ? "pill-responded" : row.status === "Skipped" ? "pill-skipped" - : "pill-default"; + : row.status === "No Response" + ? "pill-no-response" + : "pill-default"; // Get the best available name for the tooltip let emp_name = @@ -2445,9 +2459,15 @@ function render_interactive_tracker(frm, can_edit) { // Prepare time info (Date + Aging) let time_info = ""; - if (row.status === "Pending" && row.pending_time) { + if ( + (row.status === "Pending" || row.status === "No Response") && + row.pending_time + ) { let d = frappe.datetime.str_to_user(row.pending_time.split(" ")[0]); - time_info = ` | Pending: ${d} (${fmt_age(row.pending_time)})`; + time_info = + row.status === "No Response" + ? ` | No Response Since: ${d} (${fmt_age(row.pending_time)})` + : ` | Pending: ${d} (${fmt_age(row.pending_time)})`; } else if (row.status === "Responded" && row.response_time) { let d = frappe.datetime.str_to_user(row.response_time.split(" ")[0]); time_info = ` | Responded: ${d} (${fmt_age(row.response_time)} ago)`; diff --git a/audit_management/audit_management/doctype/my_audits/my_audits.py b/audit_management/audit_management/doctype/my_audits/my_audits.py old mode 100755 new mode 100644 index 841dd9c..bc27507 --- a/audit_management/audit_management/doctype/my_audits/my_audits.py +++ b/audit_management/audit_management/doctype/my_audits/my_audits.py @@ -249,11 +249,7 @@ def sync_new_to_old(self): prefix = m["prefix"] break - # Debugging log - frappe.log_error( - title="Stage Mapping Debug", - message=f"Stage Name: {row.stage_name} | Found Prefix: {prefix}" - ) + if prefix: self.set(f"{prefix}_name", row.employee_name) @@ -348,30 +344,48 @@ def populate_audit_stages(doc): @frappe.whitelist() def get_status_tracker_html(docname): audit_doc = frappe.get_doc("My Audits", docname) + + from frappe.utils import getdate, nowdate + creation_date = getdate(audit_doc.creation) + today = getdate(nowdate()) + dynamic_aging = (today - creation_date).days def create_status_box(text, color, title): - return f"
{text}
" + return f"
{text}
" html_output = create_status_box( - "AUDIT TEAM", '#1E6EB2', 'Stage 0 : Audit Query') + " --> " + "AUDIT TEAM", '#1E6EB2', f'Stage 0 : Audit Query (Aging: {dynamic_aging} days)') + " --> " for i, row in enumerate(audit_doc.audit_stages): color = "grey" - title = f"Stage {row.stage} : {row.stage_name} - {row.status or 'Waiting'}" + # Include aging in tooltip + title = f"Stage {row.stage} : {row.stage_name} - {row.status or 'Waiting'} (Aging: {dynamic_aging} days)" if row.status == "Pending": color = "red" elif row.status == "Responded": color = "green" elif row.status == "No Response": - color = "#4b0a7d" + color = "#4b0a7d" elif row.status == "Skipped": color = "#ffbe0b" + else: + color = "grey" + + # Constructing Title with specific check for No Response details + status_text = row.status or 'Waiting' + title = f"Stage {row.stage} : {row.stage_name} - {status_text} (Aging: {dynamic_aging} days)" + if row.status == "No Response" and row.pending_time: + from frappe.utils import format_datetime + title += f" | Last Attempt: {format_datetime(row.pending_time, 'dd-MM-yyyy')}" html_output += create_status_box(row.stage_name, color, title) if i < len(audit_doc.audit_stages) - 1: html_output += " --> " + # Log for debugging + frappe.log_error(title="Tracker HTML Output", message=html_output) + return html_output @@ -1251,3 +1265,57 @@ def check_pending_tat(): doc.query_status = "Unresolved - Escalation Exhausted" doc.save(ignore_permissions=True) +import frappe +from frappe.utils import getdate, nowdate, format_datetime +import html + +@frappe.whitelist() +def get_status_tracker_html(docname): + try: + audit_doc = frappe.get_doc("My Audits", docname) + + creation_date = getdate(audit_doc.creation) + today = getdate(nowdate()) + dynamic_aging = (today - creation_date).days + + html_output = """ + +
+ """ + + # Base Node + html_output += f"AUDIT TEAM" + + for row in audit_doc.audit_stages: + status = (row.status or "").strip() + status_class_map = { + "Pending": "tracker-red", + "Responded": "tracker-green", + "No Response": "tracker-purple", + "Skipped": "tracker-yellow", + } + css_class = status_class_map.get(status, "tracker-grey") + + # Tooltip details + title = f"{row.stage_name} : {status or 'Waiting'} (Aging: {dynamic_aging} days)" + if status == "No Response" and row.pending_time: + title += f" | Last Attempt: {format_datetime(row.pending_time, 'dd-MM-yyyy')}" + + safe_title = html.escape(title) + + html_output += f"{row.stage_name}" + + html_output += "
" + return html_output + except Exception as e: + frappe.log_error(f"Tracker Error: {str(e)}") + return f"Error: {str(e)}" diff --git a/audit_management/audit_management/report/my_audits_report/my_audits_report.js b/audit_management/audit_management/report/my_audits_report/my_audits_report.js index 31fb32d..c484742 100755 --- a/audit_management/audit_management/report/my_audits_report/my_audits_report.js +++ b/audit_management/audit_management/report/my_audits_report/my_audits_report.js @@ -3,136 +3,65 @@ /* eslint-disable */ frappe.query_reports["My Audits Report"] = { - filters: [ - { - fieldname: "from_date", - label: "From Date", - fieldtype: "Date", - }, - { - fieldname: "to_date", - label: "To Date", - fieldtype: "Date", - }, - { - fieldname: "emp_branch", - label: "Branch", - fieldtype: "Link", - options: "Audit Level", - }, - { - fieldname: "query_generated_by_empid", - label: "Generated By (Emp ID)", - fieldtype: "Link", - options: "Employee", - get_query: () => { - return { - filters: { - status: "Active", - department: "Audit", - }, - }; - }, - }, - { - fieldname: "query_generated_by_name", - label: "Generated By (Name)", - fieldtype: "Link", - options: "Employee", - get_query: () => { - return { - filters: { - status: "Active", - department: "Audit", - }, - }; - }, - }, - { - fieldname: "query_type", - label: "Query Type", - fieldtype: "Select", - options: "\nAudit Report Compliance\nCritical Compliance", - }, - { - fieldname: "status", - label: "Query Status", - fieldtype: "Select", - options: "\nDraft\nPending\nClosed", - }, - { - fieldname: "bm_user_status", - label: "BM Response Status", - fieldtype: "Select", - options: "\nResponded\nNo response", - }, - { - fieldname: "dh_user_status", - label: "DH Response Status", - fieldtype: "Select", - options: "\nResponded\nNo response", - }, - { - fieldname: "com_user_status", - label: "COM Response Status", - fieldtype: "Select", - options: "\nResponded\nNo response", - }, - { - fieldname: "rm_user_status", - label: "RM Response Status", - fieldtype: "Select", - options: "\nResponded\nNo response", - }, - { - fieldname: "rom_user_status", - label: "ROM Response Status", - fieldtype: "Select", - options: "\nResponded\nNo response", - }, - { - fieldname: "zm_user_status", - label: "ZM Response Status", - fieldtype: "Select", - options: "\nResponded\nNo response", - }, - { - fieldname: "zom_user_status", - label: "ZOM Response Status", - fieldtype: "Select", - options: "\nResponded\nNo response", - }, - { - fieldname: "gm_user_status", - label: "GM Response Status", - fieldtype: "Select", - options: "\nResponded\nNo response", - }, - { - fieldname: "hr_user_status", - label: "HR Response Status", - fieldtype: "Select", - options: "\nResponded\nNo response", - }, - { - fieldname: "coo_user_status", - label: "COO Response Status", - fieldtype: "Select", - options: "\nResponded\nNo response", - }, - { - fieldname: "ceo_user_status", - label: "CEO Response Status", - fieldtype: "Select", - options: "\nResponded\nNo response", - }, - ], + filters: [ + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + }, + { + fieldname: "emp_branch", + label: __("Branch"), + fieldtype: "Link", + options: "Audit Level", + }, + { + fieldname: "query_generated_by_empid", + label: __("Generated By Employee"), + fieldtype: "Link", + options: "Employee", + }, + { + fieldname: "query_type", + label: __("Query Type"), + fieldtype: "Link", + options: "Audit Query Type", + }, + { + fieldname: "status", + label: __("Query Status"), + fieldtype: "Select", + options: "\nDraft\nPending\nClosed", + }, + { + fieldname: "stage_name", + label: __("Audit Stage"), + fieldtype: "Link", + options: "Audit Stage", + }, + { + fieldname: "stage_status", + label: __("Stage Response Status"), + fieldtype: "Select", + options: "\nPending\nResponded\nNo response\nSkipped", + }, + { + fieldname: "stage_employee", + label: __("Stage Employee"), + fieldtype: "Link", + options: "Employee", + }, + ], - // clear filters button - onload: function (report) { - report.page.add_inner_button(__("Clear Filters"), function () { - report.filters.forEach((f) => f.set_value("")); - report.refresh(); - }); - }, + onload: function (report) { + report.page.add_inner_button(__("Clear Filters"), function () { + report.filters.forEach((f) => f.set_value("")); + report.refresh(); + }); + }, }; diff --git a/audit_management/audit_management/report/my_audits_report/my_audits_report.py b/audit_management/audit_management/report/my_audits_report/my_audits_report.py index 0673b38..ea9a799 100755 --- a/audit_management/audit_management/report/my_audits_report/my_audits_report.py +++ b/audit_management/audit_management/report/my_audits_report/my_audits_report.py @@ -5,15 +5,13 @@ get_user_allowed_sol_ids ) - def execute(filters=None): columns = get_columns() data = get_data(filters) return columns, data - def get_columns(): - return [ + columns = [ {"label": "ID", "fieldname": "name", "fieldtype": "Link", "options": "My Audits", "width": 120}, {"label": "Date", "fieldname": "date_formatted", "fieldtype": "Data", "width": 100}, {"label": "Branch", "fieldname": "emp_branch", "fieldtype": "Link", "options": "Audit Level", "width": 100}, @@ -23,143 +21,109 @@ def get_columns(): {"label": "Query Status", "fieldname": "status", "fieldtype": "Select", "width": 130}, {"label": "Subject", "fieldname": "audit_query_subject_box", "fieldtype": "Data", "width": 200}, {"label": "Description", "fieldname": "audit_query_box", "fieldtype": "Data", "width": 300}, - {"label": "BM Name", "fieldname": "bm_name", "fieldtype": "Data", "width": 120}, - {"label": "BM Response", "fieldname": "bm_response_box", "fieldtype": "Data", "width": 180}, - {"label": "DH Name", "fieldname": "dh_name", "fieldtype": "Data", "width": 120}, - {"label": "DH Response", "fieldname": "dh_response_box", "fieldtype": "Data", "width": 180}, - {"label": "COM Name", "fieldname": "com_name", "fieldtype": "Data", "width": 120}, - {"label": "COM Response", "fieldname": "com_response_box", "fieldtype": "Data", "width": 180}, - {"label": "RM Name", "fieldname": "rm_name", "fieldtype": "Data", "width": 120}, - {"label": "RM Response", "fieldname": "rm_response_box", "fieldtype": "Data", "width": 180}, - {"label": "ROM Name", "fieldname": "rom_name", "fieldtype": "Data", "width": 120}, - {"label": "ROM Response", "fieldname": "rom_response_box", "fieldtype": "Data", "width": 180}, - {"label": "ZM Name", "fieldname": "zm_name", "fieldtype": "Data", "width": 120}, - {"label": "ZM Response", "fieldname": "zm_response_box", "fieldtype": "Data", "width": 180}, - {"label": "ZOM Name", "fieldname": "zom_name", "fieldtype": "Data", "width": 120}, - {"label": "ZOM Response", "fieldname": "zom_response_box", "fieldtype": "Data", "width": 180}, - {"label": "GM Name", "fieldname": "gm_name", "fieldtype": "Data", "width": 120}, - {"label": "GM Response", "fieldname": "gm_response_box", "fieldtype": "Data", "width": 180}, - {"label": "HR Name", "fieldname": "hr_name", "fieldtype": "Data", "width": 120}, - {"label": "HR Response", "fieldname": "hr_response_box", "fieldtype": "Data", "width": 180}, - {"label": "COO Name", "fieldname": "coo_name", "fieldtype": "Data", "width": 120}, - {"label": "COO Response", "fieldname": "coo_response_box", "fieldtype": "Data", "width": 180}, - {"label": "CEO Name", "fieldname": "ceo_name", "fieldtype": "Data", "width": 120}, - {"label": "CEO Response", "fieldname": "ceo_response_box", "fieldtype": "Data", "width": 180}, ] + # Fetch all audit stages to add dynamic columns + stages = frappe.get_all("Audit Stage", order_by="creation asc") + for s in stages: + stage_name = s.name + prefix = stage_name.lower().replace(" ", "_").replace("(", "").replace(")", "").replace("/", "_") + + columns.extend([ + {"label": f"{stage_name} Name", "fieldname": f"{prefix}_name", "fieldtype": "Data", "width": 120}, + {"label": f"{stage_name} Response", "fieldname": f"{prefix}_response", "fieldtype": "Data", "width": 180}, + {"label": f"{stage_name} Status", "fieldname": f"{prefix}_status", "fieldtype": "Data", "width": 100} + ]) -def get_data(filters): - conditions = [] - user = frappe.session.user - roles = frappe.get_roles(user) + return columns - # ====================== - # ROLE-BASED PERMISSIONS - # ====================== - - is_audit_manager = "Audit Manager" in roles or "Administrator" in roles or "System Manager" in roles - is_audit_member = "Audit Member" in roles +def get_data(filters): + # Base query filters + conditions = ["1=1"] + params = {} - if is_audit_manager: - # Sees everything, no perm filter needed - pass - elif is_audit_member: - # Audit Member: only own created records - conditions.append( - f"owner = {frappe.db.escape(user)}" - ) - else: - # Other users: Sol ID based access (from Report Preference) - # OR records where they are participants - allowed_sol_ids = get_user_allowed_sol_ids(user) - perm_conds = [f"owner = {frappe.db.escape(user)}"] - - if allowed_sol_ids: - sol_list = ", ".join([frappe.db.escape(str(s)) for s in allowed_sol_ids]) - perm_conds.append(f""" - ( - status != 'Draft' - AND - emp_branch IN ( - SELECT name FROM `tabAudit Level` - WHERE sahayog_branch IN ({sol_list}) - ) - ) - """) - - # Also include where they are assigned (Audit Items) - perm_conds.append(f""" - ( - status != 'Draft' - AND EXISTS ( - SELECT name - FROM `tabAudit Items` - WHERE parent = `tabMy Audits`.name - AND ( - user_id = {frappe.db.escape(user)} - OR email = {frappe.db.escape(user)} - ) - ) - ) - """) - - conditions.append(f"({' OR '.join(perm_conds)})") - - # ====================== - # UI FILTERS - # ====================== + # 1. Standard Filters if filters.get("emp_branch"): - conditions.append(f"emp_branch = {frappe.db.escape(filters.emp_branch)}") - + conditions.append("ma.emp_branch = %(emp_branch)s") + params["emp_branch"] = filters.emp_branch + if filters.get("query_generated_by_empid"): - conditions.append(f"query_generated_by_empid = {frappe.db.escape(filters.query_generated_by_empid)}") - + conditions.append("ma.query_generated_by_empid = %(emp_id)s") + params["emp_id"] = filters.query_generated_by_empid + if filters.get("query_type"): - conditions.append(f"query_type = {frappe.db.escape(filters.query_type)}") - + conditions.append("ma.query_type = %(q_type)s") + params["q_type"] = filters.query_type + if filters.get("status"): - conditions.append(f"status = {frappe.db.escape(filters.status)}") - - for level in ["bm", "dh", "com", "rm", "rom", "zm", "zom", "gm", "hr", "coo", "ceo"]: - if filters.get(f"{level}_user_status"): - status = filters.get(f"{level}_user_status") - if status == "Responded": - conditions.append(f"{level}_response_box IS NOT NULL AND {level}_response_box != ''") - elif status == "No response": - conditions.append(f"({level}_response_box IS NULL OR {level}_response_box = '')") + conditions.append("ma.status = %(status)s") + params["status"] = filters.status + + # 2. Dynamic Child Table Filters (via JOIN) + stage_conditions = [] + if filters.get("stage_name"): + stage_conditions.append("ai.stage_name = %(stage_name)s") + params["stage_name"] = filters.stage_name + + if filters.get("stage_status"): + stage_conditions.append("ai.status = %(stage_status)s") + params["stage_status"] = filters.stage_status + + if filters.get("stage_employee"): + stage_conditions.append("ai.employee = %(stage_employee)s") + params["stage_employee"] = filters.stage_employee - if filters.get("from_date"): - conditions.append(f"DATE(creation) >= {frappe.db.escape(filters.from_date)}") + if stage_conditions: + conditions.append(f"EXISTS (SELECT 1 FROM `tabAudit Items` ai WHERE ai.parent = ma.name AND {' AND '.join(stage_conditions)})") - if filters.get("to_date"): - conditions.append(f"DATE(creation) <= {frappe.db.escape(filters.to_date)}") + # 3. Permissions + user = frappe.session.user + if "Audit Manager" not in frappe.get_roles(user): + allowed_sol_ids = get_user_allowed_sol_ids(user) + if allowed_sol_ids: + sol_list = ", ".join([frappe.db.escape(str(s)) for s in allowed_sol_ids]) + conditions.append(f"ma.emp_branch IN (SELECT name FROM `tabAudit Level` WHERE sahayog_branch IN ({sol_list}))") + else: + conditions.append("ma.owner = %(user)s") + params["user"] = user - where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else "" + where_clause = "WHERE " + " AND ".join(conditions) query = f""" SELECT - name, creation, status, - emp_branch, query_generated_by_empid, query_generated_by_name, query_type, - audit_query_subject_box, audit_query_box, - bm_name, bm_response_box, - dh_name, dh_response_box, - com_name, com_response_box, - rm_name, rm_response_box, - rom_name, rom_response_box, - zm_name, zm_response_box, - zom_name, zom_response_box, - gm_name, gm_response_box, - hr_name, hr_response_box, - coo_name, coo_response_box, - ceo_name, ceo_response_box - FROM `tabMy Audits` + ma.name, ma.creation, ma.status, + ma.emp_branch, ma.query_generated_by_empid, ma.query_generated_by_name, ma.query_type, + ma.audit_query_subject_box, ma.audit_query_box + FROM `tabMy Audits` ma {where_clause} - ORDER BY creation DESC + ORDER BY ma.creation DESC """ - result = frappe.db.sql(query, as_dict=True) + result = frappe.db.sql(query, params, as_dict=True) + + if not result: + return [] + + parent_names = [r.name for r in result] + items = frappe.get_all( + "Audit Items", + filters={"parent": ["in", parent_names]}, + fields=["parent", "stage_name", "employee_name", "response", "status"] + ) + + items_map = {} + for it in items: + if it.parent not in items_map: + items_map[it.parent] = {} + + prefix = it.stage_name.lower().replace(" ", "_").replace("(", "").replace(")", "").replace("/", "_") + items_map[it.parent][f"{prefix}_name"] = it.employee_name + items_map[it.parent][f"{prefix}_response"] = it.response + items_map[it.parent][f"{prefix}_status"] = it.status for row in result: row["date_formatted"] = formatdate(row.creation, "dd-mm-yyyy") + if row.name in items_map: + row.update(items_map[row.name]) return result