// public/js/managers/taskManager.js import eventBus from '../utils/eventBus.js'; // Import service functions import { getTasks as getServiceTasks, createTask as createServiceTask, updateTask as updateServiceTask, deleteTask as deleteServiceTask, // Import comment service functions from taskService getTaskComments as getServiceTaskComments, addCommentToTask as addServiceCommentToTask, updateTaskComment as updateServiceTaskComment, deleteTaskComment as deleteServiceTaskComment } from '../services/taskService.js'; import * as userManager from './userManager.js'; // Import userManager import { generateAvatarUrl } from '../utils/utils.js'; // Assuming generateAvatarUrl is kept in utils import { handleError } from '../utils/errorHandler.js'; // Keep handleError for now, might refactor error handling later import { db } from '../firebase-init.js'; // Import db // --- Internal State (Caches) --- let tasksCache = {}; // Use object for faster lookup by ID let commentsCache = {}; // Cache comments by task ID: { taskId: [comment1, comment2, ...] } // --- Internal Helper: Load all tasks into cache --- async function loadAllTasksIntoCache() { console.log('TaskManager: Loading all tasks into cache'); try { const tasksArray = await getServiceTasks(); tasksCache = tasksArray.reduce((cache, task) => { cache[task.id] = task; return cache; }, {}); console.log('TaskManager: All tasks loaded into cache:', Object.keys(tasksCache).length); // Publish event after cache is loaded eventBus.publish('tasksCacheLoaded', tasksCache); eventBus.publish('tasksDataReady', Object.values(tasksCache)); // Publish the array for UI rendering } catch (error) { console.error('TaskManager: Error loading all tasks into cache:', error); eventBus.publish('tasksLoadFailed', { message: 'Không thể tải danh sách công việc.', error: error }); } } // --- Internal Helper: Process raw comment data (add author info, format dates) --- function processCommentForCache(comment) { const author = userManager.getUserData(comment.userId); // console.log('TaskManager: processCommentForCache - Input comment:', comment); // Log input if (!author) console.warn(`TaskManager: User data not found in userManager cache for userId ${comment.userId} for comment ${comment.id}`); const processed = { ...comment, authorName: author?.name || 'Người dùng không rõ', authorAvatar: author?.avatar || generateAvatarUrl(author?.name || 'Người dùng'), // Explicitly handle Firebase Timestamps and existing Date objects createdAt: comment.createdAt?.toDate ? comment.createdAt.toDate() : null, updatedAt: comment.updatedAt?.toDate ? comment.updatedAt.toDate() : (comment.updatedAt instanceof Date ? comment.updatedAt : null), // Handle already a Date }; console.log('TaskManager: processCommentForCache - Processed comment:', processed); // Log output return processed; } // --- Internal Helper: Load comments for a specific task into cache --- // Renamed and moved from commentManager.js async function handleLoadTaskComments(taskId, forceReload = false) { console.log('TaskManager: handleLoadTaskComments called for taskId', taskId, 'forceReload:', forceReload); if (!taskId) { console.warn('TaskManager: handleLoadTaskComments called without taskId.'); eventBus.publish('commentLoadFailed', { taskId, message: 'Thiếu ID công việc để tải bình luận.' }); return []; // Return empty array as a safe default } // Use cache if available and not forced to reload if (commentsCache[taskId] && !forceReload) { console.log('TaskManager: Returning comments from cache for taskId', taskId); // Publish the cached data eventBus.publish('taskCommentsLoaded', { taskId, comments: commentsCache[taskId] }); return commentsCache[taskId]; // Return cached data } // If cache is not available or forced reload try { console.log('TaskManager: Fetching comments from service for taskId', taskId); const comments = await getServiceTaskComments(taskId); console.log('TaskManager: Fetched', comments.length, 'comments from service.'); // Process and filter comments before caching const processedComments = comments .map(processCommentForCache) // Add author info, format dates .filter(comment => !comment.deleted); // Filter out soft-deleted comments for display // Sort comments (e.g., by creation date) processedComments.sort((a, b) => (a.createdAt?.getTime() || 0) - (b.createdAt?.getTime() || 0)); // Update cache commentsCache[taskId] = processedComments; console.log('TaskManager: Updated cache for taskId', taskId); // Publish success event with the processed and cached comments eventBus.publish('taskCommentsLoaded', { taskId, comments: processedComments }); return processedComments; // Return the fetched and processed data } catch (error) { console.error('TaskManager: Error loading comments for taskId', taskId, ':', error); // Publish error event eventBus.publish('commentLoadFailed', { taskId, error: error.message || 'Không thể tải bình luận.' }); throw error; // Re-throw for higher-level error handling } } // Function for UI to get cached comment data immediately - Moved from commentManager.js function getTaskCommentsData(taskId) { console.log('TaskManager: getTaskCommentsData (from cache) called for taskId', taskId); return commentsCache[taskId] || []; // Return empty array if no comments cached } // --- Task Action Handlers (Called by UI via EventBus or direct call) --- async function handleSaveTask(taskData) { // Handles both add and initial save for edit console.log('TaskManager: handleSaveTask called with data', taskData); // Basic validation (can be more extensive in UI) if (!taskData.title || !taskData.status) { eventBus.publish('taskActionFailed', { action: 'save', message: 'Tiêu đề và trạng thái công việc không được để trống.' }); return; } try { console.log('TaskManager: Calling createServiceTask'); const newTask = await createServiceTask(taskData); // Service call successful. Update cache and publish event. console.log('TaskManager: Task created successfully. Updating cache and publishing event.'); tasksCache[newTask.id] = newTask; // Add to cache // Publish a specific event for UI to update eventBus.publish('taskAdded', newTask); eventBus.publish('tasksDataReady', Object.values(tasksCache)); // Publish updated full list } catch (error) { console.error('TaskManager: Error saving task:', error); // Publish an error event for UI eventBus.publish('taskActionFailed', { action: 'save', message: 'Không thể lưu công việc.', error: error }); } } async function handleUpdateTask(data) { const { taskId, taskData } = data; console.log('TaskManager: handleUpdateTask called for ID', taskId, 'with data', taskData); // Keep this log to verify data if (!taskId || !taskData.title || !taskData.status) { eventBus.publish('taskActionFailed', { action: 'update', message: 'Thiếu thông tin công việc để cập nhật.' }); return; } try { console.log('TaskManager: Calling updateServiceTask'); const updatedTask = await updateServiceTask(taskId, taskData); // Service call successful. Update cache and publish event. console.log('TaskManager: Task updated successfully. Updating cache and publishing event.'); // Update in cache if (tasksCache[taskId]) { tasksCache[taskId] = { ...tasksCache[taskId], ...updatedTask }; // Merge updated data } else { console.warn(`TaskManager: Updated task with ID ${taskId} not found in cache. Reloading all tasks.`); loadAllTasksIntoCache(); // Fallback to full reload } // Publish a specific event for UI to update eventBus.publish('taskUpdated', tasksCache[taskId]); // Publish the updated object from cache eventBus.publish('tasksDataReady', Object.values(tasksCache)); // Publish updated full list } catch (error) { console.error(`TaskManager: Error updating task ${taskId}:`, error); // Publish an error event for UI eventBus.publish('taskActionFailed', { action: 'update', message: 'Không thể cập nhật công việc.', error: error }); } } async function handleDeleteTask(taskId) { // Renamed from handleDeleteTaskClick for consistency console.log('TaskManager: handleDeleteTask called for ID', taskId); if (!taskId) { eventBus.publish('taskActionFailed', { action: 'delete', message: 'Thiếu ID công việc để xoá.' }); return; } try { console.log('TaskManager: Calling deleteTaskService'); const deletedTaskId = await deleteServiceTask(taskId); // Service call successful. Update cache and publish event. console.log('TaskManager: Task deleted successfully. Updating cache and publishing event.'); // Remove from cache delete tasksCache[deletedTaskId]; // Also remove comments cache for this task delete commentsCache[deletedTaskId]; // Publish a specific event for UI to update eventBus.publish('taskDeleted', deletedTaskId); eventBus.publish('tasksDataReady', Object.values(tasksCache)); // Publish updated full list } catch (error) { console.error(`TaskManager: Error deleting task ${taskId}:`, error); // Publish an error event for UI eventBus.publish('taskActionFailed', { action: 'delete', message: 'Không thể xoá công việc.', error: error }); } } // --- Comment Action Handlers (Called by UI via EventBus or direct call) --- // Moved and updated from commentManager.js // Điều chỉnh thứ tự thực hiện và sửa lỗi ReferenceError async function handleAddComment(eventData) { console.log('TaskManager: handleAddComment called for Task ID', eventData.taskId, 'with text:', eventData.text); const { taskId, text } = eventData; // Validate input if (!taskId || typeof taskId !== 'string' || taskId.trim() === '') { console.error('TaskManager: handleAddComment - Invalid Task ID:', taskId); eventBus.publish('commentSaveFailed', { error: 'Task ID không hợp lệ.', taskId }); return; } if (!text || text.trim() === '') { eventBus.publish('commentSaveFailed', { error: 'Nội dung bình luận không được để trống.', taskId }); return; } const currentUser = userManager.getCurrentUserData(); if (!currentUser || !currentUser.id) { console.error('TaskManager: handleAddComment - User data not available. Aborting comment submission.'); eventBus.publish('commentSaveFailed', { error: 'Người dùng chưa đăng nhập.', taskId }); return; } const commentData = { taskId: taskId, userId: currentUser.id, text: text, // createdAt và updatedAt sẽ được set bởi server Firestore deleted: false, edited: false }; let fetchedNewComment = null; // Khai báo và khởi tạo biến ở ngoài khối try try { console.log('TaskManager: Calling addServiceCommentToTask'); // Service now returns the full comment object with server timestamps fetchedNewComment = await addServiceCommentToTask(taskId, commentData); console.log('TaskManager: Received comment from service (fetched by service):', fetchedNewComment); // Process the fetched comment (which now should have server timestamps) const processedNewComment = processCommentForCache(fetchedNewComment); console.log('TaskManager: Raw fetchedNewComment object BEFORE processing:', fetchedNewComment); console.log('TaskManager: processedNewComment object AFTER processing:', processedNewComment); console.log('TaskManager: Type of processedNewComment.createdAt:', typeof processedNewComment.createdAt); console.log('TaskManager: Value of processedNewComment.createdAt:', processedNewComment.createdAt); console.log('TaskManager: Comment added successfully. Updating cache and publishing event.'); // Update cache if (!commentsCache[taskId]) { commentsCache[taskId] = []; } // Find and replace the comment in cache if it exists (e.g., from a previous incomplete add) const existingIndex = commentsCache[taskId].findIndex(c => c.id === processedNewComment.id); if (existingIndex > -1) { commentsCache[taskId][existingIndex] = processedNewComment; console.log('TaskManager: Replaced existing comment in cache:', processedNewComment.id); } else { commentsCache[taskId].push(processedNewComment); console.log('TaskManager: Added new comment to cache:', processedNewComment.id); } // Keep comments sorted by createdAt (now should be Date objects) commentsCache[taskId].sort((a, b) => (a.createdAt?.getTime() || 0) - (b.createdAt?.getTime() || 0)); console.log('TaskManager: Cache sorted.'); // Publish specific event for UI to add comment // PUBLISH ONLY AFTER FETCHING AND PROCESSING SUCCEED eventBus.publish('commentAdded', { taskId: taskId, comment: processedNewComment }); console.log('TaskManager: Publishing commentAdded event with object:', processedNewComment); console.log('TaskManager: Type of object.createdAt being published:', typeof processedNewComment.createdAt); console.log('TaskManager: Value of object.createdAt being published:', processedNewComment.createdAt); eventBus.publish('taskCommentsLoaded', { taskId: taskId, comments: commentsCache[taskId] }); // Publish updated full list after adding } catch (error) { console.error('TaskManager: handleAddComment error during add or fetch:', error); // Sử dụng biến taskId được truyền vào hàm eventBus.publish('commentSaveFailed', { error: error.message || 'Không thể thêm hoặc tải bình luận.', taskId: taskId }); // Không throw error ở đây để không chặn các xử lý khác nếu cần } } async function handleSaveEditedComment(taskId, commentId, newText) { console.log('TaskManager: handleSaveEditedComment called for task', taskId, 'comment', commentId, 'with text', newText); if (!taskId || !commentId || !newText || newText.trim() === '') { eventBus.publish('commentSaveFailed', { error: 'Nội dung bình luận không được để trống.', taskId, commentId }); return; } const updateData = { text: newText.trim(), edited: true, updatedAt: db.firestore.FieldValue.serverTimestamp(), // Sử dụng db.firestore.FieldValue }; try { console.log('TaskManager: Calling updateServiceTaskComment'); await updateServiceTaskComment(taskId, commentId, updateData); console.log('TaskManager: Comment updated via service:', commentId); // After successfully updating, update cache and publish event console.log('TaskManager: Comment updated successfully. Updating cache and publishing event.'); if (commentsCache[taskId]) { const index = commentsCache[taskId].findIndex(comment => comment.id === commentId); if (index !== -1) { // Update text, edited status, and updatedAt in cache commentsCache[taskId][index] = { ...commentsCache[taskId][index], ...updateData }; // Ensure createdAt remains (might be lost in simple merge) if (!commentsCache[taskId][index].createdAt && commentsCache[taskId][index].createdAt_raw) { commentsCache[taskId][index].createdAt = commentsCache[taskId][index].createdAt_raw; // Assuming raw timestamp was stored } // Re-process the updated comment to ensure author info/date format is correct const updatedComment = processCommentForCache(commentsCache[taskId][index]); commentsCache[taskId][index] = updatedComment; // Keep comments sorted by createdAt commentsCache[taskId].sort((a, b) => (a.createdAt?.getTime() || 0) - (b.createdAt?.getTime() || 0)); // Publish the updated comment object eventBus.publish('commentUpdated', { taskId: taskId, comment: updatedComment }); eventBus.publish('taskCommentsLoaded', { taskId: taskId, comments: commentsCache[taskId] }); // Publish updated full list } else { console.warn(`TaskManager: Updated comment with ID ${commentId} not found in cache for task ${taskId}. Reloading comments for task.`); handleLoadTaskComments(taskId, true); // Fallback to full reload } } else { console.warn(`TaskManager: Comment cache for task ${taskId} not found during update. Reloading comments for task.`); handleLoadTaskComments(taskId, true); // Fallback to full reload } } catch (error) { console.error(`TaskManager: Error updating comment ${commentId} for task ${taskId}:`, error); eventBus.publish('commentSaveFailed', { error: error.message || 'Không thể cập nhật bình luận.', taskId, commentId }); } } async function handleDeleteComment(taskId, commentId) { // Handles soft delete console.log('TaskManager: handleDeleteComment (soft delete) called for Task ID', taskId, 'Comment ID', commentId); if (!taskId || !commentId) { eventBus.publish('commentDeleteFailed', { error: 'Thiếu thông tin bình luận để xoá.', taskId, commentId }); return; } const updateData = { deleted: true, edited: true, // Mark as edited as content changes updatedAt: db.firestore.FieldValue.serverTimestamp(), // Sử dụng db.firestore.FieldValue }; try { console.log('TaskManager: Calling updateServiceTaskComment for soft delete commentId', commentId); await updateServiceTaskComment(taskId, commentId, updateData); console.log('TaskManager: Comment soft deleted via service:', commentId); // After successfully soft deleting, update cache and publish event console.log('TaskManager: Comment soft deleted successfully. Updating cache and publishing event.'); if (commentsCache[taskId]) { const index = commentsCache[taskId].findIndex(comment => comment.id === commentId); if (index !== -1) { // Update deleted status, edited status, and updatedAt in cache commentsCache[taskId][index] = { ...commentsCache[taskId][index], ...updateData }; // Re-process the soft-deleted comment to ensure author info/date format is correct and filter it out const updatedComment = processCommentForCache(commentsCache[taskId][index]); // Since it's soft deleted, remove it from the displayed cache array commentsCache[taskId] = commentsCache[taskId].filter(comment => comment.id !== commentId); // Publish a specific event for UI to remove comment from display eventBus.publish('commentDeleted', { taskId: taskId, commentId: commentId }); eventBus.publish('taskCommentsLoaded', { taskId: taskId, comments: commentsCache[taskId] }); // Publish updated full list } else { console.warn(`TaskManager: Soft deleted comment with ID ${commentId} not found in cache for task ${taskId}. Reloading comments for task.`); handleLoadTaskComments(taskId, true); // Fallback to full reload for comments } } else { console.warn(`TaskManager: Comment cache for task ${taskId} not found during soft delete. Cache might be out of sync.`); handleLoadTaskComments(taskId, true); // Fallback to full reload for comments } } catch (error) { console.error('TaskManager: handleDeleteComment error:', error); eventBus.publish('commentDeleteFailed', { error: error.message || 'Không thể xoá bình luận.', taskId, commentId }); } } async function handleHardDeleteComment(taskId, commentId) { console.log('TaskManager: handleHardDeleteComment called for Task ID', taskId, 'Comment ID', commentId); if (!taskId || !commentId) { eventBus.publish('commentDeleteFailed', { error: 'Thiếu thông tin bình luận để xoá vĩnh viễn.', taskId, commentId }); return; } console.log('TaskManager: Calling deleteTaskComment service for hard delete commentId', commentId); try { await deleteServiceTaskComment(taskId, commentId); console.log('TaskManager: Comment hard deleted via service:', commentId); // After successfully hard deleting, update cache and publish event console.log('TaskManager: Comment hard deleted successfully. Updating cache and publishing event.'); // Remove from comments cache for this task if (commentsCache[taskId]) { commentsCache[taskId] = commentsCache[taskId].filter(comment => comment.id !== commentId); } else { console.warn(`TaskManager: Comment cache for task ${taskId} not found during hard delete. Cache might be out of sync.`); // Optionally trigger a full reload of comments for this task handleLoadTaskComments(taskId, true); // Fallback to full reload } // Publish a specific event for UI to remove comment eventBus.publish('commentDeleted', { taskId: taskId, commentId: commentId }); eventBus.publish('taskCommentsLoaded', { taskId: taskId, comments: commentsCache[taskId] }); // Publish updated full list } catch (error) { console.error('TaskManager: handleHardDeleteComment error:', error); eventBus.publish('commentDeleteFailed', { error: error.message || 'Không thể xoá vĩnh viễn bình luận.', taskId, commentId }); } } // --- Helper to get cached data --- // Kept from previous version and commentManager.js function getAllTasksFromCache() { console.log('TaskManager: getAllTasksFromCache called. Cache size:', Object.keys(tasksCache).length); return Object.values(tasksCache); } function getTaskByIdFromCache(taskId) { console.log('TaskManager: getTaskByIdFromCache called for ID:', taskId); return tasksCache[taskId] || null; } // --- EventBus Listeners --- function setupEventBusListeners() { console.log('TaskManager: Setting up EventBus listeners'); // Listen for currentUserDataLoaded from userManager to trigger initial task load eventBus.subscribe('currentUserDataLoaded', () => { console.log('TaskManager: currentUserDataLoaded event received. Loading tasks...'); loadAllTasksIntoCache(); // Initial load of tasks }); // Listen for task detail modal opening to load comments for that task // This assumes a UI component publishes this event when the modal is opened eventBus.subscribe('taskDetailModalOpened', (taskId) => { console.log(`TaskManager: taskDetailModalOpened event received for Task ID: ${taskId}. Loading comments.`); handleLoadTaskComments(taskId); // Load comments when modal opens }); // Listen for task deletion to clear comment cache for that task eventBus.subscribe('taskDeleted', (deletedTaskId) => { // Assuming taskManager publishes taskDeleted with taskId if (commentsCache[deletedTaskId]) { delete commentsCache[deletedTaskId]; console.log('TaskManager: Cleared comment cache for deleted task', deletedTaskId); } }); // No listeners for Service events anymore. } // --- Setup UI Event Listeners --- function setupUIEventListeners() { console.log('TaskManager: Setting up UI Event listeners'); // Listen for UI events published by listeners.js or tasks.js eventBus.subscribe('saveTaskSubmit', handleSaveTask); // Listen for form submit to save/add task eventBus.subscribe('updateTaskSubmit', handleUpdateTask); // Listen for form submit to update task eventBus.subscribe('deleteTaskClicked', handleDeleteTask); // Listen for delete button click (after confirmation in UI) // Comment UI Listeners (Moved from where they were in commentManager.js or UI file) eventBus.subscribe('addCommentSubmit', handleAddComment); // Listen for add comment form submit eventBus.subscribe('saveEditedCommentSubmit', handleSaveEditedComment); // Listen for save edited comment button click // Note: Assuming UI publishes these events after confirmation for hard delete eventBus.subscribe('deleteCommentClicked', handleDeleteComment); // Listen for soft delete (if used) eventBus.subscribe('hardDeleteCommentClicked', handleHardDeleteComment); // Listen for hard delete } // --- Initialization Function --- function initTaskManager() { console.log('Initializing Task Manager'); setupEventBusListeners(); // Set up listeners for other managers/UI setupUIEventListeners(); // Set up listeners for UI actions // Initial load of tasks is now triggered by currentUserDataLoaded event // Initial load of comments is triggered by taskDetailModalOpened event } // Export public functions export { // Export task handlers handleSaveTask, handleUpdateTask, handleDeleteTask, // Export comment handlers (now in TaskManager) handleLoadTaskComments, // Export the function to load comments handleAddComment, handleSaveEditedComment, handleDeleteComment, // Soft delete handleHardDeleteComment, // Hard delete // Export cache getters getAllTasksFromCache, getTaskByIdFromCache, getTaskCommentsData, // Export the function to get cached comments initTaskManager };