diff --git a/one_compliance/custom/custom_field/timesheet_detail.py b/one_compliance/custom/custom_field/timesheet_detail.py
index eafc0438..13f87f7e 100644
--- a/one_compliance/custom/custom_field/timesheet_detail.py
+++ b/one_compliance/custom/custom_field/timesheet_detail.py
@@ -31,5 +31,12 @@ def get_timesheet_detail_custom_fields():
"default": "Pending",
"insert_after": "lag_notification_sent",
},
+ {
+ "fieldname": "event",
+ "fieldtype": "Link",
+ "label": "Event",
+ "options": "Event",
+ "insert_after": "approval_status",
+ },
]
}
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 472592a3..7bc6b7f9 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,7 +1,7 @@
{
"actions": [],
"allow_rename": 1,
- "autoname": "full_name:task",
+ "autoname": "prompt",
"creation": "2026-04-18 11:40:00",
"doctype": "DocType",
"editable_grid": 1,
@@ -10,6 +10,7 @@
"user",
"full_name",
"task",
+ "is_ad_hoc_event",
"project",
"subject",
"start_time"
@@ -29,6 +30,12 @@
"label": "Task",
"options": "Task"
},
+ {
+ "default": "0",
+ "fieldname": "is_ad_hoc_event",
+ "fieldtype": "Check",
+ "label": "Is Ad-hoc Event"
+ },
{
"fieldname": "project",
"fieldtype": "Data",
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
index a43725c0..2718c7d3 100644
--- 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
@@ -1,5 +1,11 @@
+# Copyright (c) 2026, efeone and contributors
+# For license information, please see license.txt
+
import frappe
from frappe.model.document import Document
class ActiveTaskTimer(Document):
- pass
+ def autoname(self):
+ full_name = self.full_name or frappe.db.get_value("User", self.user, "full_name") or self.user
+ task_label = self.task if self.task else ("Ad-hoc Event" if self.is_ad_hoc_event else "No Task")
+ self.name = f"{full_name}: {task_label}"
diff --git a/one_compliance/one_compliance/page/project_management_tool/project_management_tool.js b/one_compliance/one_compliance/page/project_management_tool/project_management_tool.js
index e6367316..aaafdac1 100644
--- a/one_compliance/one_compliance/page/project_management_tool/project_management_tool.js
+++ b/one_compliance/one_compliance/page/project_management_tool/project_management_tool.js
@@ -10,6 +10,25 @@ frappe.pages['project-management_tool'].on_page_load = function (wrapper) {
page.main.addClass("frappe-card");
+ page.add_event_btn = page.add_inner_button(__('Add Event'), function () {
+ window.start_active_event_timer();
+ });
+
+ $(document).on('one-compliance-refresh-tools', function () {
+ refresh_projects(page);
+ });
+
+ $(document).on('one-compliance-timer-changed', function (e, timers) {
+ const has_event_timer = (timers || []).some(t => t.is_ad_hoc_event);
+ if (page.add_event_btn) {
+ if (has_event_timer) {
+ page.add_event_btn.hide();
+ } else {
+ page.add_event_btn.show();
+ }
+ }
+ });
+
make_filters(page);
// Initialize pagination
page.current_page = 1;
@@ -137,28 +156,31 @@ function refresh_projects(page, page_num = null) {
page_length: page.page_length
},
callback: (r) => {
- if (r.message && r.message.length > 0) {
- $(frappe.render_template("project_management_tool", { project_list: r.message })).appendTo(page.body);
-
- // Action to redirect to the task management tool
- page.body.find(".showTask").on("click", function () {
- var project_id = $(this).attr("project");
-
- // Set route options before navigation
- frappe.route_options = {
- project: project_id
- };
- // Navigate to the task management tool page
- frappe.set_route('task-management-tool');
- });
-
- // Attach pagination controls and page-length button logic
- render_pagination_controls(page, r.message.length);
- setup_page_length_buttons(page);
- } else {
- // If no projects are found, append a message to the page body
- $('
').appendTo(page.body)
- .append('No Project found with matching filters.
');
+ if (r.message) {
+ if (r.message.projects && r.message.projects.length > 0) {
+ $(frappe.render_template("project_management_tool", { project_list: r.message.projects })).appendTo(page.body);
+
+ // Action to redirect to the task management tool
+ page.body.find(".showTask").on("click", function () {
+ var project_id = $(this).attr("project");
+
+ // Set route options before navigation
+ frappe.route_options = {
+ project: project_id
+ };
+ // Navigate to the task management tool page
+ frappe.set_route('task-management-tool');
+ });
+
+ // Attach pagination controls and page-length button logic
+ render_pagination_controls(page, r.message.projects.length);
+ setup_page_length_buttons(page);
+ } else {
+ // If no projects are found, append a message to the page body
+ $('').appendTo(page.body)
+ .append('No Project found with matching filters.
');
+ }
+ $(document).trigger('one-compliance-timer-changed', [r.message.active_timers]);
}
},
freeze: true,
diff --git a/one_compliance/one_compliance/page/project_management_tool/project_management_tool.py b/one_compliance/one_compliance/page/project_management_tool/project_management_tool.py
index 4a151e42..b3322f76 100644
--- a/one_compliance/one_compliance/page/project_management_tool/project_management_tool.py
+++ b/one_compliance/one_compliance/page/project_management_tool/project_management_tool.py
@@ -60,6 +60,7 @@ def get_project(
query += f" ORDER BY p.modified DESC LIMIT {int(page_length)} OFFSET {int(offset)};"
project_list = frappe.db.sql(query, as_dict=1)
+
# Process employee assignment details
for project in project_list:
project['employee_names'] = []
@@ -82,4 +83,9 @@ def get_project(
project['_assign'] = []
project['employee_names'] = []
- return project_list
+ from one_compliance.one_compliance.page.task_management_tool.task_management_tool import get_active_timer
+
+ return {
+ "projects": project_list,
+ "active_timers": get_active_timer()
+ }
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 b144162b..b6f0c304 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
@@ -18,10 +18,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 b8f293a6..24f1ee4f 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
@@ -10,6 +10,25 @@ frappe.pages['task-management-tool'].on_page_load = function (wrapper) {
page.current_page = 1;
page.page_length = 20;
+ page.add_event_btn = page.add_inner_button(__('Add Event'), function () {
+ window.start_active_event_timer();
+ });
+
+ $(document).on('one-compliance-refresh-tools', function () {
+ refresh_tasks(page);
+ });
+
+ $(document).on('one-compliance-timer-changed', function (e, timers) {
+ const has_event_timer = (timers || []).some(t => t.is_ad_hoc_event);
+ if (page.add_event_btn) {
+ if (has_event_timer) {
+ page.add_event_btn.hide();
+ } else {
+ page.add_event_btn.show();
+ }
+ }
+ });
+
make_filters(page);
if (!frappe.route_options || !frappe.route_options.project) {
refresh_tasks(page, true);
@@ -159,13 +178,30 @@ function refresh_tasks(page, reset_page = false) {
page_length: page.page_length
},
callback: (r) => {
- if (r.message && r.message.tasks.length > 0) {
- render_task_list(page, r.message.tasks, r.message.icons);
- setup_pagination(page, r.message.total_tasks);
- setup_page_length_buttons(page);
- initialize_task_actions(page);
- } else {
- show_no_task_found(page);
+ if (r.message) {
+ let tasks = r.message.tasks || [];
+ const active_timers = r.message.active_timers || [];
+
+ const event_timer = active_timers.find(t => t.is_ad_hoc_event);
+ if (event_timer) {
+ tasks.unshift({
+ name: 'EVENT-' + frappe.session.user,
+ subject: event_timer.subject || 'Ad-hoc Event',
+ status: 'Working',
+ is_event: true,
+ start_time: event_timer.start_time
+ });
+ }
+
+ if (tasks.length > 0) {
+ render_task_list(page, tasks, r.message.icons);
+ setup_pagination(page, r.message.total_tasks);
+ setup_page_length_buttons(page);
+ initialize_task_actions(page, active_timers);
+ } else {
+ show_no_task_found(page);
+ }
+ $(document).trigger('one-compliance-timer-changed', [active_timers]);
}
},
freeze: true,
@@ -227,7 +263,7 @@ function render_task_list(page, tasks, icons) {
Handles all task-related button bindings and UI updates
*/
-function initialize_task_actions(page) {
+function initialize_task_actions(page, active_timers = null) {
const body = page.body;
body.find(".paymentEntryButton").off().on("click", function () {
@@ -247,6 +283,12 @@ function initialize_task_actions(page) {
body.find(".timeEntryButton").hide();
+ body.find(".eventDialogButton").off().on("click", function () {
+ const start_time = $(this).attr("start-time");
+ const subject = $(this).attr("task-subject");
+ window.show_add_event_dialog(start_time, subject);
+ });
+
body.find(".startButton").off().on("click", function () {
const task_name = $(this).attr("task-id");
const project_name = $(this).attr("project-id");
@@ -284,29 +326,47 @@ function initialize_task_actions(page) {
});
});
- 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);
+ const apply_active_timers = (timers) => {
+ const active_timers_list = timers || [];
+ body.find(".start-time").each(function () {
+ const task_name = $(this).attr("task-id");
+ const project_name = $(this).attr("project-id");
+ const is_event = task_name && task_name.startsWith('EVENT-');
- 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();
+ const task_timer = is_event ? active_timers_list.find(t => t.is_ad_hoc_event) : active_timers_list.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();
+
+ if (is_event) {
+ body.find(`.eventDialogButton[task-id='${task_name}']`).show();
+ body.find(`.timeEntryButton[task-id='${task_name}']`).hide();
} else {
- $(this).text("");
+ body.find(`.timeEntryButton[task-id='${task_name}'][project-id='${project_name}']`).show();
+ }
+ } else {
+ $(this).text("");
+ if (!is_event) {
body.find(`.startButton[task-id='${task_name}'][project-id='${project_name}']`).show();
- body.find(`.timeEntryButton[task-id='${task_name}'][project-id='${project_name}']`).hide();
}
- });
- }
- });
+ body.find(`.timeEntryButton[task-id='${task_name}'][project-id='${project_name}']`).hide();
+ body.find(`.eventDialogButton[task-id='${task_name}']`).hide();
+ }
+ });
+ };
+
+ if (active_timers) {
+ apply_active_timers(active_timers);
+ } else {
+ frappe.call({
+ method: "one_compliance.one_compliance.page.task_management_tool.task_management_tool.get_active_timer",
+ callback: (r) => {
+ apply_active_timers(r.message);
+ }
+ });
+ }
body.find(".timeEntryButton").off().on("click", function () {
const task_name = $(this).attr("task-id");
@@ -751,7 +811,10 @@ function set_status_colors(page) {
status_el.css("color", color);
project_el.css("color", color);
- if (["Open", "Overdue", "Working", "Pending Review", "Hold", "Pending with Authority"].includes(status)) add_check_icon(status_el[0]);
+ const task_id = status_el.attr("task-id");
+ const is_event = task_id && task_id.startsWith('EVENT-');
+
+ if (!is_event && ["Open", "Overdue", "Working", "Pending Review", "Hold", "Pending with Authority"].includes(status)) add_check_icon(status_el[0]);
});
function add_check_icon(element) {
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 2d051767..e96880fb 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
@@ -126,7 +126,8 @@ def get_task(status=None, task=None, project=None, customer=None, department=Non
return {
"tasks": task_list,
"total_tasks": total_tasks,
- "icons": get_icon_hidden_status()
+ "icons": get_icon_hidden_status(),
+ "active_timers": get_active_timer()
}
@frappe.whitelist()
@@ -282,17 +283,21 @@ def get_icon_hidden_status():
return data
@frappe.whitelist()
-def start_active_timer(task, project, subject, start_time):
+def start_active_timer(task, project, subject, start_time, is_ad_hoc_event=0):
"""
Start a timer for a specific task, ensuring no overlapping timers for the same user.
"""
user = frappe.session.user
+ is_ad_hoc_event = int(is_ad_hoc_event)
+
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 not is_ad_hoc_event:
+ if not task or 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:
@@ -302,9 +307,8 @@ def start_active_timer(task, project, subject, start_time):
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)
+ settings = frappe.db.get_value("Projects Settings", "Projects Settings", ["ignore_employee_time_overlap", "ignore_user_time_overlap"], as_dict=True) or {}
+ ignore_overlap = (int(settings.get("ignore_employee_time_overlap") or 0) == 1) or (int(settings.get("ignore_user_time_overlap") or 0) == 1)
if not ignore_overlap:
existing_timer = frappe.db.sql("""
@@ -315,16 +319,24 @@ def start_active_timer(task, project, subject, start_time):
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})
+ filters = {"user": user}
+ if is_ad_hoc_event:
+ filters["is_ad_hoc_event"] = 1
+ else:
+ filters["task"] = task
+
+ timer_name = frappe.db.get_value("Active Task Timer", filters)
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
+ if not is_ad_hoc_event:
+ doc.task = task
doc.flags.ignore_permissions = True
+ doc.is_ad_hoc_event = is_ad_hoc_event
doc.project = project
doc.subject = subject
@@ -338,13 +350,17 @@ def start_active_timer(task, project, subject, start_time):
return all_timers
@frappe.whitelist()
-def stop_active_timer(task=None):
+def stop_active_timer(task=None, is_ad_hoc_event=0):
"""
- Stop the active timer for the current user, optionally filtering by task.
+ Stop the active timer for the current user, optionally filtering by task or ad-hoc status.
"""
user = frappe.session.user
+ is_ad_hoc_event = int(is_ad_hoc_event)
filters = {"user": user}
- if task:
+
+ if is_ad_hoc_event:
+ filters["is_ad_hoc_event"] = 1
+ elif task:
filters["task"] = task
timer_names = frappe.get_all("Active Task Timer", filters=filters, pluck="name", ignore_permissions=True)
@@ -367,7 +383,63 @@ def get_active_timer():
return []
timers = frappe.db.sql("""
- SELECT task, project, subject, start_time FROM `tabActive Task Timer` WHERE user = %s
+ SELECT task, project, subject, start_time, is_ad_hoc_event FROM `tabActive Task Timer` WHERE user = %s
""", (user,), as_dict=True)
- return timers
\ No newline at end of file
+ return timers
+
+@frappe.whitelist()
+def check_active_timer():
+ """
+ Check if an active timer exists for the current user, respecting overlap settings.
+ """
+ user = frappe.session.user
+ settings = frappe.db.get_value("Projects Settings", "Projects Settings", ["ignore_employee_time_overlap", "ignore_user_time_overlap"], as_dict=True) or {}
+ ignore_overlap = (int(settings.get("ignore_employee_time_overlap") or 0) == 1) or (int(settings.get("ignore_user_time_overlap") or 0) == 1)
+
+ if not ignore_overlap:
+ existing_timer = frappe.db.sql("""
+ SELECT task, subject FROM `tabActive Task Timer` WHERE user = %s
+ """, (user,), as_dict=True)
+ if existing_timer:
+ existing_timer = existing_timer[0]
+ return _("Another task is already running: {0}. Please stop it before starting a new one.").format(existing_timer.subject or existing_timer.task)
+ return None
+
+@frappe.whitelist()
+def create_event_from_tool(subject, event_category, start_time, company, ends_on, description=None, customer=None):
+ """
+ Create an Event record from the management tool and finalize the associated timer.
+ """
+ user = frappe.session.user
+ employee = frappe.db.get_value("Employee", {"user_id": user}, ["name", "employee_name"], as_dict=True)
+
+ event = frappe.new_doc("Event")
+ event.subject = subject
+ event.event_category = event_category
+ event.description = description
+ event.starts_on = start_time
+ event.ends_on = ends_on
+ event.company = company
+ event.custom_customer = customer
+ event.event_type = "Private"
+ event.status = "Completed"
+ event.send_reminder = 0
+
+ if employee:
+ event.append("event_participants", {
+ "reference_doctype": "Employee",
+ "reference_docname": employee.name,
+ "custom_participant_name": employee.employee_name
+ })
+
+ event.insert()
+
+ # Create timesheet entry via utility
+ from one_compliance.one_compliance.utils import make_time_sheet_entry
+ make_time_sheet_entry(event.name)
+
+ # Stop the ad-hoc event timer
+ stop_active_timer(is_ad_hoc_event=1)
+
+ return event.name
\ No newline at end of file
diff --git a/one_compliance/one_compliance/utils.py b/one_compliance/one_compliance/utils.py
index 162b830f..a1098119 100644
--- a/one_compliance/one_compliance/utils.py
+++ b/one_compliance/one_compliance/utils.py
@@ -365,29 +365,48 @@ def make_time_sheet_entry(event):
if participant.reference_doctype == "Employee":
employee_id = participant.reference_docname
if employee_id:
- create_timesheet(employee_id, activity_type, from_time, to_time)
+ create_timesheet(employee_id, activity_type, from_time, to_time, event)
@frappe.whitelist()
-def create_timesheet(employee, activity_type, from_time, to_time):
+def create_timesheet(employee, activity_type, from_time, to_time, event=None):
from_time = get_datetime(from_time)
to_time = get_datetime(to_time)
employee_id = frappe.get_value("Employee", {"name": employee}, "name")
- # Check if a timesheet already exists for the employee within the given date range
- if frappe.db.exists("Timesheet", {"employee": employee_id, "start_date": from_time.date(), "end_date": to_time.date()}):
- frappe.throw(_("Timesheet already Created"))
+ # Check if a draft timesheet already exists for the employee for today
+ existing_ts = frappe.db.get_value("Timesheet", {
+ "employee": employee_id,
+ "start_date": from_time.date(),
+ "docstatus": 0
+ }, "name")
+
+ if existing_ts:
+ timesheet = frappe.get_doc("Timesheet", existing_ts)
+ # Check if event already logged
+ if event and any(log.event == event for log in timesheet.time_logs):
+ return
+
+ timesheet.append("time_logs", {
+ "activity_type": activity_type,
+ "from_time": from_time,
+ "to_time": to_time,
+ "event": event
+ })
+ timesheet.save(ignore_permissions=True)
else:
timesheet = frappe.new_doc("Timesheet")
timesheet.employee = employee_id
timesheet.append("time_logs",{
"activity_type": activity_type,
"from_time": from_time,
- "to_time": to_time
+ "to_time": to_time,
+ "event": event
})
timesheet.insert(ignore_permissions=True)
- frappe.db.commit()
+
+ frappe.db.commit()
@frappe.whitelist()
def get_employee_list_for_hod():
diff --git a/one_compliance/public/js/one_compliance.js b/one_compliance/public/js/one_compliance.js
index 0031cd5e..cd1ec9f5 100644
--- a/one_compliance/public/js/one_compliance.js
+++ b/one_compliance/public/js/one_compliance.js
@@ -1,12 +1,12 @@
(function () {
- 'use strict';
+ 'use strict';
- const PREFIX = 'one-compliance-active-timer-';
+ const PREFIX = 'one-compliance-active-timer-';
- // =========================
- // CSS Injection
- // =========================
- function injectStyles() {
+ // =========================
+ // CSS Injection
+ // =========================
+ function injectStyles() {
const Z_INDEX = 1040;
const style = document.createElement('style');
@@ -79,225 +79,366 @@
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