import { Op } from 'sequelize'; import { WorkNote } from '@models/WorkNote'; import { WorkNoteAttachment } from '@models/WorkNoteAttachment'; import { Participant } from '@models/Participant'; import { activityService } from './activity.service'; import logger from '@utils/logger'; export class WorkNoteService { async list(requestId: string) { const notes = await WorkNote.findAll({ where: { requestId }, order: [['created_at' as any, 'ASC']] }); // Load attachments for each note const enriched = await Promise.all(notes.map(async (note) => { const noteId = (note as any).noteId; const attachments = await WorkNoteAttachment.findAll({ where: { noteId } }); const noteData = (note as any).toJSON(); const mappedAttachments = attachments.map((a: any) => { const attData = typeof a.toJSON === 'function' ? a.toJSON() : a; return { attachmentId: attData.attachmentId || attData.attachment_id, fileName: attData.fileName || attData.file_name, fileType: attData.fileType || attData.file_type, fileSize: attData.fileSize || attData.file_size, filePath: attData.filePath || attData.file_path, storageUrl: attData.storageUrl || attData.storage_url, isDownloadable: attData.isDownloadable || attData.is_downloadable, uploadedAt: attData.uploadedAt || attData.uploaded_at }; }); return { noteId: noteData.noteId || noteData.note_id, requestId: noteData.requestId || noteData.request_id, userId: noteData.userId || noteData.user_id, userName: noteData.userName || noteData.user_name, userRole: noteData.userRole || noteData.user_role, message: noteData.message, isPriority: noteData.isPriority || noteData.is_priority, hasAttachment: noteData.hasAttachment || noteData.has_attachment, createdAt: noteData.createdAt || noteData.created_at, updatedAt: noteData.updatedAt || noteData.updated_at, attachments: mappedAttachments }; })); return enriched; } async getUserRole(requestId: string, userId: string): Promise { try { const participant = await Participant.findOne({ where: { requestId, userId } }); if (participant) { const type = (participant as any).participantType || (participant as any).participant_type; return type ? type.toString() : 'Participant'; } return 'Participant'; } catch (error) { logger.error('[WorkNote] Error fetching user role:', error); return 'Participant'; } } async create(requestId: string, user: { userId: string; name?: string; role?: string }, payload: { message: string; isPriority?: boolean; parentNoteId?: string | null; mentionedUsers?: string[] | null; }, files?: Array<{ path: string; originalname: string; mimetype: string; size: number }>): Promise { logger.info('[WorkNote] Creating note:', { requestId, user, messageLength: payload.message?.length }); const note = await WorkNote.create({ requestId, userId: user.userId, userName: user.name || null, userRole: user.role || null, // Store participant type (INITIATOR/APPROVER/SPECTATOR) message: payload.message, isPriority: !!payload.isPriority, parentNoteId: payload.parentNoteId || null, mentionedUsers: payload.mentionedUsers || null, hasAttachment: files && files.length > 0 ? true : false } as any); logger.info('[WorkNote] Created note:', { noteId: (note as any).noteId, userId: (note as any).userId, userName: (note as any).userName, userRole: (note as any).userRole }); const attachments = []; if (files && files.length) { for (const f of files) { const attachment = await WorkNoteAttachment.create({ noteId: (note as any).noteId, fileName: f.originalname, fileType: f.mimetype, fileSize: f.size, filePath: f.path, isDownloadable: true } as any); attachments.push({ attachmentId: (attachment as any).attachmentId, fileName: (attachment as any).fileName, fileType: (attachment as any).fileType, fileSize: (attachment as any).fileSize, filePath: (attachment as any).filePath, isDownloadable: (attachment as any).isDownloadable }); } } // Log activity for work note activityService.log({ requestId, type: 'comment', user: { userId: user.userId, name: user.name || 'User' }, timestamp: new Date().toISOString(), action: 'Work Note Added', details: `${user.name || 'User'} added a work note: ${payload.message.substring(0, 100)}${payload.message.length > 100 ? '...' : ''}` }); try { // Optional realtime emit (if socket layer is initialized) const { emitToRequestRoom } = require('../realtime/socket'); if (emitToRequestRoom) { // Emit note with all fields explicitly (to ensure camelCase fields are sent) const noteData = { noteId: (note as any).noteId, requestId: (note as any).requestId, userId: (note as any).userId, userName: (note as any).userName, userRole: (note as any).userRole, // Include participant role message: (note as any).message, createdAt: (note as any).createdAt, hasAttachment: (note as any).hasAttachment, attachments: attachments // Include attachments }; emitToRequestRoom(requestId, 'worknote:new', { note: noteData }); } } catch (e) { logger.warn('Realtime emit failed (not initialized)'); } return { ...note, attachments }; } async downloadAttachment(attachmentId: string) { const attachment = await WorkNoteAttachment.findOne({ where: { attachmentId } }); if (!attachment) { throw new Error('Attachment not found'); } return { filePath: (attachment as any).filePath, fileName: (attachment as any).fileName, fileType: (attachment as any).fileType }; } } export const workNoteService = new WorkNoteService();