// Zoho Projects Tasks API Module // EU data center base: https://projectsapi.zoho.eu var API_BASE = 'https://projectsapi.zoho.eu'; function makeRequest(token, method, path, params) { var portalId = shadowman.config.value('portal_id'); if (!portalId) { return null; // Let the caller handle missing portal ID } // Use Zoho Projects API domain. EU data center: projectsapi.zoho.eu // Note: auth may return api_domain as www.zohoapis.eu (generic), but Projects // requires its own domain. var apiBase = 'https://projectsapi.zoho.eu'; // Replace placeholders in path var url = path.replace('{PORTALID}', portalId); var fullUrl = apiBase + url; var headers = { 'Authorization': 'Zoho-oauthtoken ' + token, 'Content-Type': 'application/x-www-form-urlencoded' }; var body = null; var contentType = 'application/x-www-form-urlencoded'; if (method === 'GET' && params) { var qs = []; for (var key in params) { if (params.hasOwnProperty(key) && params[key] !== undefined) { qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); } } if (qs.length > 0) { fullUrl += '?' + qs.join('&'); } } else if (method === 'POST' && params) { var parts = []; for (var key in params) { if (params.hasOwnProperty(key) && params[key] !== undefined) { parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); } } body = parts.join('&'); } var resp = shadowman.http.request(fullUrl, { method: method, body: body, headers: headers, contentType: contentType }); if (resp.status === 401) { // Token might have expired - signal to retry return { _tokenExpired: true }; } if (resp.status >= 400) { shadowman.log.error('Zoho API error: ' + resp.status + ' ' + resp.body); var errBody; try { errBody = JSON.parse(resp.body); } catch (e) { errBody = { error: resp.body }; } return { error: 'Zoho API error (' + resp.status + '): ' + (errBody.description || errBody.message || errBody.error || resp.body) }; } try { return JSON.parse(resp.body); } catch (e) { return { error: 'Invalid JSON response: ' + resp.body }; } } // Retry wrapper - auto-refreshes token on 401 function apiCall(token, method, path, params) { var result = makeRequest(token, method, path, params); if (result && result._tokenExpired) { // Token expired, trigger a refresh via auth module var auth = require('./auth'); var newToken = auth.getValidToken(); if (!newToken) { return { error: 'Authentication expired and refresh failed. Re-run setup_auth.' }; } return makeRequest(newToken, method, path, params); } return result; } function listProjects(token, index) { var portalId = shadowman.config.value('portal_id'); if (!portalId) { return { error: 'Missing portal_id in plugin config.' }; } return apiCall(token, 'GET', '/restapi/portal/{PORTALID}/projects/', { index: String(index) }); } function listTasks(token, projectId, filters) { var portalId = shadowman.config.value('portal_id'); if (!portalId) { return { error: 'Missing portal_id in plugin config.' }; } var params = {}; if (filters.status) params.status = filters.status; if (filters.priority) params.priority = filters.priority; if (filters.owner) params.owner = filters.owner; if (filters.index) params.index = String(filters.index); if (filters.milestone_id) params.milestone_id = filters.milestone_id; if (filters.tasklist_id) params.tasklist_id = filters.tasklist_id; return apiCall(token, 'GET', '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/' .replace('{PROJECTID}', projectId), params); } function getTask(token, projectId, taskId) { var portalId = shadowman.config.value('portal_id'); if (!portalId) { return { error: 'Missing portal_id in plugin config.' }; } return apiCall(token, 'GET', '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/' .replace('{PROJECTID}', projectId) .replace('{TASKID}', taskId), {}); } function createTask(token, projectId, args) { var portalId = shadowman.config.value('portal_id'); if (!portalId) { return { error: 'Missing portal_id in plugin config.' }; } var params = { name: args.name }; if (args.description) params.description = args.description; if (args.person_responsible) params.person_responsible = String(args.person_responsible); if (args.priority) params.priority = args.priority; if (args.start_date) params.start_date = args.start_date; if (args.end_date) params.end_date = args.end_date; if (args.tasklist_id) params.tasklist_id = String(args.tasklist_id); if (args.milestone_id) params.milestone_id = String(args.milestone_id); if (args.percent_complete) params.percent_complete = String(args.percent_complete); return apiCall(token, 'POST', '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/' .replace('{PROJECTID}', projectId), params); } function updateTask(token, projectId, taskId, args) { var portalId = shadowman.config.value('portal_id'); if (!portalId) { return { error: 'Missing portal_id in plugin config.' }; } var params = {}; if (args.name) params.name = args.name; if (args.description !== undefined) params.description = args.description; if (args.person_responsible) params.person_responsible = String(args.person_responsible); if (args.priority) params.priority = args.priority; if (args.start_date) params.start_date = args.start_date; if (args.end_date) params.end_date = args.end_date; if (args.percent_complete !== undefined) params.percent_complete = String(args.percent_complete); return apiCall(token, 'POST', '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/' .replace('{PROJECTID}', projectId) .replace('{TASKID}', taskId), params); } function deleteTask(token, projectId, taskId) { var portalId = shadowman.config.value('portal_id'); if (!portalId) { return { error: 'Missing portal_id in plugin config.' }; } return apiCall(token, 'DELETE', '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/' .replace('{PROJECTID}', projectId) .replace('{TASKID}', taskId), {}); } function getMyTasks(token, filters) { var portalId = shadowman.config.value('portal_id'); if (!portalId) { return { error: 'Missing portal_id in plugin config.' }; } var params = {}; if (filters.status) params.status = filters.status; if (filters.priority) params.priority = filters.priority; if (filters.index) params.index = String(filters.index); return apiCall(token, 'GET', '/restapi/portal/{PORTALID}/mytasks/', params); } function searchTasks(token, args) { // Zoho Projects doesn't have a dedicated search endpoint, so we search across all projects var portalId = shadowman.config.value('portal_id'); if (!portalId) { return { error: 'Missing portal_id in plugin config.' }; } var query = args.query; var projectId = args.project_id; if (projectId) { // Search within a specific project var result = apiCall(token, 'GET', '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/' .replace('{PROJECTID}', projectId), { index: '1' }); if (result.error) return result; var tasks = result.tasks || []; var matched = []; for (var i = 0; i < tasks.length; i++) { var t = tasks[i]; if (t.name && t.name.toLowerCase().indexOf(query.toLowerCase()) !== -1) { matched.push(t); } else if (t.description && t.description.toLowerCase().indexOf(query.toLowerCase()) !== -1) { matched.push(t); } } return { tasks: matched, total: matched.length, query: query }; } // Search across all projects - first get project list var projects = apiCall(token, 'GET', '/restapi/portal/{PORTALID}/projects/', {}); if (projects.error) return projects; var allTasks = []; var projectList = projects.projects || []; for (var p = 0; p < projectList.length; p++) { var proj = projectList[p]; var taskResult = apiCall(token, 'GET', '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/' .replace('{PROJECTID}', String(proj.id)), { index: '1' }); if (taskResult.error) continue; var tasks = taskResult.tasks || []; for (var i = 0; i < tasks.length; i++) { var t = tasks[i]; if (t.name && t.name.toLowerCase().indexOf(query.toLowerCase()) !== -1) { t._project_name = proj.name; allTasks.push(t); } else if (t.description && t.description.toLowerCase().indexOf(query.toLowerCase()) !== -1) { t._project_name = proj.name; allTasks.push(t); } } } return { tasks: allTasks, total: allTasks.length, query: query }; } module.exports = { listProjects: listProjects, listTasks: listTasks, getTask: getTask, createTask: createTask, updateTask: updateTask, deleteTask: deleteTask, getMyTasks: getMyTasks, searchTasks: searchTasks };