| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543 |
- // 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 ---
- 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 params = {};
- if (index) params.index = String(index);
- return apiCall(token, 'GET', '/api/v3/portal/{PORTALID}/projects', null, params);
- }
- function getProject(token, projectId) {
- return apiCall(token, 'GET',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}'
- .replace('{PROJECTID}', projectId), null, {});
- }
- // --- Tasks ---
- 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 !== undefined) body.completion_percentage = String(args.percent_complete);
- // v3 extras
- if (args.status_id) body.status = { id: String(args.status_id) };
- if (args.parent_task_id) body.parental_info = { parent_task_id: String(args.parent_task_id) };
- if (args.billing_type) body.billing_type = args.billing_type;
- 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);
- if (args.status_id) body.status = { id: String(args.status_id) };
- if (args.tasklist_id) body.tasklist = { id: String(args.tasklist_id) };
- if (args.milestone_id) body.milestone_id = String(args.milestone_id);
- if (args.billing_type) body.billing_type = args.billing_type;
- 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, {});
- }
- function listSubtasks(token, projectId, taskId) {
- return apiCall(token, 'GET',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/subtasks'
- .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 listComments(token, projectId, taskId, index) {
- var params = {};
- if (index) params.index = String(index);
- return apiCall(token, 'GET',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments'
- .replace('{PROJECTID}', projectId)
- .replace('{TASKID}', taskId), null, params);
- }
- 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, {});
- }
- // --- Milestones ---
- function listMilestones(token, projectId, filters) {
- var params = {};
- if (filters && filters.status) params.status = filters.status;
- if (filters && filters.index) params.index = String(filters.index);
- return apiCall(token, 'GET',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/milestones'
- .replace('{PROJECTID}', projectId), null, params);
- }
- function createMilestone(token, projectId, args) {
- var body = { name: args.name };
- if (args.start_date) body.start_date = args.start_date;
- if (args.end_date) body.end_date = args.end_date;
- if (args.owner) body.owner = String(args.owner);
- if (args.flag) body.flag = args.flag; // "internal" | "external"
- if (args.milestone_type) body.milestone_type = args.milestone_type;
- return apiCall(token, 'POST',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/milestones'
- .replace('{PROJECTID}', projectId), body);
- }
- function updateMilestone(token, projectId, milestoneId, args) {
- var body = {};
- if (args.name) body.name = args.name;
- if (args.start_date) body.start_date = args.start_date;
- if (args.end_date) body.end_date = args.end_date;
- if (args.owner) body.owner = String(args.owner);
- if (args.flag) body.flag = args.flag;
- if (args.status) body.status = args.status;
- return apiCall(token, 'PATCH',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/milestones/{MILESTONEID}'
- .replace('{PROJECTID}', projectId)
- .replace('{MILESTONEID}', milestoneId), body);
- }
- function deleteMilestone(token, projectId, milestoneId) {
- return apiCall(token, 'DELETE',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/milestones/{MILESTONEID}'
- .replace('{PROJECTID}', projectId)
- .replace('{MILESTONEID}', milestoneId), null, {});
- }
- // --- Tasklists ---
- function listTasklists(token, projectId, filters) {
- var params = {};
- if (filters && filters.flag) params.flag = filters.flag;
- if (filters && filters.index) params.index = String(filters.index);
- if (projectId) {
- return apiCall(token, 'GET',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasklists'
- .replace('{PROJECTID}', projectId), null, params);
- }
- // Portal-wide
- return apiCall(token, 'GET',
- '/api/v3/portal/{PORTALID}/all-tasklists', null, params);
- }
- function getTasklist(token, projectId, tasklistId) {
- return apiCall(token, 'GET',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasklists/{TASKLISTID}'
- .replace('{PROJECTID}', projectId)
- .replace('{TASKLISTID}', tasklistId), null, {});
- }
- function createTasklist(token, projectId, args) {
- var body = { name: args.name };
- if (args.milestone_id) body.milestone = { id: String(args.milestone_id) };
- if (args.flag) body.flag = args.flag; // "internal" | "external"
- if (args.status) body.status = args.status; // "active" | "archived"
- return apiCall(token, 'POST',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasklists'
- .replace('{PROJECTID}', projectId), body);
- }
- function updateTasklist(token, projectId, tasklistId, args) {
- var body = {};
- if (args.name) body.name = args.name;
- if (args.milestone_id) body.milestone = { id: String(args.milestone_id) };
- if (args.flag) body.flag = args.flag;
- if (args.status) body.status = args.status;
- return apiCall(token, 'PATCH',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasklists/{TASKLISTID}'
- .replace('{PROJECTID}', projectId)
- .replace('{TASKLISTID}', tasklistId), body);
- }
- function deleteTasklist(token, projectId, tasklistId) {
- return apiCall(token, 'DELETE',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasklists/{TASKLISTID}'
- .replace('{PROJECTID}', projectId)
- .replace('{TASKLISTID}', tasklistId), null, {});
- }
- // --- Users ---
- // Returns zpuid + name + email for every portal user. Needed before
- // assigning a task so you can resolve a name/email to a zpuid.
- function listUsers(token, filters) {
- var params = {};
- if (filters && filters.index) params.index = String(filters.index);
- if (filters && filters.user_type) params.user_type = filters.user_type;
- return apiCall(token, 'GET',
- '/api/v3/portal/{PORTALID}/users', null, params);
- }
- // --- Task Statuses ---
- // v3 task body expects status = { id: <statusId> }. IDs are project-local,
- // so call this first if you don't already know the target status's id.
- function listTaskStatuses(token, projectId) {
- return apiCall(token, 'GET',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/taskstatuses'
- .replace('{PROJECTID}', projectId), null, {});
- }
- // --- Timesheet ---
- function logTime(token, projectId, args) {
- var logDate = args.date;
- if (!logDate) {
- var now = new Date();
- logDate = now.getFullYear() + '-' +
- String(now.getMonth() + 1).padStart(2, '0') + '-' +
- String(now.getDate()).padStart(2, '0');
- } else if (logDate.length === 10 && logDate.charAt(2) === '-') {
- // MM-DD-YYYY → YYYY-MM-DD
- 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: args.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 updateTimesheet(token, projectId, logId, args) {
- var body = { module: 'task', from_page: 'timesheetdetails' };
- if (args.hours !== undefined) {
- var h = Number(args.hours) + (Number(args.minutes || 0) / 60);
- body.hours = String(Math.round(h * 100) / 100);
- }
- if (args.notes !== undefined) body.notes = args.notes;
- if (args.log_name !== undefined) body.log_name = args.log_name;
- if (args.bill_status) body.bill_status = args.bill_status;
- if (args.date) {
- var d = args.date;
- if (d.length === 10 && d.charAt(2) === '-') {
- var parts = d.split('-');
- d = parts[2] + '-' + parts[0] + '-' + parts[1];
- }
- body.date = d;
- }
- return apiCall(token, 'PATCH',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/logs/{LOGID}'
- .replace('{PROJECTID}', projectId)
- .replace('{LOGID}', String(logId)), body);
- }
- function listTimesheets(token, projectId, filters) {
- var portalId = shadowman.config.value('portal_id');
- if (!portalId) return { error: 'Missing portal_id in plugin config.' };
- var params = { view_type: 'customdate' };
- if (filters.from_date) {
- var parts = filters.from_date.split('-');
- if (parts.length === 3) params.start_date = parts[2] + '-' + parts[0] + '-' + parts[1];
- else params.start_date = filters.from_date;
- } else {
- var d = new Date();
- d.setDate(d.getDate() - 30);
- params.start_date = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
- }
- if (filters.to_date) {
- var parts2 = filters.to_date.split('-');
- if (parts2.length === 3) params.end_date = parts2[2] + '-' + parts2[0] + '-' + parts2[1];
- else params.end_date = filters.to_date;
- } else {
- var today = new Date();
- params.end_date = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0');
- }
- if (filters.index) params.page = String(filters.index);
- if (filters.task_id) {
- params.module = JSON.stringify({ id: String(filters.task_id), type: 'task' });
- } else {
- params.module = JSON.stringify({ type: 'task' });
- }
- var url = '/api/v3/portal/{PORTALID}/timelogs';
- if (projectId) {
- url = '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/timelogs'
- .replace('{PROJECTID}', projectId);
- }
- return apiCall(token, 'GET', url, null, params);
- }
- function deleteTimesheet(token, projectId, logId) {
- return apiCall(token, 'DELETE',
- '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/logs/{LOGID}'
- .replace('{PROJECTID}', projectId)
- .replace('{LOGID}', String(logId)), { module: 'task', from_page: 'timesheetdetails' }, {});
- }
- module.exports = {
- // Projects
- listProjects: listProjects,
- getProject: getProject,
- // Tasks
- listTasks: listTasks,
- getTask: getTask,
- createTask: createTask,
- updateTask: updateTask,
- deleteTask: deleteTask,
- listSubtasks: listSubtasks,
- getMyTasks: getMyTasks,
- searchTasks: searchTasks,
- // Comments
- listComments: listComments,
- addComment: addComment,
- updateComment: updateComment,
- deleteComment: deleteComment,
- // Milestones
- listMilestones: listMilestones,
- createMilestone: createMilestone,
- updateMilestone: updateMilestone,
- deleteMilestone: deleteMilestone,
- // Tasklists
- listTasklists: listTasklists,
- getTasklist: getTasklist,
- createTasklist: createTasklist,
- updateTasklist: updateTasklist,
- deleteTasklist: deleteTasklist,
- // Users + Statuses
- listUsers: listUsers,
- listTaskStatuses: listTaskStatuses,
- // Timesheet
- logTime: logTime,
- updateTimesheet: updateTimesheet,
- listTimesheets: listTimesheets,
- deleteTimesheet: deleteTimesheet
- };
|