agenda_view.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. const { div, h2, p, section, button, form, img, textarea, a, br, h1, span } = require("../server/node_modules/hyperaxe");
  2. const { template, i18n, userLink} = require('./main_views');
  3. const moment = require('../server/node_modules/moment');
  4. const { config } = require('../server/SSB_server.js');
  5. const userId = config.keys.id;
  6. const renderCardField = (labelText, value) =>
  7. div({ class: 'card-field' },
  8. span({ class: 'card-label' }, labelText),
  9. span(
  10. { class: 'card-value' },
  11. ...(Array.isArray(value) ? value : [value ?? ''])
  12. )
  13. );
  14. function getViewDetailsAction(item) {
  15. switch (item.type) {
  16. case 'transfer': return `/transfers/${encodeURIComponent(item.id)}`;
  17. case 'tribe': return `/tribe/${encodeURIComponent(item.id)}`;
  18. case 'event': return `/events/${encodeURIComponent(item.id)}`;
  19. case 'task': return `/tasks/${encodeURIComponent(item.id)}`;
  20. case 'market': return `/market/${encodeURIComponent(item.id)}`;
  21. case 'report': return `/reports/${encodeURIComponent(item.id)}`;
  22. case 'job': return `/jobs/${encodeURIComponent(item.id)}`;
  23. case 'project': return `/projects/${encodeURIComponent(item.id)}`;
  24. case 'calendar': return `/calendars/${encodeURIComponent(item.id)}`;
  25. default: return `/messages/${encodeURIComponent(item.id)}`;
  26. }
  27. }
  28. const renderAgendaItem = (item, userId, filter) => {
  29. const fmt = d => moment(d).format('YYYY/MM/DD HH:mm:ss');
  30. const author = item.seller || item.organizer || item.from || item.author || '';
  31. const commonFields = [
  32. p({ class: 'card-footer' },
  33. span({ class: 'date-link' }, `${item.createdAt ? moment(item.createdAt).format('YYYY/MM/DD HH:mm:ss') : ''} ${i18n.performed} `),
  34. author ? userLink(author) : ''
  35. )
  36. ];
  37. let details = [];
  38. let actionButton = null;
  39. if (filter === 'discarded') {
  40. actionButton = form({ method: 'POST', action: `/agenda/restore/${encodeURIComponent(item.id)}` },
  41. button({ type: 'submit', class: 'restore-btn' }, i18n.agendaRestoreButton)
  42. );
  43. } else {
  44. actionButton = form({ method: 'POST', action: `/agenda/discard/${encodeURIComponent(item.id)}` },
  45. button({ type: 'submit', class: 'discard-btn' }, i18n.agendaDiscardButton)
  46. );
  47. }
  48. if (item.type === 'market') {
  49. details = [
  50. renderCardField(i18n.marketItemType + ":", String(item.item_type || '').toUpperCase()),
  51. renderCardField(i18n.marketItemStatus + ":", item.status),
  52. renderCardField(i18n.marketItemStock + ":", item.stock),
  53. renderCardField(i18n.marketItemPrice + ":", `${item.price} ECO`),
  54. renderCardField(i18n.marketItemIncludesShipping + ":", item.includesShipping ? i18n.agendaYes : i18n.agendaNo),
  55. renderCardField(i18n.deadline + ":", item.deadline ? new Date(item.deadline).toLocaleString() : '')
  56. ];
  57. if (String(item.item_type || '').toLowerCase() === 'auction') {
  58. const bids = Array.isArray(item.auctions_poll) ? item.auctions_poll.map(bid => parseFloat(String(bid).split(':')[1])).filter(n => !isNaN(n)) : [];
  59. const maxBid = bids.length ? Math.max(...bids) : 0;
  60. details.push(renderCardField(i18n.marketItemHighestBid + ":", `${maxBid} ECO`));
  61. }
  62. const seller = author ? p(userLink(author)) : '';
  63. details.push(br(), div({ class: 'members-list' }, i18n.marketItemSeller + ': ', seller));
  64. }
  65. if (item.type === 'tribe') {
  66. details = [
  67. renderCardField(i18n.agendaAnonymousLabel + ":", item.isAnonymous ? i18n.agendaYes : i18n.agendaNo),
  68. renderCardField(i18n.agendaInviteModeLabel + ":", (item.inviteMode ? String(item.inviteMode).toUpperCase() : i18n.noInviteMode)),
  69. renderCardField(i18n.agendaLARPLabel + ":", item.isLARP ? i18n.agendaYes : i18n.agendaNo),
  70. renderCardField(i18n.agendaLocationLabel + ":", item.location || i18n.noLocation),
  71. renderCardField(i18n.agendaMembersCount + ":", Array.isArray(item.members) ? item.members.length : 0),
  72. br()
  73. ];
  74. const membersList = Array.isArray(item.members) ? item.members.map(member => p(userLink(member))) : [];
  75. details.push(div({ class: 'members-list' }, `${i18n.agendaMembersLabel}:`, membersList));
  76. }
  77. if (item.type === 'report') {
  78. details = [
  79. renderCardField(i18n.agendareportStatus + ":", item.status || i18n.noStatus),
  80. renderCardField(i18n.agendareportCategory + ":", item.category || i18n.noCategory),
  81. renderCardField(i18n.agendareportSeverity + ":", (item.severity ? String(item.severity).toUpperCase() : i18n.noSeverity))
  82. ];
  83. }
  84. if (item.type === 'event') {
  85. details = [
  86. renderCardField(i18n.eventDateLabel + ":", item.date ? fmt(item.date) : ''),
  87. renderCardField(i18n.eventLocationLabel + ":", item.location || ''),
  88. renderCardField(i18n.eventPriceLabel + ":", `${item.price} ECO`),
  89. renderCardField(
  90. i18n.eventUrlLabel + ":",
  91. item.url ? p(a({ href: item.url, target: "_blank" }, item.url)) : p(i18n.noUrl)
  92. )
  93. ];
  94. actionButton = actionButton || form({ method: 'POST', action: `/events/attend/${encodeURIComponent(item.id)}` },
  95. button({ type: 'submit', class: 'assign-btn' }, `${i18n.eventAttendButton}`)
  96. );
  97. }
  98. if (item.type === 'task') {
  99. details = [
  100. renderCardField(i18n.taskStatus + ":", item.status),
  101. renderCardField(i18n.taskPriorityLabel + ":", item.priority),
  102. renderCardField(i18n.taskStartTimeLabel + ":", item.startTime ? new Date(item.startTime).toLocaleString() : ''),
  103. renderCardField(i18n.taskEndTimeLabel + ":", item.endTime ? new Date(item.endTime).toLocaleString() : ''),
  104. renderCardField(i18n.taskLocationLabel + ":", item.location || '')
  105. ];
  106. const assigned = Array.isArray(item.assignees) && item.assignees.includes(userId);
  107. actionButton = actionButton || form({ method: 'POST', action: `/tasks/assign/${encodeURIComponent(item.id)}` },
  108. button({ type: 'submit', class: 'assign-btn' }, assigned ? i18n.taskUnassignButton : i18n.taskAssignButton)
  109. );
  110. }
  111. if (item.type === 'transfer') {
  112. details = [
  113. renderCardField(i18n.agendaTransferConcept + ":", item.concept),
  114. renderCardField(i18n.agendaTransferAmount + ":", item.amount),
  115. renderCardField(i18n.agendaTransferDeadline + ":", item.deadline ? fmt(item.deadline) : ''),
  116. br()
  117. ];
  118. const membersList = item.to ? p(userLink(item.to)) : '';
  119. details.push(div({ class: 'members-list' }, i18n.to + ': ', membersList));
  120. }
  121. if (item.type === 'project') {
  122. details = [
  123. renderCardField(i18n.projectStatus + ":", item.status || i18n.noStatus),
  124. renderCardField(i18n.projectProgress + ":", `${item.progress || 0}%`),
  125. renderCardField(i18n.projectGoal + ":", `${item.goal} ECO`),
  126. renderCardField(i18n.projectPledged + ":", `${item.pledged || 0} ECO`),
  127. renderCardField(i18n.projectDeadline + ":", item.deadline ? new Date(item.deadline).toLocaleString() : i18n.noDeadline)
  128. ];
  129. }
  130. if (item.type === 'calendar') {
  131. details = [
  132. renderCardField((i18n.calendarStatusLabel || 'Status') + ':', item.isClosed ? (i18n.calendarStatusClosed || 'CLOSED') : (i18n.calendarStatusOpen || 'OPEN')),
  133. renderCardField((i18n.calendarDeadlineLabel || 'Deadline') + ':', item.deadline ? moment(item.deadline).format('YYYY/MM/DD HH:mm') : ''),
  134. renderCardField((i18n.calendarParticipantsLabel || 'Participants') + ':', Array.isArray(item.participants) ? item.participants.length : 0)
  135. ];
  136. }
  137. if (item.type === 'job') {
  138. const subs = Array.isArray(item.subscribers)
  139. ? item.subscribers
  140. : (typeof item.subscribers === 'string'
  141. ? item.subscribers.split(',').map(s => s.trim()).filter(Boolean)
  142. : (item.subscribers && typeof item.subscribers.length === 'number'
  143. ? Array.from(item.subscribers)
  144. : []));
  145. const subsInterleaved = subs
  146. .map((id, i) => [i > 0 ? ', ' : '', userLink(id)])
  147. .flat();
  148. details = [
  149. renderCardField(i18n.jobStatus + ":", item.status),
  150. renderCardField(i18n.jobLocation + ":", (item.location || '').toUpperCase()),
  151. renderCardField(i18n.jobType + ":", (item.job_type || '').toUpperCase()),
  152. renderCardField(i18n.jobSalary + ":", `${item.salary} ECO`),
  153. renderCardField(i18n.jobVacants + ":", item.vacants),
  154. renderCardField(i18n.jobLanguages + ":", (item.languages || '').toUpperCase()),
  155. br(),
  156. div(
  157. { class: 'members-list' },
  158. i18n.jobSubscribers + ': ',br(),br(),
  159. ...(subs.length ? subsInterleaved : [i18n.noSubscribers.toUpperCase()])
  160. ),
  161. ];
  162. const subscribed = subs.includes(userId);
  163. if (!subscribed && String(item.status).toUpperCase() !== 'CLOSED' && item.author !== userId) {
  164. actionButton = form({ method: 'GET', action: `/jobs/subscribe/${encodeURIComponent(item.id)}` },
  165. button({ type: 'submit', class: 'subscribe-btn' }, i18n.jobSubscribeButton)
  166. );
  167. }
  168. }
  169. return div({ class: 'agenda-item card' },
  170. h2(`[${String(item.type || '').toUpperCase()}] ${item.title || item.name || item.concept || ''}`),
  171. form({ method: "GET", action: getViewDetailsAction(item) },
  172. button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
  173. ),
  174. actionButton,
  175. br(),
  176. ...details,
  177. br(),
  178. ...commonFields
  179. );
  180. };
  181. exports.agendaView = async (data, filter) => {
  182. const { items = [], counts: _c = {} } = data || {};
  183. const counts = { all: 0, open: 0, closed: 0, events: 0, tasks: 0, reports: 0, tribes: 0, jobs: 0, market: 0, projects: 0, transfers: 0, calendars: 0, discarded: 0, ..._c };
  184. return template(
  185. i18n.agendaTitle,
  186. section(
  187. div({ class: 'tags-header' },
  188. h2(i18n.agendaTitle),
  189. p(i18n.agendaDescription)
  190. ),
  191. div({ class: 'filters' },
  192. form({ method: 'GET', action: '/agenda' },
  193. button({ type: 'submit', name: 'filter', value: 'all', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' },
  194. `${i18n.agendaFilterAll} (${counts.all})`),
  195. button({ type: 'submit', name: 'filter', value: 'open', class: filter === 'open' ? 'filter-btn active' : 'filter-btn' },
  196. `${i18n.agendaFilterOpen} (${counts.open})`),
  197. button({ type: 'submit', name: 'filter', value: 'closed', class: filter === 'closed' ? 'filter-btn active' : 'filter-btn' },
  198. `${i18n.agendaFilterClosed} (${counts.closed})`),
  199. button({ type: 'submit', name: 'filter', value: 'events', class: filter === 'events' ? 'filter-btn active' : 'filter-btn' },
  200. `${i18n.agendaFilterEvents} (${counts.events})`),
  201. button({ type: 'submit', name: 'filter', value: 'tasks', class: filter === 'tasks' ? 'filter-btn active' : 'filter-btn' },
  202. `${i18n.agendaFilterTasks} (${counts.tasks})`),
  203. button({ type: 'submit', name: 'filter', value: 'reports', class: filter === 'reports' ? 'filter-btn active' : 'filter-btn' },
  204. `${i18n.agendaFilterReports} (${counts.reports})`),
  205. button({ type: 'submit', name: 'filter', value: 'tribes', class: filter === 'tribes' ? 'filter-btn active' : 'filter-btn' },
  206. `${i18n.agendaFilterTribes} (${counts.tribes})`),
  207. button({ type: 'submit', name: 'filter', value: 'jobs', class: filter === 'jobs' ? 'filter-btn active' : 'filter-btn' },
  208. `${i18n.agendaFilterJobs} (${counts.jobs})`),
  209. button({ type: 'submit', name: 'filter', value: 'market', class: filter === 'market' ? 'filter-btn active' : 'filter-btn' },
  210. `${i18n.agendaFilterMarket} (${counts.market})`),
  211. button({ type: 'submit', name: 'filter', value: 'projects', class: filter === 'projects' ? 'filter-btn active' : 'filter-btn' },
  212. `${i18n.agendaFilterProjects} (${counts.projects})`),
  213. button({ type: 'submit', name: 'filter', value: 'calendars', class: filter === 'calendars' ? 'filter-btn active' : 'filter-btn' },
  214. `${i18n.agendaFilterCalendars || 'CALENDARS'} (${counts.calendars})`),
  215. button({ type: 'submit', name: 'filter', value: 'transfers', class: filter === 'transfers' ? 'filter-btn active' : 'filter-btn' },
  216. `${i18n.agendaFilterTransfers} (${counts.transfers})`),
  217. button({ type: 'submit', name: 'filter', value: 'discarded', class: filter === 'discarded' ? 'filter-btn active' : 'filter-btn' },
  218. `DISCARDED (${counts.discarded})`)
  219. )
  220. ),
  221. div({ class: 'agenda-list' },
  222. items.length
  223. ? items.map(item => renderAgendaItem(item, userId, filter))
  224. : p(i18n.agendaNoItems)
  225. )
  226. )
  227. );
  228. };