1import { ApiClient } from './base.js'; 2 3const api = new ApiClient(); 4 5export const messagesApi = { 6 /** 7 * Get paginated list of messages 8 * @param {Object} params - All filter params matching GetMessagesRequest DTO 9 * @returns {Promise<Object>} { messages, hasMore, nextCursor } 10 */ 11 // Trailing optional `options` forwarded to the shared ApiClient (see getMessage). 12 // Backward compatible: the four project-messages-panel callers pass params only. 13 getMessages: (params = {}, options = {}) => { 14 const query = new URLSearchParams(); 15 if (params.beforeDate) query.append('beforeDate', params.beforeDate); 16 if (params.pageSize) query.append('pageSize', params.pageSize); 17 if (params.projectId) query.append('projectId', params.projectId); 18 if (params.filterType != null) query.append('filterType', params.filterType); 19 if (params.unreadOnly) query.append('unreadOnly', 'true'); 20 if (params.followupOnly) query.append('followupOnly', 'true'); 21 if (params.attachmentsOnly) query.append('attachmentsOnly', 'true'); 22 if (params.deliveryIssuesOnly) query.append('deliveryIssuesOnly', 'true'); 23 if (params.categoryIds) query.append('categoryIds', params.categoryIds); 24 if (params.searchTerm) query.append('searchTerm', params.searchTerm); 25 26 const queryString = query.toString(); 27 const endpoint = `smrtbuild/messages${queryString ? '?' + queryString : ''}`; 28 return api.get(endpoint, options); 29 }, 30 31 /** 32 * Get unread count 33 * @returns {Promise<Object>} { count: 0 } 34 */ 35 getUnreadCount: (options = {}) => api.get('smrtbuild/messages/unread-count', options), 36 37 /** 38 * Get the module-dashboard catch-up summary (last N days, default 7). 39 * @param {number} days 40 * @returns {Promise<Object>} { windowDays, windowStart, inboundCount, sentCount, 41 * handledThreadCount, awaitingThreadCount, repliesToMyThreads, deliveryFailureCount, 42 * projects: [...], awaiting: [...], correspondents: [...] } 43 */ 44 getDashboardSummary: (days = 7, options = {}) => api.get(`smrtbuild/messages/dashboard-summary?days=${days}`, options), 45 46 /** 47 * Get message detail 48 * @param {string|number} id 49 * @returns {Promise<Object>} 50 */ 51 // Trailing optional `options` is forwarded to the shared ApiClient so callers can 52 // supply { signal } for transport cancellation. Backward compatible â every existing 53 // caller that passes only `id` is unaffected. 54 getMessage: (id, options = {}) => api.get(`smrtbuild/messages/${id}`, options), 55 56 /** 57 * Mark as read 58 * @param {string|number} id 59 * @returns {Promise<void>} 60 */ 61 markAsRead: (id) => api.post(`smrtbuild/messages/${id}/mark-read`), 62 63 /** 64 * Toggle followup 65 * @param {string|number} id 66 * @returns {Promise<void>} 67 */ 68 toggleFollowup: (id) => api.post(`smrtbuild/messages/${id}/toggle-followup`), 69 70 /** 71 * Toggle attention (action needed) 72 * @param {string|number} id 73 * @returns {Promise<void>} 74 */ 75 toggleAttention: (id) => api.post(`smrtbuild/messages/${id}/toggle-attention`), 76 77 /** 78 * Get attachments for a message 79 * GET /api/smrtbuild/messages/{id}/attachments 80 * @param {string|number} id 81 * @returns {Promise<Array>} 82 */ 83 getAttachments: (id) => api.get(`smrtbuild/messages/${id}/attachments`), 84 85 /** 86 * Get replies for a message 87 * GET /api/smrtbuild/messages/{id}/replies 88 * @param {string|number} id 89 * @returns {Promise<Array>} 90 */ 91 getReplies: (id) => api.get(`smrtbuild/messages/${id}/replies`), 92 93 /** 94 * Get message with full conversation thread (convenience method) 95 * Fetches message detail, attachments, and replies in parallel 96 * @param {string|number} id 97 * @returns {Promise<Object>} Message with attachments and replies arrays 98 */ 99 getMessageWithReplies: async (id) => { 100 const [message, attachments, replies] = await Promise.all([ 101 api.get(`smrtbuild/messages/${id}`), 102 api.get(`smrtbuild/messages/${id}/attachments`), 103 api.get(`smrtbuild/messages/${id}/replies`) 104 ]); 105 return { 106 ...message, 107 attachments: attachments || [], 108 replies: replies || [] 109 }; 110 }, 111 112 /** 113 * Get options for new message form (projects, recipients) 114 * GET /api/smrtbuild/messages/add-options 115 * @returns {Promise<Object>} 116 */ 117 getAddOptions: (options = {}) => api.get('smrtbuild/messages/add-options', options), 118 119 /** 120 * Get message categories for filtering 121 * GET /api/smrtbuild/messages/categories 122 * @returns {Promise<Array>} 123 */ 124 getCategories: () => api.get('smrtbuild/messages/categories'), 125 126 /** 127 * Get contacts for specified projects 128 * GET /api/smrtbuild/messages/add-options/contacts/{projectIds} 129 * @param {string} projectIds - Comma-separated project IDs 130 * @param {Object} [options] - Request options; `{ signal }` cancels the read (CHARTER law 20). 131 * @returns {Promise<Array>} 132 */ 133 getContactsForProjects: (projectIds, options) => api.get(`smrtbuild/messages/add-options/contacts/${projectIds}`, options), 134 135 /** 136 * Send a reply to a message 137 * POST /api/smrtbuild/messages/{id}/reply 138 * @param {string|number} id - Original message ID 139 * @param {Object} data - { sendTo, sendCc, subject, bodyText, bodyHtml, tempFileIds, replyAll } 140 * @returns {Promise<Object>} 141 */ 142 sendReply: (id, data) => api.post(`smrtbuild/messages/${id}/reply`, data), 143 144 /** 145 * Send a new message 146 * POST /api/smrtbuild/messages 147 * @param {Object} data - { projectId, sendTo, sendCc, subject, bodyText, bodyHtml, tempFileIds, followup, actionNeeded, isDraft } 148 * @returns {Promise<Object>} 149 */ 150 sendMessage: (data) => api.post('smrtbuild/messages', data), 151 152 /** 153 * Upload a temp file for message attachments 154 * POST /api/smrtbuild/messages/upload-attachment 155 * @param {File} file - File to upload 156 * @returns {Promise<Object>} { id, fileName } 157 */ 158 uploadAttachment: (file) => { 159 const formData = new FormData(); 160 formData.append('file', file); 161 return api.postFormData('smrtbuild/messages/upload-attachment', formData); 162 }, 163 164 /** 165 * Get task types â tasks (with doc counts) for a project (compose "Add From Tasks" picker) 166 * GET /api/smrtbuild/messages/project-task-documents/{projectId} 167 * @param {number} projectId 168 * @returns {Promise<{ taskTypes: Array }>} 169 */ 170 getProjectTaskDocuments: (projectId) => 171 api.get(`smrtbuild/messages/project-task-documents/${projectId}`), 172 173 /** 174 * Get documents for a single task (lazy-loaded when user drills into a task) 175 * GET /api/smrtbuild/messages/task-documents/{taskId} 176 * @param {number} taskId 177 * @param {{ signal?: AbortSignal }} [options] 178 * @returns {Promise<Array>} 179 */ 180 getTaskDocuments: (taskId, options = {}) => 181 api.get(`smrtbuild/messages/task-documents/${taskId}`, options), 182 183 /** 184 * Copy existing project files (task docs, photos/videos, or project documents) to temp 185 * storage for message attachment â by reference, no re-upload. 186 * POST /api/smrtbuild/messages/copy-to-temp 187 * @param {{ sourceType: 'task-doc'|'photo'|'document', fileIds: number[], projectId: number }} data 188 * @returns {Promise<{ files: Array }>} 189 */ 190 copyFilesToTemp: (data) => api.post('smrtbuild/messages/copy-to-temp', data), 191 192 /** 193 * Search messages 194 * POST /api/smrtbuild/messages/search 195 * @param {Object} params - { query, projectId, includeAttachments, fromDate, toDate, page, pageSize } 196 * @param {Object} [options] - Forwarded to the shared ApiClient; carries { signal }. 197 * @returns {Promise<Object>} 198 */ 199 // A search is a READ that happens to travel by POST, so it takes the same trailing optional 200 // `options` bag `getMessages` and `getMessage` already take (charter law 20 â a wrapper 201 // discharges cancellation by accepting and forwarding one). `ApiClient.post` spreads it into 202 // the request, so `{ signal }` reaches fetch unchanged. Backward compatible: no existing 203 // caller passes it. 204 searchMessages: (params, options = {}) => api.post('smrtbuild/messages/search', params, options), 205 206 /** 207 * Forward a message thread as PDF 208 * POST /api/smrtbuild/messages/{id}/forward 209 * @param {string|number} id 210 * @returns {Promise<Object>} { uploadCode, filesAdded, success } 211 */ 212 forwardMessage: (id) => api.post(`smrtbuild/messages/${id}/forward`, {}), 213 214 /** 215 * Forward selected attachments only (no conversation PDF). Mirrors legacy 216 * `createattachmentforward`. Returns an upload code the compose flow can 217 * attach to a new message. 218 * POST /api/smrtbuild/messages/forward-attachments 219 * @param {number[]} attachmentIds - Selected attachment IDs from the message + replies
220 * @returns {Promise<{ uploadCode: string, filesAdded: string[], success: boolean }>} 221 */ 222 forwardAttachments: (attachmentIds) => 223 api.post('smrtbuild/messages/forward-attachments', { attachmentIds }), 224 225 /** 226 * Export a message thread as a downloadable PDF (no blob upload, no upload code). 227 * Used by the "Print / Export" kebab action. 228 * GET /api/smrtbuild/messages/{id}/export-pdf 229 * @param {string|number} id 230 * @returns {Promise<{ blob: Blob, fileName: string }>} 231 */ 232 exportPdf: (id) => api.getBlob(`smrtbuild/messages/${id}/export-pdf`), 233 234 /** 235 * Link (import) an external Outlook or Gmail message into a project's SmartBuild 236 * message center. Used by the unified messages module â when a user browses their 237 * Outlook/Gmail inbox and clicks "Link to Project," this stores the message + its 238 * attachment bytes as a real becc_pm_project_emails row. 239 * 240 * Dedup: if the same externalId already exists for that project, the backend 241 * returns the existing MessageId with alreadyLinked=true â no duplicate insert. 242 * 243 * POST /api/smrtbuild/messages/link-from-external 244 * @param {Object} payload - LinkExternalMessageRequest shape 245 * @returns {Promise<{ messageId, alreadyLinked, attachmentsUploaded, attachmentsAttempted, linkedItemUrl? }>} 246 */ 247 linkFromExternal: (payload) => api.post('smrtbuild/messages/link-from-external', payload), 248 249 /** 250 * Respond to an .ics meeting invitation attached to a message (Accept / Decline / 251 * Tentative). Server builds an ICS REPLY and sends it to the organizer via MailGun. 252 * POST /api/smrtbuild/messages/attachments/{attachmentId}/ics-respond 253 * 254 * @param {string|number} attachmentId 255 * @param {{ responseValue: 'ACCEPTED'|'DECLINED'|'TENTATIVE', responseText?: string, projectId?: number }} payload 256 * @returns {Promise<{ replySent: boolean, eventUid?: string, organizer?: string, responseValue: string }>} 257 */ 258 icsRespond: (attachmentId, payload) => 259 api.post(`smrtbuild/messages/attachments/${attachmentId}/ics-respond`, payload), 260 261 /** 262 * Enrichment for an .ics attachment â server-side facts the client parse can't know: 263 * whether it's already on the user's calendar, their current RSVP, whether it's a newer 264 * revision (canUpdate), and whether they can respond. The basic event fields stay client-parsed. 265 * GET /api/smrtbuild/messages/attachments/{attachmentId}/ics-info 266 * 267 * @param {string|number} attachmentId 268 * @returns {Promise<{ uid: string, organizer?: string, eventStatus?: string, isCancelled: boolean, 269 * sequence: number, calendarEventId?: number, storedSequence?: number, userStatus: string, 270 * canUpdate: boolean, canRespond: boolean }>} 271 */ 272 getIcsInfo: (attachmentId) => 273 api.get(`smrtbuild/messages/attachments/${attachmentId}/ics-info`), 274 275 /** 276 * Accept an .ics invite AND add it to the user's (or a project's) SmartBuild calendar: 277 * creates the event, invites attendees, persists the RSVP, links it back to the message 278 * (CALENDARTASK), and sends the ACCEPTED reply. Mirrors legacy #icsAddToCalendar. 279 * POST /api/smrtbuild/messages/attachments/{attachmentId}/ics-add-to-calendar 280 * 281 * @param {string|number} attachmentId 282 * @param {{ messageId: number, projectId?: number, title: string, description?: string, 283 * location?: string, priority?: 'L'|'M'|'H', eventDateStart: string, eventStartTime?: string, 284 * eventDateEnd?: string, eventEndTime?: string, durationMinutes: number, 285 * attendeeUserIds?: number[], attendeeContactIds?: number[] }} payload
286 * @returns {Promise<{ calendarEventId: number, calendarEventUrl?: string, linked: boolean, replySent: boolean }>} 287 */ 288 addIcsToCalendar: (attachmentId, payload) => 289 api.post(`smrtbuild/messages/attachments/${attachmentId}/ics-add-to-calendar`, payload), 290 291 /** 292 * Reschedule an already-on-calendar event from an updated invite (newer SEQUENCE). 293 * POST /api/smrtbuild/messages/attachments/{attachmentId}/ics-update 294 * 295 * @param {string|number} attachmentId 296 * @param {{ title: string, eventDateStart: string, eventStartTime?: string, 297 * eventDateEnd?: string, durationMinutes: number }} payload 298 */ 299 updateIcs: (attachmentId, payload) => 300 api.post(`smrtbuild/messages/attachments/${attachmentId}/ics-update`, payload), 301 302 /** 303 * Cancel (soft-delete) the calendar event for an .ics invite and drop its CALENDARTASK links. 304 * POST /api/smrtbuild/messages/attachments/{attachmentId}/ics-cancel 305 * 306 * @param {string|number} attachmentId 307 */ 308 cancelIcs: (attachmentId) => 309 api.post(`smrtbuild/messages/attachments/${attachmentId}/ics-cancel`, {}), 310 311 /** 312 * Copy a message attachment as a file on a project item â PROGBILL (Progress 313 * Application), SUBMITTALITEM (Submittal Item), or SUPPLIERITEM (Supplier Item). 314 * Mirrors legacy `savefile/{save_type}/{save_id}/{file_id}` for the targets the 315 * new app's per-domain uploadDocument endpoints don't cover. The blob is shared 316 * (no copy) â only a reference row is inserted in the target's _doc table. 317 * POST /api/smrtbuild/messages/attachments/{attachmentId}/copy-to-item 318 * 319 * @param {string|number} attachmentId 320 * @param {{ targetType: 'PROGBILL'|'SUBMITTALITEM'|'SUPPLIERITEM', targetId: number }} payload 321 * @returns {Promise<{ docId: number, targetType: string, title: string }>} 322 */ 323 copyAttachmentToItem: (attachmentId, payload) => 324 api.post(`smrtbuild/messages/attachments/${attachmentId}/copy-to-item`, payload), 325 326 /** 327 * Block an email address 328 * POST /api/smrtbuild/messages/block-email 329 * @param {string} emailAddress 330 * @returns {Promise<void>} 331 */ 332 blockEmail: (emailAddress) => api.post('smrtbuild/messages/block-email', { emailAddress }), 333 334 /** 335 * Categorize a message (assign categories) 336 * POST /api/smrtbuild/messages/{id}/categorize 337 * @param {string|number} id 338 * @param {number[]} categoryIds 339 * @returns {Promise<void>} 340 */ 341 categorizeMessage: (id, categoryIds) => api.post(`smrtbuild/messages/${id}/categorize`, { categoryIds }), 342 343 /** 344 * Create a new category 345 * POST /api/smrtbuild/messages/categories 346 * @param {string} name 347 * @returns {Promise<Object>} 348 */ 349 createCategory: (name) => api.post('smrtbuild/messages/categories', { name }), 350 351 /** 352 * Delete a message category by id (soft-delete server-side). 353 * DELETE /api/smrtbuild/messages/categories/{id} 354 */ 355 deleteCategory: (id) => api.delete(`smrtbuild/messages/categories/${id}`), 356 357 /** 358 * Get linked items for a message 359 * GET /api/smrtbuild/messages/{id}/links 360 * @param {string|number} id 361 * @returns {Promise<Array>} 362 */ 363 getLinks: (id) => api.get(`smrtbuild/messages/${id}/links`), 364 365 /** 366 * Link a message to a project item 367 * POST /api/smrtbuild/messages/{id}/links 368 * @param {string|number} id 369 * @param {string} linkType - SI, RFI, CHG, SUBMITTAL, TASK, PROGBILL 370 * @param {number} itemId 371 * @returns {Promise<Object>} 372 */ 373 createLink: (id, linkType, itemId) => api.post(`smrtbuild/messages/${id}/links`, { linkType, itemId }), 374 375 /** 376 * Remove a linked item 377 * DELETE /api/smrtbuild/messages/{id}/links/{linkId} 378 * @param {string|number} id 379 * @param {string|number} linkId 380 * @returns {Promise<void>} 381 */ 382 removeLink: (id, linkId) => api.delete(`smrtbuild/messages/${id}/links/${linkId}`), 383
384 // ââ External mailbox (Gmail/Outlook) links â per-user, no message-center import ââ 385 386 /** Get the current user's links for an external mailbox message (by provider id). */ 387 getExternalLinks: (source, externalId, options = {}) => 388 api.get(`smrtbuild/messages/external-links?source=${encodeURIComponent(source)}&externalId=${encodeURIComponent(externalId)}`, options), 389 390 /** Link an external mailbox message to a record. payload: {source, externalId, projectId, linkType, itemId}. */ 391 createExternalLink: (payload) => api.post('smrtbuild/messages/external-links', payload), 392 393 /** Remove one of the current user's external-message links. */ 394 removeExternalLink: (linkId) => api.delete(`smrtbuild/messages/external-links/${linkId}`), 395 396 /** 397 * Render an external (Gmail/Outlook) email to a PDF and file it into the linked record's 398 * Documents (RFI/CHG/SI/Submittal). payload: {source, externalMessageId, subject, from, to, 399 * date, htmlContent, projectId, itemType, itemId}. 400 * POST /api/smrtbuild/messages/external/save-as-pdf 401 */ 402 saveExternalEmailAsPdf: (payload) => api.post('smrtbuild/messages/external/save-as-pdf', payload), 403 404 /** 405 * Remove a category from a message 406 * Uses the categorize endpoint to replace all categories (minus the removed one) 407 * @param {string|number} id - Message ID 408 * @param {number} categoryIdToRemove - Category ID to remove 409 * @param {number[]} currentCategoryIds - All current category IDs on the message 410 * @returns {Promise<void>} 411 */ 412 removeCategory: (id, categoryIdToRemove, currentCategoryIds) => { 413 const remaining = currentCategoryIds.filter(cid => cid !== categoryIdToRemove); 414 return api.post(`smrtbuild/messages/${id}/categorize`, { categoryIds: remaining }); 415 }, 416 417 /** 418 * Get download URL for a message attachment 419 * GET /api/smrtbuild/messages/{messageId}/attachments/{attachmentId}/download 420 * @param {string|number} messageId 421 * @param {string|number} attachmentId 422 * @returns {Promise<Object>} { url } or direct blob download 423 */ 424 downloadAttachment: (messageId, attachmentId) => 425 api.get(`smrtbuild/messages/${messageId}/attachments/${attachmentId}/download`), 426 427 /** 428 * Get view URL for a message attachment (blob URL + content type). 429 * Ported from legacy jQueryInlineDoc("ATTACHMENT", attachId). 430 * GET /api/smrtbuild/messages/attachments/{attachmentId}/view 431 * @param {string|number} attachmentId 432 * @returns {Promise<Object>} { url, contentType, fileName } 433 */ 434 viewAttachment: (attachmentId) => 435 api.get(`smrtbuild/messages/attachments/${attachmentId}/view`), 436 437 /** 438 * Get invoice creation options for a message 439 * GET /api/smrtbuild/messages/{id}/invoice-options 440 * @param {string|number} id 441 * @returns {Promise<Object>} { suppliers, teamMembers, costCodes } 442 */ 443 getInvoiceOptions: (id) => api.get(`smrtbuild/messages/${id}/invoice-options`), 444 445 /** 446 * Create an invoice from a message 447 * POST /api/smrtbuild/messages/{id}/create-invoice 448 * @param {string|number} id 449 * @param {Object} data - { projectId, supplierId, amount, daysDue, personResponsibleId } 450 * @returns {Promise<Object>} 451 */ 452 createInvoice: (id, data) => api.post(`smrtbuild/messages/${id}/create-invoice`, data), 453 454 /** 455 * Get linkable items for the "Link Message To" picker. 456 * GET /api/smrtbuild/messages/linkable-items?projectId={id}&linkType=RFI&search=abc 457 * @param {number} projectId 458 * @param {string} linkType - TASK, RFI, CHG, SI, SUBMITTAL, SUPPLIER, PROGBILL 459 * @param {string} [search] 460 * @returns {Promise<Array>} Array of { id, itemNumber, title, status } 461 */ 462 getLinkableItems: (projectId, messageId, linkType, search = '') => { 463 const query = new URLSearchParams({ projectId, messageId, linkType }); 464 if (search) query.append('search', search); 465 return api.get(`smrtbuild/messages/linkable-items?${query}`); 466 } 467};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.