| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- // Zoho Projects v3 API Module
- // EU data center: https://projects.zoho.eu
- // Collection endpoints: no trailing slash. Resource endpoints: trailing slash optional.
- var API_BASE = 'https://projects.zoho.eu';
- function makeRequest(token, method, path, jsonBody, queryParams) {
- var portalId = shadowman.config.value('portal_id');
- if (!portalId) {
- return { error: 'Missing portal_id in plugin config. Run resolve_portal first.' };
- }
- var url = API_BASE + path.replace('{PORTALID}', portalId);
- if (queryParams) {
- var qs = [];
- for (var key in queryParams) {
- if (queryParams.hasOwnProperty(key) && queryParams[key] !== undefined) {
- qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(queryParams[key]));
- }
- }
- if (qs.length > 0) {
- url += '?' + qs.join('&');
- }
- }
- var hasBody = (jsonBody !== null && jsonBody !== undefined);
- var headers = {
- 'Authorization': 'Zoho-oauthtoken ' + token
- };
- if (hasBody) {
- headers['Content-Type'] = 'application/json';
- }
- var resp = shadowman.http.request(url, {
- method: method,
- body: hasBody ? JSON.stringify(jsonBody) : null,
- headers: headers,
- contentType: hasBody ? 'application/json' : null
- });
- if (resp.status === 401) {
- return { _tokenExpired: true };
- }
- if (resp.status >= 400) {
- var raw = resp.body;
- var msg;
- if (typeof raw === 'string') {
- try {
- var p = JSON.parse(raw);
- msg = p.error && p.error.message ? p.error.message : raw;
- } catch (e) {
- msg = raw;
- }
- } else if (raw && typeof raw === 'object') {
- msg = (raw.error && raw.error.message) || raw.message || raw.description || JSON.stringify(raw);
- } else {
- msg = String(raw);
- }
- return { error: 'Zoho API error (' + resp.status + '): ' + msg };
- }
- if (resp.body && typeof resp.body === 'object') {
- return resp.body;
- }
- if (!resp.body || (typeof resp.body === 'string' && resp.body.trim() === '')) {
- return { success: true };
- }
- try {
- return JSON.parse(resp.body);
- } catch (e) {
- return { error: 'Invalid JSON response: ' + resp.body };
- }
- }
- function apiCall(token, method, path, jsonBody, queryParams) {
- var result = makeRequest(token, method, path, jsonBody, queryParams);
- if (result && result._tokenExpired) {
- 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, jsonBody, queryParams);
- }
- return result;
- }
- // --- Projects (v1 API — v3 returns 400) ---
- function listProjects(token, index) {
- var portalId = shadowman.config.value('portal_id');
- if (!portalId) {
- return { error: 'Missing portal_id in plugin config. Run resolve_portal first.' };
- }
- var url = API_BASE + '/restapi/portal/' + portalId + '/projects/?index=' + encodeURIComponent(String(index));
- var headers = { 'Authorization': 'Zoho-oauthtoken ' + token };
- var resp = shadowman.http.request(url, { method: 'GET', headers: headers });
- if (resp.status === 401) {
- var auth = require('./auth');
- var newToken = auth.getValidToken();
- if (!newToken) return { error: 'Authentication expired and refresh failed. Re-run setup_auth.' };
- headers['Authorization'] = 'Zoho-oauthtoken ' + newToken;
- resp = shadowman.http.request(url, { method: 'GET', headers: headers });
- }
- if (resp.status >= 400) return { error: 'Zoho API error (' + resp.status + '): ' + resp.body };
- try { return JSON.parse(resp.body); } catch (e) { return { error: 'Invalid JSON: ' + resp.body }; }
- }
- // --- Tasks (v3) ---
- function listTasks(token, projectId, filters) {
- 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',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks'
- .replace('{PROJECTID}', projectId), null, params);
- }
- function getTask(token, projectId, taskId) {
- return apiCall(token, 'GET',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}'
- .replace('{PROJECTID}', projectId)
- .replace('{TASKID}', taskId), null, {});
- }
- function createTask(token, projectId, args) {
- var body = { name: args.name };
- if (args.description) body.description = args.description;
- if (args.person_responsible) body.person_responsible = String(args.person_responsible);
- if (args.priority) body.priority = args.priority;
- if (args.start_date) body.start_date = args.start_date;
- if (args.end_date) body.end_date = args.end_date;
- if (args.tasklist_id) body.tasklist_id = String(args.tasklist_id);
- if (args.milestone_id) body.milestone_id = String(args.milestone_id);
- if (args.percent_complete) body.completion_percentage = String(args.percent_complete);
- return apiCall(token, 'POST',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks'
- .replace('{PROJECTID}', projectId), body);
- }
- function updateTask(token, projectId, taskId, args) {
- var body = {};
- if (args.name) body.name = args.name;
- if (args.description !== undefined) body.description = args.description;
- if (args.person_responsible) body.person_responsible = String(args.person_responsible);
- if (args.priority) body.priority = args.priority;
- if (args.start_date) body.start_date = args.start_date;
- if (args.end_date) body.end_date = args.end_date;
- if (args.percent_complete !== undefined) body.completion_percentage = String(args.percent_complete);
- return apiCall(token, 'PATCH',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}'
- .replace('{PROJECTID}', projectId)
- .replace('{TASKID}', taskId), body);
- }
- function deleteTask(token, projectId, taskId) {
- return apiCall(token, 'DELETE',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}'
- .replace('{PROJECTID}', projectId)
- .replace('{TASKID}', taskId), null, {});
- }
- // --- My Tasks ---
- function getMyTasks(token, filters) {
- 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', '/api/v3/portal/{PORTALID}/mytasks', null, params);
- }
- // --- Search ---
- function searchTasks(token, args) {
- 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) {
- var result = apiCall(token, 'GET',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks'
- .replace('{PROJECTID}', projectId), null, { index: '1' });
- if (result.error) return result;
- return filterTasks(result.tasks || [], query);
- }
- var projects = listProjects(token, 1);
- 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',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks'
- .replace('{PROJECTID}', String(proj.id)), null, { index: '1' });
- if (taskResult.error) continue;
- var tasks = taskResult.tasks || [];
- for (var j = 0; j < tasks.length; j++) {
- tasks[j]._project_name = proj.name;
- }
- allTasks = allTasks.concat(tasks);
- }
- return filterTasks(allTasks, query);
- }
- function filterTasks(tasks, query) {
- var matched = [];
- var q = query.toLowerCase();
- for (var i = 0; i < tasks.length; i++) {
- var t = tasks[i];
- if ((t.name && t.name.toLowerCase().indexOf(q) !== -1) ||
- (t.description && t.description.toLowerCase().indexOf(q) !== -1)) {
- matched.push(t);
- }
- }
- return { tasks: matched, total: matched.length, query: query };
- }
- // --- Comments ---
- function addComment(token, projectId, taskId, content) {
- return apiCall(token, 'POST',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments'
- .replace('{PROJECTID}', projectId)
- .replace('{TASKID}', taskId), { content: content });
- }
- function updateComment(token, projectId, taskId, commentId, content) {
- return apiCall(token, 'PUT',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments/{COMMENTID}'
- .replace('{PROJECTID}', projectId)
- .replace('{TASKID}', taskId)
- .replace('{COMMENTID}', commentId), { content: content });
- }
- function deleteComment(token, projectId, taskId, commentId) {
- return apiCall(token, 'DELETE',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments/{COMMENTID}'
- .replace('{PROJECTID}', projectId)
- .replace('{TASKID}', taskId)
- .replace('{COMMENTID}', commentId), null, {});
- }
- // --- Timesheet ---
- function logTime(token, projectId, args) {
- var logDate = args.date || '2026-04-15';
- if (logDate.indexOf('-') === 2) {
- var parts = logDate.split('-');
- logDate = parts[2] + '-' + parts[0] + '-' + parts[1];
- }
- var totalHours = Number(args.hours) + (Number(args.minutes || 0) / 60);
- totalHours = Math.round(totalHours * 100) / 100;
- var body = {
- log_name: args.notes || 'Time logged',
- date: logDate,
- bill_status: 'Billable',
- hours: String(totalHours),
- notes: args.notes || '',
- module: { id: String(args.task_id), type: 'task' },
- for_timer: false,
- frompage: 'taskdetails'
- };
- return apiCall(token, 'POST',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/log'
- .replace('{PROJECTID}', projectId), body);
- }
- function listTimesheets(token, projectId, filters) {
- var params = {};
- if (filters.task_id) params.task_id = String(filters.task_id);
- if (filters.from_date) params.from_date = filters.from_date;
- if (filters.to_date) params.to_date = filters.to_date;
- if (filters.index) params.index = String(filters.index);
- return apiCall(token, 'GET',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/logs'
- .replace('{PROJECTID}', projectId), null, params);
- }
- function deleteTimesheet(token, projectId, logId) {
- // v3 API DELETE — requires JSON body: {module, from_page}
- // NOTE: If this fails with INVALID_INPUTSTREAM, the SDK may not send DELETE body correctly.
- var portalId = shadowman.config.value('portal_id');
- var url = API_BASE + '/api/v3/portal/' + portalId + '/projects/' + projectId + '/logs/' + String(logId);
- var body = JSON.stringify({ module: 'task', from_page: 'timesheetdetails' });
- var headers = {
- 'Authorization': 'Zoho-oauthtoken ' + token,
- 'Content-Type': 'application/json'
- };
- var resp = shadowman.http.request(url, {
- method: 'DELETE',
- body: body,
- headers: headers,
- contentType: 'application/json'
- });
- if (resp.status === 401) {
- var auth = require('./auth');
- var newToken = auth.getValidToken();
- if (!newToken) return { error: 'Auth expired and refresh failed.' };
- headers['Authorization'] = 'Zoho-oauthtoken ' + newToken;
- resp = shadowman.http.request(url, {
- method: 'DELETE',
- body: body,
- headers: headers,
- contentType: 'application/json'
- });
- }
- if (resp.status >= 400) {
- var msg = '';
- if (resp.body && typeof resp.body === 'object') {
- msg = (resp.body.error && resp.body.error.message) || JSON.stringify(resp.body);
- } else {
- msg = String(resp.body);
- }
- return { error: 'Zoho API error (' + resp.status + '): ' + msg };
- }
- return { success: true };
- }
- module.exports = {
- listProjects: listProjects,
- listTasks: listTasks,
- getTask: getTask,
- createTask: createTask,
- updateTask: updateTask,
- deleteTask: deleteTask,
- getMyTasks: getMyTasks,
- searchTasks: searchTasks,
- addComment: addComment,
- updateComment: updateComment,
- deleteComment: deleteComment,
- logTime: logTime,
- listTimesheets: listTimesheets,
- deleteTimesheet: deleteTimesheet
- };
|