From 4688de60553cae92a872f4a4389c783453f4d214 Mon Sep 17 00:00:00 2001 From: Jumana-K Date: Tue, 21 Apr 2026 17:12:37 +0530 Subject: [PATCH 1/6] feat:enhancements --- one_compliance/hooks.py | 4 +- .../doctype/active_task_timer/__init__.py | 0 .../active_task_timer/active_task_timer.js | 5 + .../active_task_timer/active_task_timer.json | 76 ++++++ .../active_task_timer/active_task_timer.py | 5 + .../task_management_tool.html | 2 + .../task_management_tool.js | 155 +++++++----- .../task_management_tool.py | 72 +++++- one_compliance/public/css/one_compliance.css | 68 ++++++ one_compliance/public/js/one_compliance.js | 225 ++++++++++++++++++ 10 files changed, 549 insertions(+), 63 deletions(-) create mode 100644 one_compliance/one_compliance/doctype/active_task_timer/__init__.py create mode 100644 one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.js create mode 100644 one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.json create mode 100644 one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.py create mode 100644 one_compliance/public/css/one_compliance.css create mode 100644 one_compliance/public/js/one_compliance.js diff --git a/one_compliance/hooks.py b/one_compliance/hooks.py index 39f06aed..8b081a1b 100644 --- a/one_compliance/hooks.py +++ b/one_compliance/hooks.py @@ -12,8 +12,8 @@ # ------------------ # include js, css files in header of desk.html -# app_include_css = "/assets/one_compliance/css/one_compliance.css" -# app_include_js = "/assets/one_compliance/js/one_compliance.js" +app_include_css = "/assets/one_compliance/css/one_compliance.css" +app_include_js = "/assets/one_compliance/js/one_compliance.js" # include js, css files in header of web template # web_include_css = "/assets/one_compliance/css/one_compliance.css" diff --git a/one_compliance/one_compliance/doctype/active_task_timer/__init__.py b/one_compliance/one_compliance/doctype/active_task_timer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.js b/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.js new file mode 100644 index 00000000..e0536320 --- /dev/null +++ b/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.js @@ -0,0 +1,5 @@ +frappe.ui.form.on("Active Task Timer", { + refresh(frm) { + + }, +}); diff --git a/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.json b/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.json new file mode 100644 index 00000000..af986c35 --- /dev/null +++ b/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.json @@ -0,0 +1,76 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-04-18 11:40:00.000000", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "task", + "project", + "subject", + "start_time" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "reqd": 1 + }, + { + "fieldname": "task", + "fieldtype": "Link", + "label": "Task", + "options": "Task" + }, + { + "fieldname": "project", + "fieldtype": "Data", + "label": "Project ID" + }, + { + "fieldname": "subject", + "fieldtype": "Data", + "label": "Task Subject" + }, + { + "fieldname": "start_time", + "fieldtype": "Datetime", + "label": "Start Time" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-04-18 11:40:00.000000", + "modified_by": "Administrator", + "module": "One Compliance", + "name": "Active Task Timer", + "autoname": "hash", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "read": 1, + "role": "All", + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] + } + \ No newline at end of file diff --git a/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.py b/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.py new file mode 100644 index 00000000..a43725c0 --- /dev/null +++ b/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.py @@ -0,0 +1,5 @@ +import frappe +from frappe.model.document import Document + +class ActiveTaskTimer(Document): + pass diff --git a/one_compliance/one_compliance/page/task_management_tool/task_management_tool.html b/one_compliance/one_compliance/page/task_management_tool/task_management_tool.html index 97ed0b28..b144162b 100644 --- a/one_compliance/one_compliance/page/task_management_tool/task_management_tool.html +++ b/one_compliance/one_compliance/page/task_management_tool/task_management_tool.html @@ -67,12 +67,14 @@ diff --git a/one_compliance/one_compliance/page/task_management_tool/task_management_tool.js b/one_compliance/one_compliance/page/task_management_tool/task_management_tool.js index 8d966d9c..9cf3dd9d 100644 --- a/one_compliance/one_compliance/page/task_management_tool/task_management_tool.js +++ b/one_compliance/one_compliance/page/task_management_tool/task_management_tool.js @@ -250,49 +250,61 @@ function initialize_task_actions(page) { body.find(".startButton").off().on("click", function () { const task_name = $(this).attr("task-id"); const project_name = $(this).attr("project-id"); + const task_subject = $(this).attr("task-subject"); const status = page.fields_dict.status.get_value(); if (["Completed", "Hold", "Cancelled"].includes(status)) return; const current_time = frappe.datetime.now_datetime(); const formatted_time = frappe.datetime.str_to_user(current_time); - localStorage.setItem(`start-time-task-${task_name}-project-${project_name}`, current_time); - body.find(`.start-time[task-id='${task_name}'][project-id='${project_name}']`).text(formatted_time); - update_task_status(page, task_name, "Working"); - - $(this).hide(); - body.find(`.timeEntryButton[task-id='${task_name}'][project-id='${project_name}']`).show(); - }); + frappe.call({ + method: "one_compliance.one_compliance.page.task_management_tool.task_management_tool.start_active_timer", + args: { + task: task_name, + project: project_name || "", + subject: task_subject || "", + start_time: current_time + }, + callback: (r) => { + if (r.message) { + const user = frappe.session.user; + if (user) { + localStorage.setItem('one-compliance-active-timer-' + user, JSON.stringify(r.message)); + } + + body.find(`.start-time[task-id='${task_name}'][project-id='${project_name}']`).text(formatted_time); + update_task_status(page, task_name, "Working"); - body.find(".start-time").each(function () { - const task_name = $(this).attr("task-id"); - const project_name = $(this).attr("project-id"); - let start_time = localStorage.getItem(`start-time-task-${task_name}-project-${project_name}`); - - if (start_time) { - const stored_date = new Date(start_time); - const current_date = new Date(); - - if ( - stored_date.getDate() !== current_date.getDate() || - stored_date.getMonth() !== current_date.getMonth() || - stored_date.getFullYear() !== current_date.getFullYear() - ) { - localStorage.removeItem(`start-time-task-${task_name}-project-${project_name}`); - start_time = null; + $(this).hide(); + body.find(`.timeEntryButton[task-id='${task_name}'][project-id='${project_name}']`).show(); + $(document).trigger('one-compliance-timer-changed', [r.message]); + } } - } + }); + }); - if (start_time) { - const formatted_time = frappe.datetime.str_to_user(start_time); - $(this).text(formatted_time); - body.find(`.startButton[task-id='${task_name}'][project-id='${project_name}']`).hide(); - body.find(`.timeEntryButton[task-id='${task_name}'][project-id='${project_name}']`).show(); - } else { - $(this).text(""); - body.find(`.startButton[task-id='${task_name}'][project-id='${project_name}']`).show(); - body.find(`.timeEntryButton[task-id='${task_name}'][project-id='${project_name}']`).hide(); + frappe.call({ + method: "one_compliance.one_compliance.page.task_management_tool.task_management_tool.get_active_timer", + callback: (r) => { + const active_timers = r.message || []; + body.find(".start-time").each(function () { + const task_name = $(this).attr("task-id"); + const project_name = $(this).attr("project-id"); + + const task_timer = active_timers.find(t => t.task === task_name); + + if (task_timer) { + const formatted_time = frappe.datetime.str_to_user(task_timer.start_time); + $(this).text(formatted_time); + body.find(`.startButton[task-id='${task_name}'][project-id='${project_name}']`).hide(); + body.find(`.timeEntryButton[task-id='${task_name}'][project-id='${project_name}']`).show(); + } else { + $(this).text(""); + body.find(`.startButton[task-id='${task_name}'][project-id='${project_name}']`).show(); + body.find(`.timeEntryButton[task-id='${task_name}'][project-id='${project_name}']`).hide(); + } + }); } }); @@ -300,20 +312,29 @@ function initialize_task_actions(page) { const task_name = $(this).attr("task-id"); const project_name = $(this).attr("project-id"); const assignees = $(this).attr("assignees"); - const start_time = localStorage.getItem(`start-time-task-${task_name}-project-${project_name}`); - frappe.db.get_value("Task", task_name, "has_external_dependencies") - .then(({ message }) => { - const show_lag = !!message?.has_external_dependencies; - - show_time_entry_dialog( - page, - task_name, - project_name, - assignees, - start_time, - show_lag - ); - }); + + frappe.call({ + method: "one_compliance.one_compliance.page.task_management_tool.task_management_tool.get_active_timer", + callback: (r) => { + const active_timers = r.message || []; + const task_timer = active_timers.find(t => t.task === task_name); + const start_time = task_timer ? task_timer.start_time : null; + + frappe.db.get_value("Task", task_name, "has_external_dependencies") + .then(({ message }) => { + const show_lag = !!message?.has_external_dependencies; + + show_time_entry_dialog( + page, + task_name, + project_name, + assignees, + start_time, + show_lag + ); + }); + } + }); }); @@ -657,7 +678,17 @@ function show_time_entry_dialog(page, task_name, project_name, assignees, start_ ], primary_action_label: __("Submit"), primary_action(values) { - localStorage.removeItem(`start-time-task-${task_name}-project-${project_name}`); + frappe.call({ + method: "one_compliance.one_compliance.page.task_management_tool.task_management_tool.stop_active_timer", + args: { task: task_name }, + callback: (r) => { + const user = frappe.session.user; + if (user) { + localStorage.setItem('one-compliance-active-timer-' + user, JSON.stringify(r.message || [])); + } + $(document).trigger('one-compliance-timer-changed', [r.message || []]); + } + }); frappe.call({ method: "one_compliance.one_compliance.page.task_management_tool.task_management_tool.create_timesheet", args: values, @@ -917,21 +948,25 @@ Hide start buttons if no assignees are set OR if task already has a start time. function hide_start_button_without_assignees(page) { if (!page || !page.body) return; - page.body.find(".startButton").each(function () { - const $btn = $(this); - let assignees = $btn.attr("assignees") || ""; - const task_id = $btn.attr("task-id"); - const project_id = $btn.attr("project-id"); + frappe.call({ + method: "one_compliance.one_compliance.page.task_management_tool.task_management_tool.get_active_timer", + callback: (r) => { + const active_timers = r.message || []; + page.body.find(".startButton").each(function () { + const $btn = $(this); + let assignees = $btn.attr("assignees") || ""; + const task_id = $btn.attr("task-id"); - assignees = assignees.replace(/\s+/g, "").trim(); + assignees = assignees.replace(/\s+/g, "").trim(); - const start_time_key = "start-time-task-" + task_id + "-project-" + project_id; - const start_time = localStorage.getItem(start_time_key); + const is_active = active_timers.some(t => t.task === task_id); - if (!assignees || start_time) { - $btn.hide(); - } else { - $btn.show(); + if (!assignees || is_active) { + $btn.hide(); + } else { + $btn.show(); + } + }); } }); } diff --git a/one_compliance/one_compliance/page/task_management_tool/task_management_tool.py b/one_compliance/one_compliance/page/task_management_tool/task_management_tool.py index 4c20b3f3..3e61add4 100644 --- a/one_compliance/one_compliance/page/task_management_tool/task_management_tool.py +++ b/one_compliance/one_compliance/page/task_management_tool/task_management_tool.py @@ -279,4 +279,74 @@ def get_icon_hidden_status(): 'hide_credentials_icon': frappe.db.get_single_value("Compliance Settings", "hide_credentials_icon"), 'hide_payment_icon': frappe.db.get_single_value("Compliance Settings", "hide_payment_icon") } - return data \ No newline at end of file + return data + +@frappe.whitelist() +def start_active_timer(task, project, subject, start_time): + user = frappe.session.user + if not user or user == 'Guest': + return + + val1 = frappe.db.get_value("Projects Settings", "Projects Settings", "ignore_employee_time_overlap") + val2 = frappe.db.get_value("Projects Settings", "Projects Settings", "ignore_user_time_overlap") + ignore_overlap = bool(val1 or val2) + + if not ignore_overlap: + existing_timer = frappe.db.sql(""" + SELECT task, subject FROM `tabActive Task Timer` WHERE user = %s AND task != %s + """, (user, task), as_dict=True) + + if existing_timer: + existing_timer = existing_timer[0] + frappe.throw(_("Another task is already running: {0}. Please stop it before starting a new one.").format(existing_timer.subject or existing_timer.task)) + + timer_name = frappe.db.get_value("Active Task Timer", {"user": user, "task": task}) + + if timer_name: + doc = frappe.get_doc("Active Task Timer", timer_name) + else: + doc = frappe.new_doc("Active Task Timer") + doc.user = user + doc.task = task + + doc.flags.ignore_permissions = True + + doc.project = project + doc.subject = subject + doc.start_time = start_time + doc.save(ignore_permissions=True) + frappe.db.commit() + + all_timers = get_active_timer() + frappe.publish_realtime("one_compliance_timer_update", all_timers, user=user) + + return all_timers + +@frappe.whitelist() +def stop_active_timer(task=None): + user = frappe.session.user + filters = {"user": user} + if task: + filters["task"] = task + + timer_names = frappe.get_all("Active Task Timer", filters=filters, pluck="name", ignore_permissions=True) + for name in timer_names: + frappe.delete_doc("Active Task Timer", name, ignore_permissions=True) + + frappe.db.commit() + + all_timers = get_active_timer() + frappe.publish_realtime("one_compliance_timer_update", all_timers, user=user) + return all_timers + +@frappe.whitelist() +def get_active_timer(): + user = frappe.session.user + if not user or user == 'Guest': + return [] + + timers = frappe.db.sql(""" + SELECT task, project, subject, start_time FROM `tabActive Task Timer` WHERE user = %s + """, (user,), as_dict=True) + + return timers \ No newline at end of file diff --git a/one_compliance/public/css/one_compliance.css b/one_compliance/public/css/one_compliance.css new file mode 100644 index 00000000..dd20a564 --- /dev/null +++ b/one_compliance/public/css/one_compliance.css @@ -0,0 +1,68 @@ +#oc-timer-wrap { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 2147483647; +} + +#oc-timer-box { + background: #ff851b; + color: white; + padding: 10px 15px; + border-radius: 8px; + + display: flex; + align-items: center; + gap: 10px; + + font-weight: bold; + font-size: 14px; + + cursor: pointer; + + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + + position: relative; + z-index: 2; + + text-decoration: none; + white-space: nowrap; + + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +#oc-timer-wrap::before { + content: ""; + position: absolute; + top: -3px; + left: -3px; + right: -3px; + bottom: -3px; + + border-radius: 10px; + + pointer-events: none; + + background: conic-gradient( + from 0deg, + rgba(255,133,27,0) 0deg, + rgba(255,133,27,0) 60deg, + rgba(255,133,27,0.9) 90deg, + rgba(255,133,27,0) 140deg, + rgba(255,133,27,0) 360deg + ); + + filter: blur(1.5px); + + animation: oc-wave-rotate 1.8s linear infinite; +} + + +@keyframes oc-wave-rotate { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/one_compliance/public/js/one_compliance.js b/one_compliance/public/js/one_compliance.js new file mode 100644 index 00000000..ec995db2 --- /dev/null +++ b/one_compliance/public/js/one_compliance.js @@ -0,0 +1,225 @@ +(function() { + 'use strict'; + + var PREFIX = 'one-compliance-active-timer-'; + + // ========================= + // CSS injection + // ========================= + var style = document.createElement('style'); + style.textContent = ` + #oc-timer-wrap { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 2147483647; + } + + #oc-timer-box { + background: #ff851b; + color: white; + padding: 10px 15px; + border-radius: 8px; + + display: flex; + align-items: center; + gap: 10px; + + font-weight: bold; + font-size: 14px; + + cursor: pointer; + + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + + position: relative; + z-index: 2; + + text-decoration: none; + white-space: nowrap; + + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + } + + /* ========================= + OUTER BLINK ONLY (no inside effect) + ========================= */ + #oc-timer-wrap::before { + content: ""; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + + border-radius: 10px; + + pointer-events: none; + + box-shadow: 0 0 0 2px rgba(255,133,27,0.9); + + animation: oc-border-blink 1.2s infinite; + } + + @keyframes oc-border-blink { + 0% { + opacity: 1; + box-shadow: 0 0 0 2px rgba(255,133,27,0.9); + } + 50% { + opacity: 0.3; + box-shadow: 0 0 0 2px rgba(255,133,27,0.3); + } + 100% { + opacity: 1; + box-shadow: 0 0 0 2px rgba(255,133,27,0.9); + } + } + `; + document.documentElement.appendChild(style); + + // ========================= + // DATA + // ========================= + function getActiveData() { + var match = document.cookie.match(/(?:^|; )user_id=([^;]*)/); + var user = match ? decodeURIComponent(match[1]) : null; + + if (user) { + var d = localStorage.getItem(PREFIX + user); + if (d) try { + var j = JSON.parse(d); + return Array.isArray(j) ? j : [j]; + } catch(e) {} + } + + for (var i = 0; i < localStorage.length; i++) { + var key = localStorage.key(i); + if (key && key.indexOf(PREFIX) === 0) { + try { + var data = JSON.parse(localStorage.getItem(key)); + return Array.isArray(data) ? data : [data]; + } catch(e) {} + } + } + return []; + } + + function esc(s) { + if (!s) return ""; + return String(s) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); + } + + function getHTML(timers) { + if (!timers || timers.length === 0) return ''; + + var icon = ''; + var content = ''; + + if (timers.length <= 3) { + var lines = []; + for (var i = 0; i < timers.length; i++) { + var d = timers[i]; + var p = d.project ? esc(d.project) + ': ' : ''; + var t = esc(d.formatted_start_time || d.start_time); + lines.push('
Timer is ON: ' + p + esc(d.subject) + ' (' + t + ')
'); + } + content = '
' + lines.join('') + '
'; + } else { + content = 'Timer is ON: ' + timers.length + ' Tasks running'; + } + + return icon + '
' + content + '
'; + } + + // ========================= + // RENDER + // ========================= + function render() { + if (document.getElementById('oc-timer-wrap')) return true; + if (!document.body) return false; + + var wrap = document.createElement('div'); + wrap.id = 'oc-timer-wrap'; + + var a = document.createElement('a'); + a.id = 'oc-timer-box'; + a.href = '/app/task-management-tool'; + + var timers = getActiveData(); + if (timers.length > 0) { + a.innerHTML = getHTML(timers); + } else { + a.style.display = 'none'; + } + + wrap.appendChild(a); + document.body.appendChild(wrap); + + return true; + } + + (function boot() { + if (!render()) setTimeout(boot, 1); + })(); + + // ========================= + // SYNC (Frappe integration) + // ========================= + function startSync() { + if (!window.frappe || !frappe.call || !window.jQuery) { + setTimeout(startSync, 100); + return; + } + + var up = function(data) { + var el = document.getElementById('oc-timer-box'); + if (!el) return; + + var ts = Array.isArray(data) ? data : (data ? [data] : []); + + if (ts.length > 0) { + if (frappe.datetime) { + ts.forEach(function(x){ + if(x.start_time) + x.formatted_start_time = frappe.datetime.str_to_user(x.start_time); + }); + } + + el.innerHTML = getHTML(ts); + el.style.display = 'flex'; + + var u = (frappe.session && frappe.session.user) || + (frappe.boot && frappe.boot.user && frappe.boot.user.name); + + if (u) localStorage.setItem(PREFIX + u, JSON.stringify(ts)); + } else { + el.style.display = 'none'; + + var u = (frappe.session && frappe.session.user) || + (frappe.boot && frappe.boot.user && frappe.boot.user.name); + + if (u) localStorage.removeItem(PREFIX + u); + } + }; + + $(document).on('one-compliance-timer-changed', function(e, d) { up(d); }); + + if (frappe.realtime) + frappe.realtime.on('one_compliance_timer_update', up); + + frappe.call({ + method: "one_compliance.one_compliance.page.task_management_tool.task_management_tool.get_active_timer", + callback: function(r) { + up(r.message); + } + }); + } + + startSync(); + +})(); \ No newline at end of file From ad60fd0accf6a326f484aa9669513b7b53e17c7b Mon Sep 17 00:00:00 2001 From: Jumana-K Date: Tue, 21 Apr 2026 17:45:30 +0530 Subject: [PATCH 2/6] feat: add initial pyproject.toml for project configuration --- pyproject.toml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..5601dd8d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "one_compliance" +authors = [ + { name = "efeone", email = "info@efeone.com"} +] +description = “One Compliance is a comprehensive task management software solution designed to help organizations efficiently plan, track, and execute compliance-related and operational tasks. It provides a centralized platform to assign responsibilities, monitor progress, set deadlines, and ensure accountability across teams. With features such as task scheduling, workflow tracking, and reporting, One Compliance enables businesses to maintain regulatory adherence, improve coordination, and enhance overall productivity while reducing the risk of missed or incomplete tasks." +requires-python = ">=3.10" +readme = "README.md" +dynamic = ["version"] +dependencies = [ + # "frappe~=15.0.0" # Installed and managed by bench. +] + +[build-system] +requires = ["flit_core >=3.4,<4"] +build-backend = "flit_core.buildapi" + +# These dependencies are only installed when developer mode is enabled +[tool.bench.dev-dependencies] +# package_name = "~=1.1.0" \ No newline at end of file From 3a4205067a6cb2ef9bf1e1cd058e0621159122f6 Mon Sep 17 00:00:00 2001 From: Jumana-K Date: Tue, 21 Apr 2026 18:14:28 +0530 Subject: [PATCH 3/6] fix: correct pyproject.toml syntax --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5601dd8d..36b5c221 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "one_compliance" authors = [ { name = "efeone", email = "info@efeone.com"} ] -description = “One Compliance is a comprehensive task management software solution designed to help organizations efficiently plan, track, and execute compliance-related and operational tasks. It provides a centralized platform to assign responsibilities, monitor progress, set deadlines, and ensure accountability across teams. With features such as task scheduling, workflow tracking, and reporting, One Compliance enables businesses to maintain regulatory adherence, improve coordination, and enhance overall productivity while reducing the risk of missed or incomplete tasks." +description = "One Compliance is a comprehensive task management software solution designed to help organizations efficiently plan, track, and execute compliance-related and operational tasks. It provides a centralized platform to assign responsibilities, monitor progress, set deadlines, and ensure accountability across teams. With features such as task scheduling, workflow tracking, and reporting, One Compliance enables businesses to maintain regulatory adherence, improve coordination, and enhance overall productivity while reducing the risk of missed or incomplete tasks." requires-python = ">=3.10" readme = "README.md" dynamic = ["version"] From d296fe232c698be18123132ee34eea140f8fdc34 Mon Sep 17 00:00:00 2001 From: hridyalakshmi Date: Thu, 23 Apr 2026 15:30:14 +0530 Subject: [PATCH 4/6] fix: Update timer in task management tool --- .../task_management_tool.js | 110 ++++++++++-------- .../task_management_tool.py | 2 +- 2 files changed, 64 insertions(+), 48 deletions(-) diff --git a/one_compliance/one_compliance/page/task_management_tool/task_management_tool.js b/one_compliance/one_compliance/page/task_management_tool/task_management_tool.js index 9cf3dd9d..b8f293a6 100644 --- a/one_compliance/one_compliance/page/task_management_tool/task_management_tool.js +++ b/one_compliance/one_compliance/page/task_management_tool/task_management_tool.js @@ -869,57 +869,73 @@ Opens a dialog to update the status of a specific task and refreshes the task li */ function update_status(page, task_name, project_id, task_id) { - frappe.model.with_doctype('Task', () => { - let meta = frappe.get_meta('Task'); - let status_field = meta.fields.find(df => df.fieldname === 'status'); + frappe.call({ + method: "one_compliance.one_compliance.page.task_management_tool.task_management_tool.get_active_timer", + callback: function(r) { - const exclude = ["Template", "Cancelled", "Overdue"]; + const active_timers = r.message || []; + const is_running = active_timers.some(t => t.task === task_id); - // Create newline string - let option_string = (status_field.options || "") - .split("\n") - .filter(opt => opt && opt.trim() !== "" && !exclude.includes(opt)) - .join("\n"); - - const dialog = new frappe.ui.Dialog({ - title: __("Update Task Status"), - fields: [ - { - label: __("Status"), - fieldname: "status", - fieldtype: "Select", - options: option_string, - default: "Completed", - }, - { - label: __("Completed By"), - fieldname: "completed_by", - fieldtype: "Link", - options: "User", - default: frappe.session.user, - }, - { - label: __("Completed On"), - fieldname: "completed_on", - fieldtype: "Date", - default: frappe.datetime.get_today(), - }, - ], - primary_action_label: __("Update"), - primary_action(values) { - frappe.call({ - method: "one_compliance.one_compliance.doc_events.task.update_task_status", - args: { task_id, ...values }, - callback(r) { - if (r.message) { - dialog.hide(); - refresh_tasks(page); - } + if (is_running) { + frappe.msgprint({ + title: __("Not Allowed"), + message: __("This task is currently running. Please stop the timer before marking it as Completed."), + indicator: "red" + }); + return; + } + frappe.model.with_doctype('Task', () => { + let meta = frappe.get_meta('Task'); + let status_field = meta.fields.find(df => df.fieldname === 'status'); + + const exclude = ["Template", "Cancelled", "Overdue"]; + + let option_string = (status_field.options || "") + .split("\n") + .filter(opt => opt && opt.trim() !== "" && !exclude.includes(opt)) + .join("\n"); + + const dialog = new frappe.ui.Dialog({ + title: __("Update Task Status"), + fields: [ + { + label: __("Status"), + fieldname: "status", + fieldtype: "Select", + options: option_string, + default: "Completed", + }, + { + label: __("Completed By"), + fieldname: "completed_by", + fieldtype: "Link", + options: "User", + default: frappe.session.user, + }, + { + label: __("Completed On"), + fieldname: "completed_on", + fieldtype: "Date", + default: frappe.datetime.get_today(), + }, + ], + primary_action_label: __("Update"), + primary_action(values) { + frappe.call({ + method: "one_compliance.one_compliance.doc_events.task.update_task_status", + args: { task_id, ...values }, + callback(r) { + if (r.message) { + dialog.hide(); + refresh_tasks(page); + } + }, + }); }, }); - }, - }); - dialog.show(); + dialog.show(); + }); + } }); } diff --git a/one_compliance/one_compliance/page/task_management_tool/task_management_tool.py b/one_compliance/one_compliance/page/task_management_tool/task_management_tool.py index 3e61add4..efc21968 100644 --- a/one_compliance/one_compliance/page/task_management_tool/task_management_tool.py +++ b/one_compliance/one_compliance/page/task_management_tool/task_management_tool.py @@ -289,7 +289,7 @@ def start_active_timer(task, project, subject, start_time): val1 = frappe.db.get_value("Projects Settings", "Projects Settings", "ignore_employee_time_overlap") val2 = frappe.db.get_value("Projects Settings", "Projects Settings", "ignore_user_time_overlap") - ignore_overlap = bool(val1 or val2) + ignore_overlap = (int(val1 or 0) == 1) or (int(val2 or 0) == 1) if not ignore_overlap: existing_timer = frappe.db.sql(""" From 732c2a15b62410a0547aa60795a7d14453230ce8 Mon Sep 17 00:00:00 2001 From: hridyalakshmi Date: Tue, 28 Apr 2026 12:48:57 +0530 Subject: [PATCH 5/6] fix: update task management tool --- .../task_management_tool.py | 23 +- one_compliance/public/js/one_compliance.js | 472 ++++++++++-------- 2 files changed, 297 insertions(+), 198 deletions(-) diff --git a/one_compliance/one_compliance/page/task_management_tool/task_management_tool.py b/one_compliance/one_compliance/page/task_management_tool/task_management_tool.py index efc21968..2d051767 100644 --- a/one_compliance/one_compliance/page/task_management_tool/task_management_tool.py +++ b/one_compliance/one_compliance/page/task_management_tool/task_management_tool.py @@ -283,9 +283,24 @@ def get_icon_hidden_status(): @frappe.whitelist() def start_active_timer(task, project, subject, start_time): + """ + Start a timer for a specific task, ensuring no overlapping timers for the same user. + """ user = frappe.session.user if not user or user == 'Guest': - return + frappe.throw(_("User authentication required. Please login first.")) + if not frappe.db.exists("Task", task): + frappe.throw(_("Task {0} not found").format(task)) + if not frappe.has_permission("Task", "read", task): + frappe.throw(_("No permission to access this task")) + if project and not frappe.db.exists("Project", project): + frappe.throw(_("Project {0} not found").format(project)) + try: + start_dt = frappe.utils.get_datetime(start_time) + if start_dt > frappe.utils.now_datetime(): + frappe.throw(_("Start time cannot be in the future")) + except Exception: + frappe.throw(_("Invalid start_time format")) val1 = frappe.db.get_value("Projects Settings", "Projects Settings", "ignore_employee_time_overlap") val2 = frappe.db.get_value("Projects Settings", "Projects Settings", "ignore_user_time_overlap") @@ -324,6 +339,9 @@ def start_active_timer(task, project, subject, start_time): @frappe.whitelist() def stop_active_timer(task=None): + """ + Stop the active timer for the current user, optionally filtering by task. + """ user = frappe.session.user filters = {"user": user} if task: @@ -341,6 +359,9 @@ def stop_active_timer(task=None): @frappe.whitelist() def get_active_timer(): + """ + Retrieve the active timer for the current user, ensuring proper permissions and handling guest users. + """ user = frappe.session.user if not user or user == 'Guest': return [] diff --git a/one_compliance/public/js/one_compliance.js b/one_compliance/public/js/one_compliance.js index ec995db2..0031cd5e 100644 --- a/one_compliance/public/js/one_compliance.js +++ b/one_compliance/public/js/one_compliance.js @@ -1,225 +1,303 @@ -(function() { - 'use strict'; +(function () { + 'use strict'; - var PREFIX = 'one-compliance-active-timer-'; + const PREFIX = 'one-compliance-active-timer-'; + + // ========================= + // CSS Injection + // ========================= + function injectStyles() { + const Z_INDEX = 1040; + const style = document.createElement('style'); - // ========================= - // CSS injection - // ========================= - var style = document.createElement('style'); style.textContent = ` #oc-timer-wrap { - position: fixed; - bottom: 20px; - right: 20px; - z-index: 2147483647; + position: fixed; + bottom: 20px; + right: 20px; + z-index: ${Z_INDEX}; } #oc-timer-box { - background: #ff851b; - color: white; - padding: 10px 15px; - border-radius: 8px; + background: #ff851b; + color: white; + padding: 10px 15px; + border-radius: 8px; - display: flex; - align-items: center; - gap: 10px; + display: flex; + align-items: center; + gap: 10px; - font-weight: bold; - font-size: 14px; + font-weight: bold; + font-size: 14px; - cursor: pointer; + cursor: pointer; - box-shadow: 0 4px 6px rgba(0,0,0,0.1); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - position: relative; - z-index: 2; + position: relative; + z-index: 2; - text-decoration: none; - white-space: nowrap; + text-decoration: none; + white-space: nowrap; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + Helvetica, Arial, sans-serif; } - /* ========================= - OUTER BLINK ONLY (no inside effect) - ========================= */ #oc-timer-wrap::before { - content: ""; - position: absolute; - top: -2px; - left: -2px; - right: -2px; - bottom: -2px; - - border-radius: 10px; - - pointer-events: none; - - box-shadow: 0 0 0 2px rgba(255,133,27,0.9); - - animation: oc-border-blink 1.2s infinite; + content: ''; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + + border-radius: 10px; + pointer-events: none; + + box-shadow: 0 0 0 2px rgba(255, 133, 27, 0.9); + animation: oc-border-blink 1.2s infinite; } @keyframes oc-border-blink { - 0% { - opacity: 1; - box-shadow: 0 0 0 2px rgba(255,133,27,0.9); - } - 50% { - opacity: 0.3; - box-shadow: 0 0 0 2px rgba(255,133,27,0.3); - } - 100% { - opacity: 1; - box-shadow: 0 0 0 2px rgba(255,133,27,0.9); - } - } - `; - document.documentElement.appendChild(style); - - // ========================= - // DATA - // ========================= - function getActiveData() { - var match = document.cookie.match(/(?:^|; )user_id=([^;]*)/); - var user = match ? decodeURIComponent(match[1]) : null; - - if (user) { - var d = localStorage.getItem(PREFIX + user); - if (d) try { - var j = JSON.parse(d); - return Array.isArray(j) ? j : [j]; - } catch(e) {} + 0% { + opacity: 1; + box-shadow: 0 0 0 2px rgba(255, 133, 27, 0.9); } - - for (var i = 0; i < localStorage.length; i++) { - var key = localStorage.key(i); - if (key && key.indexOf(PREFIX) === 0) { - try { - var data = JSON.parse(localStorage.getItem(key)); - return Array.isArray(data) ? data : [data]; - } catch(e) {} - } + 50% { + opacity: 0.3; + box-shadow: 0 0 0 2px rgba(255, 133, 27, 0.3); } - return []; - } - - function esc(s) { - if (!s) return ""; - return String(s) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """); - } - - function getHTML(timers) { - if (!timers || timers.length === 0) return ''; - - var icon = ''; - var content = ''; - - if (timers.length <= 3) { - var lines = []; - for (var i = 0; i < timers.length; i++) { - var d = timers[i]; - var p = d.project ? esc(d.project) + ': ' : ''; - var t = esc(d.formatted_start_time || d.start_time); - lines.push('
Timer is ON: ' + p + esc(d.subject) + ' (' + t + ')
'); - } - content = '
' + lines.join('') + '
'; - } else { - content = 'Timer is ON: ' + timers.length + ' Tasks running'; + 100% { + opacity: 1; + box-shadow: 0 0 0 2px rgba(255, 133, 27, 0.9); } - - return icon + '
' + content + '
'; - } - - // ========================= - // RENDER - // ========================= - function render() { - if (document.getElementById('oc-timer-wrap')) return true; - if (!document.body) return false; - - var wrap = document.createElement('div'); - wrap.id = 'oc-timer-wrap'; - - var a = document.createElement('a'); - a.id = 'oc-timer-box'; - a.href = '/app/task-management-tool'; - - var timers = getActiveData(); - if (timers.length > 0) { - a.innerHTML = getHTML(timers); - } else { - a.style.display = 'none'; } + `; - wrap.appendChild(a); - document.body.appendChild(wrap); - - return true; - } - - (function boot() { - if (!render()) setTimeout(boot, 1); - })(); - - // ========================= - // SYNC (Frappe integration) - // ========================= - function startSync() { - if (!window.frappe || !frappe.call || !window.jQuery) { - setTimeout(startSync, 100); - return; - } - - var up = function(data) { - var el = document.getElementById('oc-timer-box'); - if (!el) return; - - var ts = Array.isArray(data) ? data : (data ? [data] : []); - - if (ts.length > 0) { - if (frappe.datetime) { - ts.forEach(function(x){ - if(x.start_time) - x.formatted_start_time = frappe.datetime.str_to_user(x.start_time); - }); - } - - el.innerHTML = getHTML(ts); - el.style.display = 'flex'; - - var u = (frappe.session && frappe.session.user) || - (frappe.boot && frappe.boot.user && frappe.boot.user.name); - - if (u) localStorage.setItem(PREFIX + u, JSON.stringify(ts)); - } else { - el.style.display = 'none'; - - var u = (frappe.session && frappe.session.user) || - (frappe.boot && frappe.boot.user && frappe.boot.user.name); - - if (u) localStorage.removeItem(PREFIX + u); - } - }; - - $(document).on('one-compliance-timer-changed', function(e, d) { up(d); }); - - if (frappe.realtime) - frappe.realtime.on('one_compliance_timer_update', up); - - frappe.call({ - method: "one_compliance.one_compliance.page.task_management_tool.task_management_tool.get_active_timer", - callback: function(r) { - up(r.message); - } - }); + document.documentElement.appendChild(style); } - startSync(); - + // ========================= + // Utilities + // ========================= + + /** + * Escape HTML to prevent XSS + * @param {string} str + * @returns {string} + */ + function esc(str) { + if (!str) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + /** + * Get active timer data from localStorage (fallback/cache) + * @returns {Array} + */ + function getActiveData() { + const match = document.cookie.match(/(?:^|; )user_id=([^;]*)/); + const user = match ? decodeURIComponent(match[1]) : null; + + if (user) { + const data = localStorage.getItem(PREFIX + user); + if (data) { + try { + const parsed = JSON.parse(data); + return Array.isArray(parsed) ? parsed : [parsed]; + } catch (e) { + console.warn('Invalid timer data in localStorage', e); + } + } + } + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(PREFIX)) { + try { + const data = JSON.parse(localStorage.getItem(key)); + return Array.isArray(data) ? data : [data]; + } catch (e) { + console.warn('Error parsing fallback timer data', e); + } + } + } + + return []; + } + + /** + * Generate HTML for timer display + * @param {Array} timers + * @returns {string} + */ + function getHTML(timers) { + if (!timers || timers.length === 0) return ''; + + const icon = + ''; + + let content = ''; + + if (timers.length <= 3) { + const lines = timers.map((d) => { + const project = d.project ? esc(d.project) + ': ' : ''; + const time = esc(d.formatted_start_time || d.start_time); + + return ( + '
' + + 'Timer is ON: ' + + project + + esc(d.subject) + + ' (' + + time + + ')' + + '
' + ); + }); + + content = + '
' + + lines.join('') + + '
'; + } else { + content = + 'Timer is ON: ' + + timers.length + + ' Tasks running'; + } + + return icon + '
' + content + '
'; + } + + // ========================= + // Render + // ========================= + + /** + * Render floating timer UI + * @returns {boolean} + */ + function render() { + if (document.getElementById('oc-timer-wrap')) return true; + if (!document.body) return false; + + const wrap = document.createElement('div'); + wrap.id = 'oc-timer-wrap'; + + const link = document.createElement('a'); + link.id = 'oc-timer-box'; + link.href = '/app/task-management-tool'; + + const timers = getActiveData(); + + if (timers.length > 0) { + link.innerHTML = getHTML(timers); + } else { + link.style.display = 'none'; + } + + wrap.appendChild(link); + document.body.appendChild(wrap); + + return true; + } + + /** + * Boot renderer safely + */ + function boot() { + if (!render()) { + setTimeout(boot, 10); + } + } + + // ========================= + // Sync with Frappe Backend + // ========================= + + /** + * Start real-time sync with backend + */ + function startSync() { + if (!window.frappe || !frappe.call || !window.jQuery) { + setTimeout(startSync, 100); + return; + } + + /** + * Update UI with timer data + * @param {Array|Object|null} data + */ + function update(data) { + const el = document.getElementById('oc-timer-box'); + if (!el) return; + + const timers = Array.isArray(data) + ? data + : data + ? [data] + : []; + + const user = + (frappe.session && frappe.session.user) || + (frappe.boot && frappe.boot.user && frappe.boot.user.name); + + if (timers.length > 0) { + if (frappe.datetime) { + timers.forEach((t) => { + if (t.start_time) { + t.formatted_start_time = + frappe.datetime.str_to_user(t.start_time); + } + }); + } + + el.innerHTML = getHTML(timers); + el.style.display = 'flex'; + + if (user) { + localStorage.setItem(PREFIX + user, JSON.stringify(timers)); + } + } else { + el.style.display = 'none'; + + if (user) { + localStorage.removeItem(PREFIX + user); + } + } + } + + // Custom event + $(document).on('one-compliance-timer-changed', function (e, data) { + update(data); + }); + + // Realtime event + if (frappe.realtime) { + frappe.realtime.on('one_compliance_timer_update', update); + } + + // Initial fetch + frappe.call({ + method: + 'one_compliance.one_compliance.page.task_management_tool.task_management_tool.get_active_timer', + callback: function (r) { + update(r.message); + }, + }); + } + + // ========================= + // Init + // ========================= + injectStyles(); + boot(); + startSync(); })(); \ No newline at end of file From d3b182a3b714ca803aee3d6835ab8b3f1d4adc57 Mon Sep 17 00:00:00 2001 From: hridyalakshmi Date: Thu, 14 May 2026 16:34:54 +0530 Subject: [PATCH 6/6] feat: Active task timer --- .../active_task_timer/active_task_timer.json | 160 ++++++++++-------- 1 file changed, 85 insertions(+), 75 deletions(-) diff --git a/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.json b/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.json index af986c35..472592a3 100644 --- a/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.json +++ b/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.json @@ -1,76 +1,86 @@ { - "actions": [], - "allow_rename": 1, - "creation": "2026-04-18 11:40:00.000000", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "user", - "task", - "project", - "subject", - "start_time" - ], - "fields": [ - { - "fieldname": "user", - "fieldtype": "Link", - "label": "User", - "options": "User", - "reqd": 1 - }, - { - "fieldname": "task", - "fieldtype": "Link", - "label": "Task", - "options": "Task" - }, - { - "fieldname": "project", - "fieldtype": "Data", - "label": "Project ID" - }, - { - "fieldname": "subject", - "fieldtype": "Data", - "label": "Task Subject" - }, - { - "fieldname": "start_time", - "fieldtype": "Datetime", - "label": "Start Time" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2026-04-18 11:40:00.000000", - "modified_by": "Administrator", - "module": "One Compliance", - "name": "Active Task Timer", - "autoname": "hash", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "read": 1, - "role": "All", - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] - } - \ No newline at end of file + "actions": [], + "allow_rename": 1, + "autoname": "full_name:task", + "creation": "2026-04-18 11:40:00", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "full_name", + "task", + "project", + "subject", + "start_time" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "reqd": 1 + }, + { + "fieldname": "task", + "fieldtype": "Link", + "label": "Task", + "options": "Task" + }, + { + "fieldname": "project", + "fieldtype": "Data", + "label": "Project ID" + }, + { + "fieldname": "subject", + "fieldtype": "Data", + "label": "Task Subject" + }, + { + "fieldname": "start_time", + "fieldtype": "Datetime", + "label": "Start Time" + }, + { + "fetch_from": "user.full_name", + "fieldname": "full_name", + "fieldtype": "Data", + "label": "Full Name", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-05-14 15:17:54.259813", + "modified_by": "Administrator", + "module": "One Compliance", + "name": "Active Task Timer", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "read": 1, + "role": "All", + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file