tasks.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. // Zoho Projects v3 API Module
  2. // EU data center: https://projects.zoho.eu
  3. // Collection endpoints: no trailing slash. Resource endpoints: trailing slash optional.
  4. var API_BASE = 'https://projects.zoho.eu';
  5. function makeRequest(token, method, path, jsonBody, queryParams) {
  6. var portalId = shadowman.config.value('portal_id');
  7. if (!portalId) {
  8. return { error: 'Missing portal_id in plugin config. Run resolve_portal first.' };
  9. }
  10. var url = API_BASE + path.replace('{PORTALID}', portalId);
  11. if (queryParams) {
  12. var qs = [];
  13. for (var key in queryParams) {
  14. if (queryParams.hasOwnProperty(key) && queryParams[key] !== undefined) {
  15. qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(queryParams[key]));
  16. }
  17. }
  18. if (qs.length > 0) {
  19. url += '?' + qs.join('&');
  20. }
  21. }
  22. var hasBody = (jsonBody !== null && jsonBody !== undefined);
  23. var headers = {
  24. 'Authorization': 'Zoho-oauthtoken ' + token
  25. };
  26. if (hasBody) {
  27. headers['Content-Type'] = 'application/json';
  28. }
  29. var resp = shadowman.http.request(url, {
  30. method: method,
  31. body: hasBody ? JSON.stringify(jsonBody) : null,
  32. headers: headers,
  33. contentType: hasBody ? 'application/json' : null
  34. });
  35. if (resp.status === 401) {
  36. return { _tokenExpired: true };
  37. }
  38. if (resp.status >= 400) {
  39. var raw = resp.body;
  40. var msg;
  41. if (typeof raw === 'string') {
  42. try {
  43. var p = JSON.parse(raw);
  44. msg = p.error && p.error.message ? p.error.message : raw;
  45. } catch (e) {
  46. msg = raw;
  47. }
  48. } else if (raw && typeof raw === 'object') {
  49. msg = (raw.error && raw.error.message) || raw.message || raw.description || JSON.stringify(raw);
  50. } else {
  51. msg = String(raw);
  52. }
  53. return { error: 'Zoho API error (' + resp.status + '): ' + msg };
  54. }
  55. if (resp.body && typeof resp.body === 'object') {
  56. return resp.body;
  57. }
  58. if (!resp.body || (typeof resp.body === 'string' && resp.body.trim() === '')) {
  59. return { success: true };
  60. }
  61. try {
  62. return JSON.parse(resp.body);
  63. } catch (e) {
  64. return { error: 'Invalid JSON response: ' + resp.body };
  65. }
  66. }
  67. function apiCall(token, method, path, jsonBody, queryParams) {
  68. var result = makeRequest(token, method, path, jsonBody, queryParams);
  69. if (result && result._tokenExpired) {
  70. var auth = require('./auth');
  71. var newToken = auth.getValidToken();
  72. if (!newToken) {
  73. return { error: 'Authentication expired and refresh failed. Re-run setup_auth.' };
  74. }
  75. return makeRequest(newToken, method, path, jsonBody, queryParams);
  76. }
  77. return result;
  78. }
  79. // --- Projects (v1 API — v3 returns 400) ---
  80. function listProjects(token, index) {
  81. var portalId = shadowman.config.value('portal_id');
  82. if (!portalId) {
  83. return { error: 'Missing portal_id in plugin config. Run resolve_portal first.' };
  84. }
  85. var params = {};
  86. if (index) params.index = String(index);
  87. return apiCall(token, 'GET',
  88. '/api/v3/portal/{PORTALID}/projects'
  89. .replace('{PORTALID}', portalId), null, params);
  90. }
  91. // --- Tasks (v3) ---
  92. function listTasks(token, projectId, filters) {
  93. var params = {};
  94. if (filters.status) params.status = filters.status;
  95. if (filters.priority) params.priority = filters.priority;
  96. if (filters.owner) params.owner = filters.owner;
  97. if (filters.index) params.index = String(filters.index);
  98. if (filters.milestone_id) params.milestone_id = filters.milestone_id;
  99. if (filters.tasklist_id) params.tasklist_id = filters.tasklist_id;
  100. return apiCall(token, 'GET',
  101. '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks'
  102. .replace('{PROJECTID}', projectId), null, params);
  103. }
  104. function getTask(token, projectId, taskId) {
  105. return apiCall(token, 'GET',
  106. '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}'
  107. .replace('{PROJECTID}', projectId)
  108. .replace('{TASKID}', taskId), null, {});
  109. }
  110. function createTask(token, projectId, args) {
  111. var body = { name: args.name };
  112. if (args.description) body.description = args.description;
  113. if (args.person_responsible) body.person_responsible = String(args.person_responsible);
  114. if (args.priority) body.priority = args.priority;
  115. if (args.start_date) body.start_date = args.start_date;
  116. if (args.end_date) body.end_date = args.end_date;
  117. if (args.tasklist_id) body.tasklist_id = String(args.tasklist_id);
  118. if (args.milestone_id) body.milestone_id = String(args.milestone_id);
  119. if (args.percent_complete) body.completion_percentage = String(args.percent_complete);
  120. return apiCall(token, 'POST',
  121. '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks'
  122. .replace('{PROJECTID}', projectId), body);
  123. }
  124. function updateTask(token, projectId, taskId, args) {
  125. var body = {};
  126. if (args.name) body.name = args.name;
  127. if (args.description !== undefined) body.description = args.description;
  128. if (args.person_responsible) body.person_responsible = String(args.person_responsible);
  129. if (args.priority) body.priority = args.priority;
  130. if (args.start_date) body.start_date = args.start_date;
  131. if (args.end_date) body.end_date = args.end_date;
  132. if (args.percent_complete !== undefined) body.completion_percentage = String(args.percent_complete);
  133. return apiCall(token, 'PATCH',
  134. '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}'
  135. .replace('{PROJECTID}', projectId)
  136. .replace('{TASKID}', taskId), body);
  137. }
  138. function deleteTask(token, projectId, taskId) {
  139. return apiCall(token, 'DELETE',
  140. '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}'
  141. .replace('{PROJECTID}', projectId)
  142. .replace('{TASKID}', taskId), null, {});
  143. }
  144. // --- My Tasks ---
  145. function getMyTasks(token, filters) {
  146. var params = {};
  147. if (filters.status) params.status = filters.status;
  148. if (filters.priority) params.priority = filters.priority;
  149. if (filters.index) params.index = String(filters.index);
  150. return apiCall(token, 'GET', '/api/v3/portal/{PORTALID}/mytasks', null, params);
  151. }
  152. // --- Search ---
  153. function searchTasks(token, args) {
  154. var portalId = shadowman.config.value('portal_id');
  155. if (!portalId) return { error: 'Missing portal_id in plugin config.' };
  156. var query = args.query;
  157. var projectId = args.project_id;
  158. if (projectId) {
  159. var result = apiCall(token, 'GET',
  160. '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks'
  161. .replace('{PROJECTID}', projectId), null, { index: '1' });
  162. if (result.error) return result;
  163. return filterTasks(result.tasks || [], query);
  164. }
  165. var projects = listProjects(token, 1);
  166. if (projects.error) return projects;
  167. var allTasks = [];
  168. var projectList = projects.projects || [];
  169. for (var p = 0; p < projectList.length; p++) {
  170. var proj = projectList[p];
  171. var taskResult = apiCall(token, 'GET',
  172. '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks'
  173. .replace('{PROJECTID}', String(proj.id)), null, { index: '1' });
  174. if (taskResult.error) continue;
  175. var tasks = taskResult.tasks || [];
  176. for (var j = 0; j < tasks.length; j++) {
  177. tasks[j]._project_name = proj.name;
  178. }
  179. allTasks = allTasks.concat(tasks);
  180. }
  181. return filterTasks(allTasks, query);
  182. }
  183. function filterTasks(tasks, query) {
  184. var matched = [];
  185. var q = query.toLowerCase();
  186. for (var i = 0; i < tasks.length; i++) {
  187. var t = tasks[i];
  188. if ((t.name && t.name.toLowerCase().indexOf(q) !== -1) ||
  189. (t.description && t.description.toLowerCase().indexOf(q) !== -1)) {
  190. matched.push(t);
  191. }
  192. }
  193. return { tasks: matched, total: matched.length, query: query };
  194. }
  195. // --- Comments ---
  196. function addComment(token, projectId, taskId, content) {
  197. return apiCall(token, 'POST',
  198. '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments'
  199. .replace('{PROJECTID}', projectId)
  200. .replace('{TASKID}', taskId), { content: content });
  201. }
  202. function updateComment(token, projectId, taskId, commentId, content) {
  203. return apiCall(token, 'PUT',
  204. '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments/{COMMENTID}'
  205. .replace('{PROJECTID}', projectId)
  206. .replace('{TASKID}', taskId)
  207. .replace('{COMMENTID}', commentId), { content: content });
  208. }
  209. function deleteComment(token, projectId, taskId, commentId) {
  210. return apiCall(token, 'DELETE',
  211. '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/tasks/{TASKID}/comments/{COMMENTID}'
  212. .replace('{PROJECTID}', projectId)
  213. .replace('{TASKID}', taskId)
  214. .replace('{COMMENTID}', commentId), null, {});
  215. }
  216. // --- Timesheet ---
  217. function logTime(token, projectId, args) {
  218. var logDate = args.date || '2026-04-15';
  219. if (logDate.indexOf('-') === 2) {
  220. var parts = logDate.split('-');
  221. logDate = parts[2] + '-' + parts[0] + '-' + parts[1];
  222. }
  223. var totalHours = Number(args.hours) + (Number(args.minutes || 0) / 60);
  224. totalHours = Math.round(totalHours * 100) / 100;
  225. var body = {
  226. log_name: args.notes || 'Time logged',
  227. date: logDate,
  228. bill_status: 'Billable',
  229. hours: String(totalHours),
  230. notes: args.notes || '',
  231. module: { id: String(args.task_id), type: 'task' },
  232. for_timer: false,
  233. frompage: 'taskdetails'
  234. };
  235. return apiCall(token, 'POST',
  236. '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/log'
  237. .replace('{PROJECTID}', projectId), body);
  238. }
  239. function getProject(token, projectId) {
  240. var portalId = shadowman.config.value('portal_id');
  241. if (!portalId) return { error: 'Missing portal_id' };
  242. return apiCall(token, 'GET',
  243. '/api/v3/portal/{PORTALID}/projects/{PROJECTID}'
  244. .replace('{PORTALID}', portalId)
  245. .replace('{PROJECTID}', projectId), null, {});
  246. }
  247. function listTimesheets(token, projectId, filters) {
  248. var portalId = shadowman.config.value('portal_id');
  249. if (!portalId) return { error: 'Missing portal_id in plugin config.' };
  250. var params = {
  251. view_type: 'customdate'
  252. };
  253. if (filters.from_date) {
  254. var parts = filters.from_date.split('-');
  255. if (parts.length === 3) params.start_date = parts[2] + '-' + parts[0] + '-' + parts[1];
  256. else params.start_date = filters.from_date;
  257. } else {
  258. var d = new Date();
  259. d.setDate(d.getDate() - 30);
  260. params.start_date = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
  261. }
  262. if (filters.to_date) {
  263. var parts = filters.to_date.split('-');
  264. if (parts.length === 3) params.end_date = parts[2] + '-' + parts[0] + '-' + parts[1];
  265. else params.end_date = filters.to_date;
  266. } else {
  267. var today = new Date();
  268. params.end_date = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0');
  269. }
  270. if (filters.index) params.page = String(filters.index);
  271. if (filters.task_id) {
  272. params.module = JSON.stringify({ id: String(filters.task_id), type: 'task' });
  273. } else {
  274. params.module = JSON.stringify({ type: 'task' });
  275. }
  276. var url = '/api/v3/portal/{PORTALID}/timelogs'.replace('{PORTALID}', portalId);
  277. if (projectId) {
  278. // Ha van projekt ID, a projekt szintű végpontot hívjuk
  279. url = '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/timelogs'
  280. .replace('{PORTALID}', portalId)
  281. .replace('{PROJECTID}', projectId);
  282. }
  283. return apiCall(token, 'GET', url, null, params);
  284. }
  285. function deleteTimesheet(token, projectId, logId) {
  286. return apiCall(token, 'DELETE',
  287. '/api/v3/portal/{PORTALID}/projects/{PROJECTID}/logs/{LOGID}'
  288. .replace('{PORTALID}', shadowman.config.value('portal_id'))
  289. .replace('{PROJECTID}', projectId)
  290. .replace('{LOGID}', String(logId)), { module: 'task', from_page: 'timesheetdetails' }, {});
  291. }
  292. module.exports = {
  293. listProjects: listProjects,
  294. listTasks: listTasks,
  295. getTask: getTask,
  296. createTask: createTask,
  297. updateTask: updateTask,
  298. deleteTask: deleteTask,
  299. getMyTasks: getMyTasks,
  300. searchTasks: searchTasks,
  301. addComment: addComment,
  302. updateComment: updateComment,
  303. deleteComment: deleteComment,
  304. logTime: logTime,
  305. listTimesheets: listTimesheets,
  306. deleteTimesheet: deleteTimesheet
  307. };