170 lines
6.2 KiB
TypeScript
170 lines
6.2 KiB
TypeScript
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<string> {
|
|
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<any> {
|
|
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();
|
|
|
|
|