import { Button, Flex, Form, Mentions, Space, Tooltip, Typography, message } from 'antd'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PaperClipOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'; import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { colors } from '@/styles/colors'; import { themeWiseColor } from '@/utils/themeWiseColor'; import { IMentionMemberSelectOption, } from '@/types/project/projectComments.types'; import { ITaskCommentsCreateRequest } from '@/types/tasks/task-comments.types'; import { ITaskAttachment } from '@/types/tasks/task-attachment-view-model'; import logger from '@/utils/errorLogger'; import taskCommentsApiService from '@/api/tasks/task-comments.api.service'; import { teamMembersApiService } from '@/api/team-members/teamMembers.api.service'; import { ITeamMember } from '@/types/teamMembers/teamMember.types'; // Utility function to convert file to base64 const getBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result as string); reader.onerror = error => reject(error); }); }; // Utility function to format file size const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const InfoTabFooter = () => { const { t } = useTranslation('task-drawer/task-drawer'); const MAXIMUM_FILE_COUNT = 5; const [characterLength, setCharacterLength] = useState(0); const [isCommentBoxExpand, setIsCommentBoxExpand] = useState(false); const [attachmentComment, setAttachmentComment] = useState(false); const [selectedFiles, setSelectedFiles] = useState([]); const { taskFormViewModel, selectedTaskId } = useAppSelector(state => state.taskDrawerReducer); const { projectId } = useAppSelector(state => state.projectReducer); const dispatch = useAppDispatch(); const [members, setMembers] = useState([]); const [membersLoading, setMembersLoading] = useState(false); const [selectedMembers, setSelectedMembers] = useState< { team_member_id: string; name: string }[] >([]); const [commentValue, setCommentValue] = useState(''); const [uploading, setUploading] = useState(false); const [form] = Form.useForm(); const fileInputRef = useRef(null); // get theme details from theme slice const themeMode = useAppSelector(state => state.themeReducer.mode); // get member list from project members slice const projectMembersList = useAppSelector(state => state.projectMemberReducer.membersList); // function to handle cancel const handleCancel = () => { form.resetFields(['comment']); setCharacterLength(0); setIsCommentBoxExpand(false); setSelectedFiles([]); setAttachmentComment(false); setCommentValue(''); setSelectedMembers([]); }; // Check if comment is valid (either has text or files) const isCommentValid = useCallback(() => { return characterLength > 0 || selectedFiles.length > 0; }, [characterLength, selectedFiles.length]); const getMembers = useCallback(async () => { if (!projectId) return; try { setMembersLoading(true); const res = await teamMembersApiService.get(1, 10, null, null, null, true); if (res.done) { setMembers(res.body.data?.filter(t => !t.pending_invitation) as ITeamMember[]); } } catch (error) { console.error('Failed to fetch members:', error); } finally { setMembersLoading(false); } }, [projectId]); // mentions options const mentionsOptions = members?.map(member => ({ value: member.name, label: member.name, key: member.id, })) ?? []; const memberSelectHandler = useCallback( (member: IMentionMemberSelectOption) => { if (!member?.value || !member?.label) return; // Find the member ID from the members list using the name const selectedMember = members.find(m => m.name === member.value); if (!selectedMember) return; // Add to selected members if not already present setSelectedMembers(prev => prev.some(mention => mention.team_member_id === selectedMember.id) ? prev : [...prev, { team_member_id: selectedMember.id!, name: selectedMember.name! }] ); }, [members] ); const handleCommentChange = useCallback((value: string) => { setCommentValue(value); setCharacterLength(value.trim().length); }, []); const handleSubmit = useCallback(async () => { if (!selectedTaskId || !projectId) return; if (!isCommentValid()) { message.error(t('taskInfoTab.comments.addCommentError')); return; } try { setUploading(true); const body: ITaskCommentsCreateRequest = { task_id: selectedTaskId, content: commentValue || '', mentions: Array.from(new Set(selectedMembers.map(member => JSON.stringify(member)))).map( str => JSON.parse(str) ), attachments: selectedFiles, }; const res = await taskCommentsApiService.create(body); if (res.done) { form.resetFields(['comment']); setCharacterLength(0); setSelectedFiles([]); setAttachmentComment(false); setIsCommentBoxExpand(false); setCommentValue(''); setSelectedMembers([]); // Dispatch event to notify that a comment was created // This will trigger the task comments component to refresh and update Redux document.dispatchEvent(new CustomEvent('task-comment-create', { detail: { taskId: selectedTaskId } })); } } catch (error) { logger.error('Failed to create comment:', error); } finally { setUploading(false); } }, [ commentValue, selectedMembers, selectedFiles, selectedTaskId, projectId, form, isCommentValid, ]); const handleFileChange = async (event: React.ChangeEvent) => { if (!event.target.files || !event.target.files.length || !selectedTaskId || !projectId) return; const files = Array.from(event.target.files); if (selectedFiles.length + files.length > MAXIMUM_FILE_COUNT) { message.error(t('taskInfoTab.comments.maxFilesError', { count: MAXIMUM_FILE_COUNT })); return; } try { setUploading(true); setAttachmentComment(true); const newFiles: ITaskAttachment[] = []; for (const file of files) { const base64Data = await getBase64(file); const attachment: ITaskAttachment = { file: base64Data, file_name: file.name, project_id: projectId, task_id: selectedTaskId, size: file.size, }; newFiles.push(attachment); } setSelectedFiles(prev => [...prev, ...newFiles]); // Expand the comment box if it's not already expanded if (!isCommentBoxExpand) { setIsCommentBoxExpand(true); } } catch (error) { console.error('Failed to process files:', error); message.error(t('taskInfoTab.comments.processFilesError')); } finally { setUploading(false); // Reset the file input so the same file can be selected again if (fileInputRef.current) { fileInputRef.current.value = ''; } } }; const removeFile = (index: number) => { setSelectedFiles(prev => { const newFiles = [...prev]; newFiles.splice(index, 1); if (newFiles.length === 0) { setAttachmentComment(false); } return newFiles; }); }; useEffect(() => { void getMembers(); }, [getMembers]); return (
{!isCommentBoxExpand ? ( // Collapsed state - simple textarea with counter setIsCommentBoxExpand(true)} onChange={e => setCharacterLength(e.length)} prefix="@" filterOption={(input, option) => { if (!input) return true; const optionLabel = (option as any)?.label || ''; return optionLabel.toLowerCase().includes(input.toLowerCase()); }} style={{ minHeight: 60, resize: 'none', borderRadius: 4, transition: 'all 0.3s ease-in-out', }} /> ) : ( // Expanded state - textarea with buttons
{selectedFiles.length > 0 && ( {t('taskInfoTab.comments.selectedFiles', { count: MAXIMUM_FILE_COUNT })} {selectedFiles.map((file, index) => ( {file.file_name} ({formatFileSize(file.size)}) )}
memberSelectHandler(option as IMentionMemberSelectOption)} onChange={handleCommentChange} prefix="@" filterOption={(input, option) => { if (!input) return true; const optionLabel = (option as any)?.label || ''; return optionLabel.toLowerCase().includes(input.toLowerCase()); }} style={{ minHeight: 100, maxHeight: 200, overflow: 'auto', paddingBlockEnd: 24, resize: 'none', borderRadius: 4, transition: 'all 0.3s ease-in-out', }} /> {`${characterLength}/5000`}
= MAXIMUM_FILE_COUNT} multiple /> = MAXIMUM_FILE_COUNT ? t('taskInfoTab.comments.maxFilesError', { count: MAXIMUM_FILE_COUNT }) : t('taskInfoTab.comments.attachFiles') } >
)} {t('taskInfoTab.comments.createdBy', { time: taskFormViewModel?.task?.created_from_now || 'N/A', user: taskFormViewModel?.task?.reporter || '' })} {t('taskInfoTab.comments.updatedTime', { time: taskFormViewModel?.task?.updated_from_now || 'N/A' })} ); }; export default InfoTabFooter;