Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions models/accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,18 +444,19 @@ function deleteAccountAndRelatedData(userId) {
"notification"
];

var numericUserId = parseInt(userId, 10);
for (let i = 0; i < tables.length; i++) {
const table = tables[i];
DBCommon.log("Deleting data from account " + userId);
DBCommon.log("Deleting data from account " + numericUserId);
try {
tx.executeSql(`DELETE FROM ${table} WHERE account_id = ?`, [userId]);
tx.executeSql(`DELETE FROM ${table} WHERE account_id = ?`, [numericUserId]);
} catch (tableErr) {
DBCommon.log("Could not delete from table " + table + ": " + tableErr);
}
}

DBCommon.log(`Deleting user from users table where id = ${userId}`);
tx.executeSql("DELETE FROM users WHERE id = ?", [userId]);
DBCommon.log(`Deleting user from users table where id = ${numericUserId}`);
tx.executeSql("DELETE FROM users WHERE id = ?", [numericUserId]);

// Ensure a valid default account exists
var defaultCheck = tx.executeSql("SELECT id FROM users WHERE is_default = 1 LIMIT 1");
Expand All @@ -466,11 +467,20 @@ function deleteAccountAndRelatedData(userId) {
}
}

DBCommon.log(`Account and related data deleted for account_id: ${userId}`);
// Post-deletion sweep across all tables to purge any unlinked records
for (let j = 0; j < tables.length; j++) {
try {
tx.executeSql(`DELETE FROM ${tables[j]} WHERE account_id NOT IN (SELECT id FROM users)`);
} catch (sweepErr) {
// Ignore sweep errors for optional tables
}
}

DBCommon.log(`Account and related data deleted for account_id: ${numericUserId}`);
});

} catch (e) {
DBCommon.logException(e);
DBCommon.logException("Accounts", e);
}
}

Expand Down
52 changes: 50 additions & 2 deletions models/dbinit.js
Original file line number Diff line number Diff line change
Expand Up @@ -647,10 +647,15 @@ function initializeDatabase() {
}


Logger.debug("Dbinit", "Database initialization complete")
}

function performPostStartupMaintenance() {
Logger.debug("Dbinit", "Performing post-startup database maintenance...")
purgeCache();
syncDraftFlags();

Logger.debug("Dbinit", "Database initialization complete")
cleanupOrphanAccountData();
Logger.debug("Dbinit", "Post-startup database maintenance complete")
}

/**
Expand Down Expand Up @@ -802,3 +807,46 @@ function syncDraftFlags() {
}
}
}

function cleanupOrphanAccountData() {
try {
var db = Sql.LocalStorage.openDatabaseSync(
DBCommon.NAME,
DBCommon.VERSION,
DBCommon.DISPLAY_NAME,
DBCommon.SIZE
);
db.transaction(function (tx) {
var tables = [
"sync_report",
"project_project_app",
"project_task_app",
"account_analytic_line_app",
"res_users_app",
"mail_activity_type_app",
"ir_model_app",
"mail_activity_app",
"ir_attachment_app",
"project_task_assignee_app",
"project_update_app",
"project_task_type_app",
"project_project_stage_app",
"attachment_download_app",
"form_drafts",
"notification"
];

for (var i = 0; i < tables.length; i++) {
try {
tx.executeSql("DELETE FROM " + tables[i] + " WHERE account_id NOT IN (SELECT id FROM users)");
} catch (tableErr) {
Logger.debug("Dbinit", "Could not purge orphan data from " + tables[i] + ": " + tableErr);
}
}
});
Logger.debug("Dbinit", "Orphan account data cleanup complete");
} catch (e) {
Logger.error("Dbinit", "cleanupOrphanAccountData failed:", e);
}
}

10 changes: 7 additions & 3 deletions models/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ function getAllProjectUpdates(accountId) {
Logger.debug("Project", "Fetching project updates for account:", accountId)
} else {

query = "SELECT * FROM project_update_app WHERE status != 'deleted' ORDER BY date DESC";
query = "SELECT * FROM project_update_app WHERE status != 'deleted' AND account_id IN (SELECT id FROM users) ORDER BY date DESC";
result = tx.executeSql(query);
Logger.debug("Project", "Fetching all project updates (no account filter)")
}
Expand Down Expand Up @@ -712,7 +712,7 @@ function getAllProjectsPaginated(limit, offset) {
var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE);

db.transaction(function (tx) {
var query = "SELECT * FROM project_project_app ORDER BY name COLLATE NOCASE ASC LIMIT ? OFFSET ?";
var query = "SELECT * FROM project_project_app WHERE account_id IN (SELECT id FROM users) ORDER BY name COLLATE NOCASE ASC LIMIT ? OFFSET ?";
var result = tx.executeSql(query, [limit, offset]);

for (var i = 0; i < result.rows.length; i++) {
Expand Down Expand Up @@ -756,6 +756,8 @@ function getProjectsFilteredPaginated(options) {
if (options.accountId !== undefined && options.accountId >= 0) {
whereClauses.push("account_id = ?");
params.push(options.accountId);
} else {
whereClauses.push("account_id IN (SELECT id FROM users)");
}

// Stage filter
Expand Down Expand Up @@ -871,7 +873,7 @@ function getAllProjectUpdatesPaginated(accountId, limit, offset) {
query = "SELECT * FROM project_update_app WHERE status != 'deleted' AND account_id = ? ORDER BY date DESC LIMIT ? OFFSET ?";
result = tx.executeSql(query, [accountId, limit, offset]);
} else {
query = "SELECT * FROM project_update_app WHERE status != 'deleted' ORDER BY date DESC LIMIT ? OFFSET ?";
query = "SELECT * FROM project_update_app WHERE status != 'deleted' AND account_id IN (SELECT id FROM users) ORDER BY date DESC LIMIT ? OFFSET ?";
result = tx.executeSql(query, [limit, offset]);
}

Expand Down Expand Up @@ -917,6 +919,8 @@ function getProjectUpdatesFilteredPaginated(options) {
if (options.accountId !== undefined && options.accountId !== null && options.accountId >= 0) {
whereClauses.push("u.account_id = ?");
params.push(options.accountId);
} else {
whereClauses.push("u.account_id IN (SELECT id FROM users)");
}

// Project filter (when viewing updates for a specific project)
Expand Down
166 changes: 129 additions & 37 deletions models/task.js
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ function markTaskAsDeleted(taskId, forceDelete = false) {
db.transaction(function (tx) {
// First, get the task details
var taskResult = tx.executeSql(
"SELECT id, name, project_id, odoo_record_id FROM project_task_app WHERE id = ? AND (status IS NULL OR status != 'deleted')",
"SELECT id, name, project_id, account_id, odoo_record_id FROM project_task_app WHERE id = ? AND (status IS NULL OR status != 'deleted')",
[taskId]
);

Expand All @@ -540,8 +540,8 @@ function markTaskAsDeleted(taskId, forceDelete = false) {

Logger.debug("Task", "Attempting to delete task: '" + taskName + "' (Local ID: " + taskId + ", Odoo ID: " + taskOdooRecordId + ")")

// Check for child tasks using both possible parent reference methods
var childTasks = getChildTasks(tx, taskId, taskOdooRecordId);
// Check for child tasks using both possible parent reference methods, scoped to account
var childTasks = getChildTasks(tx, taskId, taskOdooRecordId, taskRow.account_id);

if (childTasks.length > 0 && !forceDelete) {
// Prevent deletion - task has children
Expand Down Expand Up @@ -666,21 +666,25 @@ function markMultipleTasksAsDeleted(taskIds, forceDelete = false) {
* @param {Object} tx - Database transaction object
* @param {number} parentLocalId - The local 'id' of the parent task
* @param {number} parentOdooRecordId - The 'odoo_record_id' of the parent task
* @param {number} accountId - Optional account ID filter
* @returns {Array<Object>} Array of child task objects
*/
function getChildTasks(tx, parentLocalId, parentOdooRecordId) {
function getChildTasks(tx, parentLocalId, parentOdooRecordId, accountId) {
var childTasks = [];
var seenIds = new Set(); // To prevent duplicates
var seenIds = new Set();

try {
// Method 1: Check if parent_id references local 'id' field
var childResult1 = tx.executeSql(
"SELECT id, name, odoo_record_id FROM project_task_app WHERE parent_id = ? AND (status IS NULL OR status != 'deleted')",
[parentLocalId]
);
var query = "SELECT id, name, odoo_record_id FROM project_task_app WHERE (status IS NULL OR status != 'deleted') AND (parent_id = ? OR (parent_id = ? AND ? > 0))";
var params = [parentLocalId, parentOdooRecordId || 0, parentOdooRecordId || 0];

if (accountId !== undefined && accountId !== null && accountId >= 0) {
query += " AND account_id = ?";
params.push(accountId);
}

for (var i = 0; i < childResult1.rows.length; i++) {
var row = childResult1.rows.item(i);
var result = tx.executeSql(query, params);
for (var i = 0; i < result.rows.length; i++) {
var row = result.rows.item(i);
if (!seenIds.has(row.id)) {
childTasks.push({
id: row.id,
Expand All @@ -690,29 +694,8 @@ function getChildTasks(tx, parentLocalId, parentOdooRecordId) {
seenIds.add(row.id);
}
}

// Method 2: Check if parent_id references 'odoo_record_id' field
if (parentOdooRecordId && parentOdooRecordId > 0) {
var childResult2 = tx.executeSql(
"SELECT id, name, odoo_record_id FROM project_task_app WHERE parent_id = ? AND (status IS NULL OR status != 'deleted')",
[parentOdooRecordId]
);

for (var j = 0; j < childResult2.rows.length; j++) {
var row2 = childResult2.rows.item(j);
if (!seenIds.has(row2.id)) {
childTasks.push({
id: row2.id,
name: row2.name,
odoo_record_id: row2.odoo_record_id
});
seenIds.add(row2.id);
}
}
}

} catch (e) {
Logger.error("Task", "Error getting child tasks:", e)
Logger.error("Task", "Error getting child tasks:", e);
}

return childTasks;
Expand All @@ -730,13 +713,13 @@ function checkTaskHasChildren(taskId) {

db.transaction(function (tx) {
var taskResult = tx.executeSql(
"SELECT id, name, odoo_record_id FROM project_task_app WHERE id = ?",
"SELECT id, name, odoo_record_id, account_id FROM project_task_app WHERE id = ?",
[taskId]
);

if (taskResult.rows.length > 0) {
var taskRow = taskResult.rows.item(0);
var childTasks = getChildTasks(tx, taskRow.id, taskRow.odoo_record_id);
var childTasks = getChildTasks(tx, taskRow.id, taskRow.odoo_record_id, taskRow.account_id);

result.hasChildren = childTasks.length > 0;
result.childCount = childTasks.length;
Expand All @@ -747,7 +730,7 @@ function checkTaskHasChildren(taskId) {
return result;

} catch (e) {
Logger.error("Task", "Error checking task children:", e)
Logger.error("Task", "Error checking task children:", e);
return { hasChildren: false, childCount: 0, childTasks: [], error: e.message };
}
}
Expand Down Expand Up @@ -1544,6 +1527,115 @@ function getTasksByParentIdPaginated(parentId, accountId, limit, offset, dateFil
return taskList;
}

/**
* Retrieves direct subtasks for a given parent task, strictly scoped to account and optional project.
*
* @param {number} parentId - The parent task ID (local ID or odoo_record_id).
* @param {number} accountId - Optional account ID filter.
* @param {number} projectOdooRecordId - Optional project ID filter.
* @returns {Array<Object>} List of subtask objects.
*/
function getSubtasksForParent(parentId, accountId, projectOdooRecordId) {
var subtaskList = [];
if (!parentId || parentId <= 0) {
return subtaskList;
}

try {
var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE);
db.transaction(function (tx) {
var accId = accountId;
if (accId === undefined || accId === null || accId < 0) {
var detectAccRes = tx.executeSql(
"SELECT account_id FROM project_task_app WHERE (id = ? OR parent_id = ? OR odoo_record_id = ?) AND (status IS NULL OR status != 'deleted') AND account_id IS NOT NULL AND account_id >= 0 ORDER BY CASE WHEN id = ? THEN 0 WHEN parent_id = ? THEN 1 ELSE 2 END LIMIT 1",
[parentId, parentId, parentId, parentId, parentId]
);
if (detectAccRes.rows.length > 0) {
accId = detectAccRes.rows.item(0).account_id;
}
}

// Look up parent task row to find local ID, odoo_record_id, and account_id
var parentQuery = "SELECT id, odoo_record_id, account_id FROM project_task_app WHERE (odoo_record_id = ? OR id = ?)";
var parentParams = [parentId, parentId];
if (accId !== undefined && accId !== null && accId >= 0) {
parentQuery += " AND account_id = ?";
parentParams.push(accId);
}
parentQuery += " ORDER BY CASE WHEN odoo_record_id = ? THEN 0 ELSE 1 END LIMIT 1";
parentParams.push(parentId);

var parentRes = tx.executeSql(parentQuery, parentParams);
var localPid = parentId;
var odooPid = 0;

if (parentRes.rows.length > 0) {
var pRow = parentRes.rows.item(0);
localPid = pRow.id;
odooPid = pRow.odoo_record_id || 0;
if (accId === undefined || accId === null || accId < 0) {
accId = pRow.account_id;
}
}

var query = "SELECT * FROM project_task_app WHERE (status IS NULL OR status != 'deleted') AND (parent_id = ? OR (parent_id = ? AND ? > 0))";
var params = [localPid, odooPid, odooPid];

if (accId !== undefined && accId !== null && accId >= 0) {
query += " AND account_id = ?";
params.push(accId);
}

if (projectOdooRecordId !== undefined && projectOdooRecordId !== null && projectOdooRecordId > 0) {
query += " AND (project_id = ? OR sub_project_id = ?)";
params.push(projectOdooRecordId, projectOdooRecordId);
}

query += " ORDER BY end_date ASC";

// Build projectColorMap for project color resolution
var projectColorMap = {};
var projectQuery = "SELECT odoo_record_id, color_pallet FROM project_project_app";
var projectResult = tx.executeSql(projectQuery);
for (var j = 0; j < projectResult.rows.length; j++) {
var projectRow = projectResult.rows.item(j);
projectColorMap[projectRow.odoo_record_id] = projectRow.color_pallet;
}

var result = tx.executeSql(query, params);
for (var i = 0; i < result.rows.length; i++) {
var task = DBCommon.rowToObject(result.rows.item(i));

// Inherit color from sub_project, project, or walk up hierarchy
var inheritedColor = 0;
if (task.sub_project_id) {
inheritedColor = resolveProjectColor(task.sub_project_id, projectColorMap, tx);
}
if (!inheritedColor && task.project_id) {
inheritedColor = resolveProjectColor(task.project_id, projectColorMap, tx);
}
task.color_pallet = inheritedColor;

// Calculate total hours spent from timesheet entries
var effectiveTaskId = (task.account_id === 0 || !task.odoo_record_id) ? task.id : task.odoo_record_id;
var timeQuery = "SELECT SUM(unit_amount) as total_hours FROM account_analytic_line_app WHERE (status IS NULL OR status != 'deleted') AND (task_id = ? OR sub_task_id = ?) AND account_id = ?";
var timeParams = [effectiveTaskId, effectiveTaskId, task.account_id];
var timeResult = tx.executeSql(timeQuery, timeParams);
if (timeResult.rows.length > 0 && timeResult.rows.item(0).total_hours !== null) {
task.spent_hours = timeResult.rows.item(0).total_hours;
} else {
task.spent_hours = 0;
}

subtaskList.push(task);
}
});
} catch (e) {
Logger.error("Task", "getSubtasksForParent failed:", e);
}
return subtaskList;
}

/**
* Retrieves all non-deleted tasks from the `project_task_app` table,
* and adds inherited color and total hours spent from timesheet entries.
Expand Down
Loading
Loading