|
@@ -1,72 +1,72 @@
|
|
|
-// Zoho Projects Tasks API Module
|
|
|
|
|
-// EU data center base: https://projectsapi.zoho.eu
|
|
|
|
|
|
|
+// 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://projectsapi.zoho.eu';
|
|
|
|
|
|
|
+var API_BASE = 'https://projects.zoho.eu';
|
|
|
|
|
|
|
|
-function makeRequest(token, method, path, params) {
|
|
|
|
|
|
|
+function makeRequest(token, method, path, jsonBody, queryParams) {
|
|
|
var portalId = shadowman.config.value('portal_id');
|
|
var portalId = shadowman.config.value('portal_id');
|
|
|
if (!portalId) {
|
|
if (!portalId) {
|
|
|
- return null; // Let the caller handle missing portal ID
|
|
|
|
|
|
|
+ return { error: 'Missing portal_id in plugin config. Run resolve_portal first.' };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 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';
|
|
|
|
|
|
|
+ var url = API_BASE + path.replace('{PORTALID}', portalId);
|
|
|
|
|
|
|
|
- // 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) {
|
|
|
|
|
|
|
+ if (queryParams) {
|
|
|
var qs = [];
|
|
var qs = [];
|
|
|
- for (var key in params) {
|
|
|
|
|
- if (params.hasOwnProperty(key) && params[key] !== undefined) {
|
|
|
|
|
- qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
|
|
|
|
|
|
|
+ for (var key in queryParams) {
|
|
|
|
|
+ if (queryParams.hasOwnProperty(key) && queryParams[key] !== undefined) {
|
|
|
|
|
+ qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(queryParams[key]));
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
if (qs.length > 0) {
|
|
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]));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ url += '?' + qs.join('&');
|
|
|
}
|
|
}
|
|
|
- body = parts.join('&');
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- var resp = shadowman.http.request(fullUrl, {
|
|
|
|
|
|
|
+ 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,
|
|
method: method,
|
|
|
- body: body,
|
|
|
|
|
|
|
+ body: hasBody ? JSON.stringify(jsonBody) : null,
|
|
|
headers: headers,
|
|
headers: headers,
|
|
|
- contentType: contentType
|
|
|
|
|
|
|
+ contentType: hasBody ? 'application/json' : null
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
if (resp.status === 401) {
|
|
if (resp.status === 401) {
|
|
|
- // Token might have expired - signal to retry
|
|
|
|
|
return { _tokenExpired: true };
|
|
return { _tokenExpired: true };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (resp.status >= 400) {
|
|
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 };
|
|
|
|
|
|
|
+ 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 + '): ' + (errBody.description || errBody.message || errBody.error || resp.body) };
|
|
|
|
|
|
|
+ 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 {
|
|
try {
|
|
@@ -76,37 +76,47 @@ function makeRequest(token, method, path, params) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// Retry wrapper - auto-refreshes token on 401
|
|
|
|
|
-function apiCall(token, method, path, params) {
|
|
|
|
|
- var result = makeRequest(token, method, path, params);
|
|
|
|
|
|
|
+function apiCall(token, method, path, jsonBody, queryParams) {
|
|
|
|
|
+ var result = makeRequest(token, method, path, jsonBody, queryParams);
|
|
|
if (result && result._tokenExpired) {
|
|
if (result && result._tokenExpired) {
|
|
|
- // Token expired, trigger a refresh via auth module
|
|
|
|
|
var auth = require('./auth');
|
|
var auth = require('./auth');
|
|
|
var newToken = auth.getValidToken();
|
|
var newToken = auth.getValidToken();
|
|
|
if (!newToken) {
|
|
if (!newToken) {
|
|
|
return { error: 'Authentication expired and refresh failed. Re-run setup_auth.' };
|
|
return { error: 'Authentication expired and refresh failed. Re-run setup_auth.' };
|
|
|
}
|
|
}
|
|
|
- return makeRequest(newToken, method, path, params);
|
|
|
|
|
|
|
+ return makeRequest(newToken, method, path, jsonBody, queryParams);
|
|
|
}
|
|
}
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// --- Projects (v1 API — v3 returns 400) ---
|
|
|
|
|
+
|
|
|
function listProjects(token, index) {
|
|
function listProjects(token, index) {
|
|
|
var portalId = shadowman.config.value('portal_id');
|
|
var portalId = shadowman.config.value('portal_id');
|
|
|
if (!portalId) {
|
|
if (!portalId) {
|
|
|
- return { error: 'Missing portal_id in plugin config.' };
|
|
|
|
|
|
|
+ return { error: 'Missing portal_id in plugin config. Run resolve_portal first.' };
|
|
|
}
|
|
}
|
|
|
- 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 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 = {};
|
|
var params = {};
|
|
|
if (filters.status) params.status = filters.status;
|
|
if (filters.status) params.status = filters.status;
|
|
|
if (filters.priority) params.priority = filters.priority;
|
|
if (filters.priority) params.priority = filters.priority;
|
|
@@ -115,187 +125,225 @@ function listTasks(token, projectId, filters) {
|
|
|
if (filters.milestone_id) params.milestone_id = filters.milestone_id;
|
|
if (filters.milestone_id) params.milestone_id = filters.milestone_id;
|
|
|
if (filters.tasklist_id) params.tasklist_id = filters.tasklist_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);
|
|
|
|
|
|
|
+ return apiCall(token, 'GET',
|
|
|
|
|
+ '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks'
|
|
|
|
|
+ .replace('{PROJECTID}', projectId), null, params);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function getTask(token, projectId, taskId) {
|
|
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',
|
|
return apiCall(token, 'GET',
|
|
|
- '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/'
|
|
|
|
|
|
|
+ '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}'
|
|
|
.replace('{PROJECTID}', projectId)
|
|
.replace('{PROJECTID}', projectId)
|
|
|
- .replace('{TASKID}', taskId), {});
|
|
|
|
|
|
|
+ .replace('{TASKID}', taskId), null, {});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function createTask(token, projectId, args) {
|
|
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);
|
|
|
|
|
|
|
+ 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',
|
|
return apiCall(token, 'POST',
|
|
|
- '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/'
|
|
|
|
|
- .replace('{PROJECTID}', projectId), params);
|
|
|
|
|
|
|
+ '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks'
|
|
|
|
|
+ .replace('{PROJECTID}', projectId), body);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function updateTask(token, projectId, taskId, args) {
|
|
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}/'
|
|
|
|
|
|
|
+ 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('{PROJECTID}', projectId)
|
|
|
- .replace('{TASKID}', taskId), params);
|
|
|
|
|
|
|
+ .replace('{TASKID}', taskId), body);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function deleteTask(token, projectId, taskId) {
|
|
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',
|
|
return apiCall(token, 'DELETE',
|
|
|
- '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/'
|
|
|
|
|
|
|
+ '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}'
|
|
|
.replace('{PROJECTID}', projectId)
|
|
.replace('{PROJECTID}', projectId)
|
|
|
- .replace('{TASKID}', taskId), {});
|
|
|
|
|
|
|
+ .replace('{TASKID}', taskId), null, {});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function getMyTasks(token, filters) {
|
|
|
|
|
- var portalId = shadowman.config.value('portal_id');
|
|
|
|
|
- if (!portalId) {
|
|
|
|
|
- return { error: 'Missing portal_id in plugin config.' };
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// --- My Tasks ---
|
|
|
|
|
|
|
|
|
|
+function getMyTasks(token, filters) {
|
|
|
var params = {};
|
|
var params = {};
|
|
|
if (filters.status) params.status = filters.status;
|
|
if (filters.status) params.status = filters.status;
|
|
|
if (filters.priority) params.priority = filters.priority;
|
|
if (filters.priority) params.priority = filters.priority;
|
|
|
if (filters.index) params.index = String(filters.index);
|
|
if (filters.index) params.index = String(filters.index);
|
|
|
|
|
|
|
|
- return apiCall(token, 'GET', '/restapi/portal/{PORTALID}/mytasks/', params);
|
|
|
|
|
|
|
+ return apiCall(token, 'GET', '/api/v3/portal/{PORTALID}/mytasks', null, params);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// --- Search ---
|
|
|
|
|
+
|
|
|
function searchTasks(token, args) {
|
|
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');
|
|
var portalId = shadowman.config.value('portal_id');
|
|
|
- if (!portalId) {
|
|
|
|
|
- return { error: 'Missing portal_id in plugin config.' };
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (!portalId) return { error: 'Missing portal_id in plugin config.' };
|
|
|
|
|
|
|
|
var query = args.query;
|
|
var query = args.query;
|
|
|
var projectId = args.project_id;
|
|
var projectId = args.project_id;
|
|
|
|
|
|
|
|
if (projectId) {
|
|
if (projectId) {
|
|
|
- // Search within a specific project
|
|
|
|
|
var result = apiCall(token, 'GET',
|
|
var result = apiCall(token, 'GET',
|
|
|
- '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/'
|
|
|
|
|
- .replace('{PROJECTID}', projectId), {
|
|
|
|
|
- index: '1'
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
|
|
+ '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks'
|
|
|
|
|
+ .replace('{PROJECTID}', projectId), null, { index: '1' });
|
|
|
if (result.error) return result;
|
|
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 };
|
|
|
|
|
|
|
+ return filterTasks(result.tasks || [], query);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Search across all projects - first get project list
|
|
|
|
|
- var projects = apiCall(token, 'GET', '/restapi/portal/{PORTALID}/projects/', {});
|
|
|
|
|
|
|
+ var projects = listProjects(token, 1);
|
|
|
if (projects.error) return projects;
|
|
if (projects.error) return projects;
|
|
|
|
|
|
|
|
var allTasks = [];
|
|
var allTasks = [];
|
|
|
var projectList = projects.projects || [];
|
|
var projectList = projects.projects || [];
|
|
|
-
|
|
|
|
|
for (var p = 0; p < projectList.length; p++) {
|
|
for (var p = 0; p < projectList.length; p++) {
|
|
|
var proj = projectList[p];
|
|
var proj = projectList[p];
|
|
|
var taskResult = apiCall(token, 'GET',
|
|
var taskResult = apiCall(token, 'GET',
|
|
|
- '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/'
|
|
|
|
|
- .replace('{PROJECTID}', String(proj.id)), {
|
|
|
|
|
- index: '1'
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
|
|
+ '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks'
|
|
|
|
|
+ .replace('{PROJECTID}', String(proj.id)), null, { index: '1' });
|
|
|
if (taskResult.error) continue;
|
|
if (taskResult.error) continue;
|
|
|
-
|
|
|
|
|
var tasks = taskResult.tasks || [];
|
|
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);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ for (var j = 0; j < tasks.length; j++) {
|
|
|
|
|
+ tasks[j]._project_name = proj.name;
|
|
|
}
|
|
}
|
|
|
|
|
+ allTasks = allTasks.concat(tasks);
|
|
|
}
|
|
}
|
|
|
|
|
+ return filterTasks(allTasks, query);
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- return { tasks: allTasks, total: allTasks.length, query: 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) {
|
|
function addComment(token, projectId, taskId, content) {
|
|
|
- var portalId = shadowman.config.value('portal_id');
|
|
|
|
|
- if (!portalId) {
|
|
|
|
|
- return { error: 'Missing portal_id in plugin config.' };
|
|
|
|
|
- }
|
|
|
|
|
return apiCall(token, 'POST',
|
|
return apiCall(token, 'POST',
|
|
|
- '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments/'
|
|
|
|
|
|
|
+ '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments'
|
|
|
.replace('{PROJECTID}', projectId)
|
|
.replace('{PROJECTID}', projectId)
|
|
|
.replace('{TASKID}', taskId), { content: content });
|
|
.replace('{TASKID}', taskId), { content: content });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function updateComment(token, projectId, taskId, commentId, content) {
|
|
function updateComment(token, projectId, taskId, commentId, content) {
|
|
|
- var portalId = shadowman.config.value('portal_id');
|
|
|
|
|
- if (!portalId) {
|
|
|
|
|
- return { error: 'Missing portal_id in plugin config.' };
|
|
|
|
|
- }
|
|
|
|
|
- return apiCall(token, 'POST',
|
|
|
|
|
- '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments/{COMMENTID}/'
|
|
|
|
|
|
|
+ return apiCall(token, 'PUT',
|
|
|
|
|
+ '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments/{COMMENTID}'
|
|
|
.replace('{PROJECTID}', projectId)
|
|
.replace('{PROJECTID}', projectId)
|
|
|
.replace('{TASKID}', taskId)
|
|
.replace('{TASKID}', taskId)
|
|
|
.replace('{COMMENTID}', commentId), { content: content });
|
|
.replace('{COMMENTID}', commentId), { content: content });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function deleteComment(token, projectId, taskId, commentId) {
|
|
function deleteComment(token, projectId, taskId, commentId) {
|
|
|
- var portalId = shadowman.config.value('portal_id');
|
|
|
|
|
- if (!portalId) {
|
|
|
|
|
- return { error: 'Missing portal_id in plugin config.' };
|
|
|
|
|
- }
|
|
|
|
|
return apiCall(token, 'DELETE',
|
|
return apiCall(token, 'DELETE',
|
|
|
- '/restapi/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments/{COMMENTID}/'
|
|
|
|
|
|
|
+ '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments/{COMMENTID}'
|
|
|
.replace('{PROJECTID}', projectId)
|
|
.replace('{PROJECTID}', projectId)
|
|
|
.replace('{TASKID}', taskId)
|
|
.replace('{TASKID}', taskId)
|
|
|
- .replace('{COMMENTID}', commentId), {});
|
|
|
|
|
|
|
+ .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 = {
|
|
module.exports = {
|
|
@@ -309,5 +357,8 @@ module.exports = {
|
|
|
searchTasks: searchTasks,
|
|
searchTasks: searchTasks,
|
|
|
addComment: addComment,
|
|
addComment: addComment,
|
|
|
updateComment: updateComment,
|
|
updateComment: updateComment,
|
|
|
- deleteComment: deleteComment
|
|
|
|
|
|
|
+ deleteComment: deleteComment,
|
|
|
|
|
+ logTime: logTime,
|
|
|
|
|
+ listTimesheets: listTimesheets,
|
|
|
|
|
+ deleteTimesheet: deleteTimesheet
|
|
|
};
|
|
};
|