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..472592a3 --- /dev/null +++ b/one_compliance/one_compliance/doctype/active_task_timer/active_task_timer.json @@ -0,0 +1,86 @@ +{ + "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 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..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 @@ -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, @@ -838,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(); + }); + } }); } @@ -917,21 +964,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..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 @@ -279,4 +279,95 @@ 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): + """ + 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': + 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") + ignore_overlap = (int(val1 or 0) == 1) or (int(val2 or 0) == 1) + + 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): + """ + Stop the active timer for the current user, optionally filtering by task. + """ + 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(): + """ + 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 [] + + 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..0031cd5e --- /dev/null +++ b/one_compliance/public/js/one_compliance.js @@ -0,0 +1,303 @@ +(function () { + 'use strict'; + + const PREFIX = 'one-compliance-active-timer-'; + + // ========================= + // CSS Injection + // ========================= + function injectStyles() { + const Z_INDEX = 1040; + const style = document.createElement('style'); + + style.textContent = ` + #oc-timer-wrap { + position: fixed; + bottom: 20px; + right: 20px; + z-index: ${Z_INDEX}; + } + + #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: -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); + } + + // ========================= + // 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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..36b5c221 --- /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