const { useState, useEffect } = React;
// Defensive normalization helper
function normalizeToArray(x) {
console.log('[normalizeToArray] Input:', typeof x, x);
if (!x) return [];
if (Array.isArray(x)) return x;
if (typeof x === 'object') {
if (Array.isArray(x.data)) return x.data;
if (Array.isArray(x.assignments)) return x.assignments;
return [x];
}
return [];
}
// STEP 4: Error Boundary Component
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('[ErrorBoundary] Caught error:', error, errorInfo);
this.setState({ error, errorInfo });
}
render() {
if (this.state.hasError) {
return (
setToast(null)}
/>
)}
>
);
}
// Projects Component
function Projects() {
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [selectedProject, setSelectedProject] = useState(null);
const [toast, setToast] = useState(null);
useEffect(() => {
loadProjects();
}, []);
const loadProjects = async () => {
console.log('[Projects] Loading projects list...');
try {
const result = await api.get('/projects');
if (result.success) {
console.log('[Projects] Loaded projects:', result.data);
setProjects(result.data);
} else {
console.error('[Projects] Failed to load projects:', result.error);
setToast({ type: 'error', message: result.error || 'Failed to load projects' });
}
} catch (error) {
console.error('[Projects] Error loading projects:', error);
setToast({ type: 'error', message: 'Network error loading projects' });
} finally {
setLoading(false);
}
};
const createProject = () => {
setSelectedProject(null);
setShowModal(true);
};
// Handler for successful project save - OPTION A: Update state directly
const handleProjectSaved = (savedProject, isNew) => {
console.log('[Projects] Project saved:', savedProject, 'isNew:', isNew);
if (isNew) {
// OPTION A (Preferred): Prepend new project to state
setProjects(prevProjects => [savedProject, ...prevProjects]);
console.log('[Projects] New project added to state');
setToast({ type: 'success', message: 'â
Project created successfully!' });
} else {
// Update existing project in state
setProjects(prevProjects =>
prevProjects.map(p => p.id === savedProject.id ? savedProject : p)
);
console.log('[Projects] Project updated in state');
setToast({ type: 'success', message: 'â
Project updated successfully!' });
}
setShowModal(false);
};
// Fallback handler if state update fails
const handleProjectSaveFallback = async () => {
console.log('[Projects] Using fallback: re-fetching projects');
setShowModal(false);
await loadProjects();
setToast({ type: 'success', message: 'â
Project saved successfully!' });
};
if (loading) {
return (
);
}
return (
My Projects
{projects.length === 0 ? (
đ
No projects yet
Create your first project to get started
) : (
{projects.map(project => (
))}
)}
{showModal && (
setShowModal(false)}
onSave={handleProjectSaved}
onSaveFallback={handleProjectSaveFallback}
/>
)}
{toast && (
setToast(null)}
/>
)}
);
}
// Project Card Component
function ProjectCard({ project, onSelect }) {
return (
onSelect(project)}>
{project.title}
{project.status}
{project.description || 'No description'}
{project.start_date && `Started: ${new Date(project.start_date).toLocaleDateString()}`}
{project.budget && `Budget: $${parseFloat(project.budget).toFixed(2)}`}
);
}
// Project Modal Component
function ProjectModal({ project, onClose, onSave, onSaveFallback }) {
const [formData, setFormData] = useState({
title: project?.title || '',
description: project?.description || '',
start_date: project?.start_date || '',
end_date: project?.end_date || '',
budget: project?.budget || '',
});
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
setError(null); // Clear error on input change
};
const handleSubmit = async (e) => {
e.preventDefault();
// Validation
if (!formData.title.trim()) {
setError('Project title is required');
return;
}
setSaving(true);
setError(null);
console.log('[ProjectModal] Submitting project:', formData);
try {
let result;
const isNew = !project;
if (project) {
console.log('[ProjectModal] Updating project ID:', project.id);
result = await api.put(`/projects/${project.id}`, formData);
} else {
console.log('[ProjectModal] Creating new project');
result = await api.post('/projects', formData);
}
console.log('[ProjectModal] API Result:', result);
if (result.success) {
// Extract the created/updated project from response
const savedProject = result.data;
console.log('[ProjectModal] Success! Saved project:', savedProject);
// OPTION A (Preferred): Call onSave with the saved project data
if (savedProject && savedProject.id) {
onSave(savedProject, isNew);
} else {
// OPTION B (Fallback): If response doesn't include project data, re-fetch
console.warn('[ProjectModal] No project data in response, using fallback');
onSaveFallback();
}
} else {
// Handle API errors (4xx, 5xx)
console.error('[ProjectModal] API Error:', result.error);
setError(result.error || 'Failed to save project. Please try again.');
// Show detailed error for debugging
if (result.status === 403) {
setError('Permission denied. You may not have access to create/edit projects.');
} else if (result.status === 401) {
setError('Authentication failed. Please refresh the page and try again.');
} else if (result.status >= 500) {
setError('Server error occurred. Please try again later.');
}
}
} catch (error) {
// Handle unexpected errors
console.error('[ProjectModal] Unexpected error:', error);
setError('An unexpected error occurred. Please check console for details.');
// Last resort fallback: reload page
if (window.confirm('An error occurred. Would you like to reload the page?')) {
window.location.reload();
}
} finally {
setSaving(false);
}
};
return (
>
}
>
);
}
// Tasks Component
function Tasks() {
const [projects, setProjects] = useState([]);
const [selectedProject, setSelectedProject] = useState(null);
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
const [loadingTasks, setLoadingTasks] = useState(false);
const [showTaskModal, setShowTaskModal] = useState(false);
const [selectedTask, setSelectedTask] = useState(null);
const [toast, setToast] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
console.log('[Tasks] Component mounted, loading projects...');
loadProjects();
}, []);
useEffect(() => {
if (selectedProject) {
console.log('[Tasks] Selected project changed to:', selectedProject);
loadTasks();
} else {
console.log('[Tasks] No project selected, clearing tasks');
setTasks([]);
}
}, [selectedProject]);
const loadProjects = async () => {
console.log('[Tasks] Loading projects list...');
setLoading(true);
setError(null);
try {
const result = await api.get('/projects');
console.log('[Tasks] Projects API result:', result);
if (result.success) {
const projectsData = result.data;
console.log('[Tasks] Loaded projects:', projectsData, 'Type:', Array.isArray(projectsData) ? 'Array' : typeof projectsData);
// Handle both array and {data: []} response formats
let projectsList = [];
if (Array.isArray(projectsData)) {
projectsList = projectsData;
} else if (projectsData && Array.isArray(projectsData.data)) {
console.log('[Tasks] Response wrapped in data object, unwrapping...');
projectsList = projectsData.data;
} else if (projectsData && typeof projectsData === 'object') {
console.warn('[Tasks] Unexpected response format:', projectsData);
projectsList = [];
}
console.log('[Tasks] Final projects list:', projectsList, 'Length:', projectsList.length);
setProjects(projectsList);
if (projectsList.length > 0) {
console.log('[Tasks] Auto-selecting first project:', projectsList[0]);
setSelectedProject(projectsList[0]);
} else {
console.log('[Tasks] No projects available');
setSelectedProject(null);
}
} else {
console.error('[Tasks] Failed to load projects:', result.error);
setError(result.error || 'Failed to load projects');
setProjects([]);
setSelectedProject(null);
}
} catch (error) {
console.error('[Tasks] Error loading projects:', error);
setError('Network error loading projects');
setProjects([]);
setSelectedProject(null);
} finally {
setLoading(false);
}
};
const loadTasks = async () => {
if (!selectedProject || !selectedProject.id) {
console.warn('[Tasks] Cannot load tasks: No project selected or invalid project ID');
setTasks([]);
return;
}
console.log('[Tasks] >>> Loading tasks for project ID:', selectedProject.id); // Enhanced logging
setLoadingTasks(true);
setError(null);
try {
// STEP 2: Construct proper API endpoint with project_id parameter
const endpoint = `/projects/${selectedProject.id}/tasks`;
const fullUrl = `${api.baseUrl}${endpoint}`;
console.log('[Tasks] >>> FETCH URL:', fullUrl); // STEP 2: Confirm fetch URL
console.log('[Tasks] Fetching tasks from:', endpoint);
const result = await api.get(endpoint);
console.log('[Tasks] >>> RAW API RESPONSE:', result); // STEP 3: Log raw response
if (result.success) {
const tasksData = result.data;
console.log('[Tasks] >>> Response data type:', typeof tasksData, 'isArray:', Array.isArray(tasksData));
console.log('[Tasks] >>> Response data:', tasksData);
// STEP 3 & 4: Handle multiple response formats with validation
let tasksList = [];
if (Array.isArray(tasksData)) {
// Direct array response
tasksList = tasksData;
console.log('[Tasks] >>> Direct array response, length:', tasksList.length);
} else if (tasksData && typeof tasksData === 'object' && tasksData.data) {
// Wrapped in data property
if (Array.isArray(tasksData.data)) {
tasksList = tasksData.data;
console.log('[Tasks] >>> Unwrapped from data property, length:', tasksList.length);
} else {
console.warn('[Tasks] >>> tasksData.data is not an array:', tasksData.data);
}
} else if (tasksData && typeof tasksData === 'object') {
// Object but not wrapped - might be single object, convert to array
console.warn('[Tasks] >>> Unexpected object format, attempting conversion:', tasksData);
// Check if it's a single task object with id
if (tasksData.id) {
tasksList = [tasksData];
} else {
tasksList = [];
}
} else {
console.warn('[Tasks] >>> Invalid tasks data type:', typeof tasksData, tasksData);
tasksList = [];
}
// STEP 4: Validate it's an array before setState
if (!Array.isArray(tasksList)) {
console.error('[Tasks] >>> tasksList is not an array after parsing! Type:', typeof tasksList);
tasksList = [];
}
console.log('[Tasks] >>> Final tasks list (validated array):', tasksList, 'Length:', tasksList.length);
console.log('[Tasks] >>> Calling setTasks with:', tasksList);
// STEP 4: Force state update
setTasks(tasksList);
// Verify state update in next tick
setTimeout(() => {
console.log('[Tasks] >>> State verification - tasks should be updated now');
}, 0);
if (tasksList.length === 0) {
console.log('[Tasks] >>> â ī¸ No tasks found for project:', selectedProject.title);
console.log('[Tasks] >>> This could be legitimate (no tasks) or a filtering issue');
}
} else {
// STEP 5: Error handling
console.error('[Tasks] >>> API returned success=false:', result.error);
setError(result.error || 'Failed to load tasks');
setTasks([]);
}
} catch (error) {
// STEP 5: Error handling
console.error('[Tasks] >>> Exception while loading tasks:', error);
console.error('[Tasks] >>> Error stack:', error.stack);
setError('Network error loading tasks');
setTasks([]);
} finally {
setLoadingTasks(false);
console.log('[Tasks] >>> loadTasks complete, loadingTasks set to false');
}
};
// Handler for task save with state update
const handleTaskSaved = (savedTask, isNew) => {
console.log('[Tasks] Task saved:', savedTask, 'isNew:', isNew);
if (isNew) {
// Prepend new task
setTasks(prevTasks => [savedTask, ...prevTasks]);
console.log('[Tasks] New task added to state');
setToast({ type: 'success', message: 'â
Task created successfully!' });
} else {
// Update existing task
setTasks(prevTasks =>
prevTasks.map(t => t.id === savedTask.id ? savedTask : t)
);
console.log('[Tasks] Task updated in state');
setToast({ type: 'success', message: 'â
Task updated successfully!' });
}
setShowTaskModal(false);
};
// Fallback handler
const handleTaskSaveFallback = async () => {
console.log('[Tasks] Using fallback: re-fetching tasks');
setShowTaskModal(false);
await loadTasks();
setToast({ type: 'success', message: 'â
Task saved successfully!' });
};
// Handle project selection change
const handleProjectChange = (e) => {
const projectId = parseInt(e.target.value);
console.log('[Tasks] >>> projectClick', projectId); // STEP 1: Log project click
console.log('[Tasks] Project dropdown changed to ID:', projectId);
const project = projects.find(p => p.id === projectId);
console.log('[Tasks] Found project:', project);
if (project) {
setSelectedProject(project);
// Clear tasks immediately while loading
setTasks([]);
} else {
console.warn('[Tasks] Project not found for ID:', projectId);
}
};
if (loading) {
return (
);
}
if (error && projects.length === 0) {
return (
â ī¸
Error Loading Projects
{error}
);
}
if (projects.length === 0) {
return (
đ
No projects available
Create a project first to manage tasks
);
}
console.log('[Tasks] >>> RENDER - Projects:', projects.length, 'Selected:', selectedProject?.title, 'Tasks:', tasks, 'Tasks.length:', tasks?.length, 'Array.isArray(tasks):', Array.isArray(tasks));
return (
Tasks
{loadingTasks && (
âŗ Loading tasks...
)}
{error && (
â ī¸ {error}
)}
{/* STEP 5: Loading state */}
{loadingTasks ? (
) : Array.isArray(tasks) && tasks.length > 0 ? (
<>
{console.log('[Tasks] >>> Rendering', tasks.length, 'task items')}
{tasks.map((task, index) => {
console.log('[Tasks] >>> Rendering task', index, ':', task.id, task.title);
return (
setSelectedTask(task)}
onUpdate={loadTasks}
/>
);
})}
>
) : (
/* STEP 5: Empty state */
đ
No tasks yet for "{selectedProject?.title}"
Tasks in database: {Array.isArray(tasks) ? tasks.length : 'N/A (not an array)'}
Check console for detailed debugging info
)}
{showTaskModal && (
setShowTaskModal(false)}
onSave={handleTaskSaved}
onSaveFallback={handleTaskSaveFallback}
/>
)}
{toast && (
setToast(null)}
/>
)}
);
}
// Task Item Component
function TaskItem({ task, onSelect, onUpdate }) {
const [quickReplies, setQuickReplies] = useState([]);
const [showUpdates, setShowUpdates] = useState(false);
useEffect(() => {
loadQuickReplies();
}, []);
const loadQuickReplies = async () => {
console.log('[TaskItem] Loading quick replies...');
try {
const result = await api.get('/quick-replies');
if (result.success) {
const repliesData = Array.isArray(result.data) ? result.data : [];
console.log('[TaskItem] Loaded quick replies:', repliesData);
setQuickReplies(repliesData);
} else {
console.error('[TaskItem] Failed to load quick replies:', result.error);
}
} catch (error) {
console.error('[TaskItem] Error loading quick replies:', error);
}
};
const handleQuickReply = async (reply) => {
console.log('[TaskItem] Sending quick reply:', reply, 'for task:', task.id);
try {
const result = await api.post(`/tasks/${task.id}/updates`, {
update_type: 'quick_reply',
quick_reply_id: reply.id,
message: reply.label,
new_status: reply.status,
});
if (result.success) {
console.log('[TaskItem] Quick reply sent successfully');
onUpdate();
alert(`â
${reply.emoji} ${reply.label} - Update sent!`);
} else {
console.error('[TaskItem] Quick reply failed:', result.error);
alert('Error sending update: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('[TaskItem] Error sending quick reply:', error);
alert('Error sending update');
}
};
const statusColors = {
pending: '#95a5a6',
in_progress: '#3498db',
review: '#9b59b6',
delayed: '#e74c3c',
blocked: '#e67e22',
completed: '#27ae60',
};
return (
{task.title}
{task.priority}
{task.description}
{task.status.replace('_', ' ').toUpperCase()}
{task.due_date && (
đ
Due: {new Date(task.due_date).toLocaleDateString()}
)}
{task.estimated_hours && (
âąī¸ Est: {task.estimated_hours}h
)}
{task.status !== 'completed' && (
{quickReplies.map(reply => (
))}
)}
{showUpdates &&
}
);
}
// Task Updates Component
function TaskUpdates({ taskId }) {
const [updates, setUpdates] = useState([]);
const [loading, setLoading] = useState(true);
const [newComment, setNewComment] = useState('');
useEffect(() => {
loadUpdates();
}, [taskId]);
const loadUpdates = async () => {
try {
const data = await api.get(`/tasks/${taskId}/updates`);
setUpdates(data);
} catch (error) {
console.error('Error loading updates:', error);
} finally {
setLoading(false);
}
};
const handleAddComment = async (e) => {
e.preventDefault();
if (!newComment.trim()) return;
try {
await api.post(`/tasks/${taskId}/updates`, {
update_type: 'comment',
message: newComment,
});
setNewComment('');
loadUpdates();
} catch (error) {
console.error('Error adding comment:', error);
}
};
if (loading) {
return ;
}
return (
Activity Timeline
{updates.length === 0 ? (
No updates yet
) : (
{updates.map(update => (
{update.user_name}
{new Date(update.created_at).toLocaleString()}
{update.message}
{update.new_status && (
{update.new_status.replace('_', ' ').toUpperCase()}
)}
))}
)}
);
}
// Task Modal Component
function TaskModal({ task, projectId, onClose, onSave, onSaveFallback }) {
const [formData, setFormData] = useState({
title: task?.title || '',
description: task?.description || '',
priority: task?.priority || 'medium',
due_date: task?.due_date || '',
estimated_hours: task?.estimated_hours || '',
});
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
setError(null);
};
const handleSubmit = async (e) => {
e.preventDefault();
// Validation
if (!formData.title.trim()) {
setError('Task title is required');
return;
}
if (!projectId) {
setError('No project selected');
return;
}
setSaving(true);
setError(null);
console.log('[TaskModal] Submitting task:', formData, 'Project ID:', projectId);
try {
let result;
const isNew = !task;
if (task) {
console.log('[TaskModal] Updating task ID:', task.id);
result = await api.put(`/tasks/${task.id}`, formData);
} else {
console.log('[TaskModal] Creating new task for project:', projectId);
result = await api.post(`/projects/${projectId}/tasks`, formData);
}
console.log('[TaskModal] API Result:', result);
if (result.success) {
const savedTask = result.data;
console.log('[TaskModal] Success! Saved task:', savedTask);
// OPTION A (Preferred): Call onSave with the saved task data
if (savedTask && savedTask.id) {
onSave(savedTask, isNew);
} else {
// OPTION B (Fallback): If response doesn't include task data, re-fetch
console.warn('[TaskModal] No task data in response, using fallback');
onSaveFallback();
}
} else {
// Handle API errors
console.error('[TaskModal] API Error:', result.error);
setError(result.error || 'Failed to save task. Please try again.');
if (result.status === 403) {
setError('Permission denied. You may not have access to create/edit tasks.');
} else if (result.status === 401) {
setError('Authentication failed. Please refresh the page and try again.');
} else if (result.status >= 500) {
setError('Server error occurred. Please try again later.');
}
}
} catch (error) {
console.error('[TaskModal] Unexpected error:', error);
setError('An unexpected error occurred. Please check console for details.');
} finally {
setSaving(false);
}
};
return (
>
}
>
);
}
// Assignments Component - Replaces Projects
function Assignments() {
const [assignments, setAssignments] = useState([]);
const [binItems, setBinItems] = useState([]);
const [fieldConfig, setFieldConfig] = useState([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [selectedAssignment, setSelectedAssignment] = useState(null);
const [showDetailView, setShowDetailView] = useState(false);
const [toast, setToast] = useState(null);
const [showBin, setShowBin] = useState(false);
useEffect(() => {
loadFieldConfig();
loadAssignments();
loadBinItems();
}, []);
const loadFieldConfig = async () => {
console.log('[Assignments] Loading field configuration...');
try {
const result = await api.get('/assignments/fields');
if (result.success) {
console.log('[Assignments] Field config loaded:', result.data);
setFieldConfig(result.data);
} else {
console.error('[Assignments] Failed to load field config:', result.error);
}
} catch (error) {
console.error('[Assignments] Error loading field config:', error);
}
};
const loadAssignments = async () => {
console.log('[Assignments] Loading assignments list...');
try {
const result = await api.get('/assignments');
console.log('[Assignments] payload', result);
if (result.success) {
const rawData = result.data;
console.log('[Assignments] Raw response data:', rawData, 'Type:', Array.isArray(rawData) ? 'Array' : typeof rawData);
// STEP 2: Normalize result using helper before setState
const normalizedAssignments = normalizeToArray(rawData);
console.log('[Assignments] Normalized to array, length:', normalizedAssignments.length);
setAssignments(normalizedAssignments);
} else {
console.error('[Assignments] Failed to load assignments:', result.error);
setToast({ type: 'error', message: result.error || 'Failed to load assignments' });
setAssignments([]);
}
} catch (error) {
console.error('[Assignments] Error loading assignments:', error);
setToast({ type: 'error', message: 'Network error loading assignments' });
setAssignments([]);
} finally {
setLoading(false);
}
};
const loadBinItems = async () => {
console.log('[Assignments] Loading bin items...');
try {
const result = await api.get('/assignments/bin');
console.log('[Assignments] Bin payload', result);
if (result.success) {
const normalizedBin = normalizeToArray(result.data);
console.log('[Assignments] Normalized bin items, length:', normalizedBin.length);
setBinItems(normalizedBin);
} else {
console.error('[Assignments] Failed to load bin items:', result.error);
setBinItems([]);
}
} catch (error) {
console.error('[Assignments] Error loading bin items:', error);
setBinItems([]);
}
};
const createAssignment = () => {
setSelectedAssignment(null);
setShowModal(true);
};
const handleAssignmentSaved = (savedAssignment, isNew) => {
console.log('[Assignments] Assignment saved:', savedAssignment, 'isNew:', isNew);
if (isNew) {
setAssignments(prevAssignments => [savedAssignment, ...prevAssignments]);
setToast({ type: 'success', message: 'â
Assignment created successfully!' });
} else {
setAssignments(prevAssignments =>
prevAssignments.map(a => a.id === savedAssignment.id ? savedAssignment : a)
);
setToast({ type: 'success', message: 'â
Assignment updated successfully!' });
}
setShowModal(false);
};
const handleMoveToBin = async (assignmentId) => {
if (!confirm('Move this assignment to bin? It will be auto-deleted after 30 days.')) return;
try {
const result = await api.post(`/assignments/${assignmentId}/bin`);
if (result.success) {
setAssignments(prev => prev.filter(a => a.id !== assignmentId));
await loadBinItems();
setToast({ type: 'success', message: 'â
Assignment moved to bin' });
} else {
setToast({ type: 'error', message: result.error || 'Failed to move to bin' });
}
} catch (error) {
setToast({ type: 'error', message: 'Network error' });
}
};
const handleRestore = async (assignmentId) => {
try {
const result = await api.post(`/assignments/${assignmentId}/restore`);
if (result.success) {
setAssignments(prev => [result.data, ...prev]);
setBinItems(prev => prev.filter(item => item.id !== assignmentId));
setToast({ type: 'success', message: 'â
Assignment restored' });
} else {
setToast({ type: 'error', message: result.error || 'Failed to restore' });
}
} catch (error) {
setToast({ type: 'error', message: 'Network error' });
}
};
const handleDeletePermanent = async (assignmentId) => {
if (!confirm('Permanently delete this assignment? This action cannot be undone!')) return;
try {
const result = await api.delete(`/assignments/${assignmentId}/delete-permanent`);
if (result.success) {
setBinItems(prev => prev.filter(item => item.id !== assignmentId));
setToast({ type: 'success', message: 'â
Assignment permanently deleted' });
} else {
setToast({ type: 'error', message: result.error || 'Failed to delete' });
}
} catch (error) {
setToast({ type: 'error', message: 'Network error' });
}
};
// Handle assignment selection for detail view
const handleSelectAssignment = (assignment) => {
console.log('[Assignments] Assignment selected:', assignment);
setSelectedAssignment(assignment);
setShowDetailView(true);
};
// Handle edit from detail view
const handleEditAssignment = () => {
setShowDetailView(false);
setShowModal(true);
};
// Handle back from detail view
const handleBackToList = () => {
setShowDetailView(false);
setSelectedAssignment(null);
};
if (loading) {
return ;
}
// Show detail view if assignment selected
if (showDetailView && selectedAssignment) {
return (
);
}
// STEP 1 & 4: Normalize and guard assignments before rendering
const assignmentsRaw = assignments;
const assignmentsList = normalizeToArray(assignmentsRaw);
console.log('[Assignments] render', assignmentsList);
return (
My Assignments
{showBin && (
đī¸ Bin - Items Auto-Delete After 30 Days
{!Array.isArray(binItems) ? (
â ī¸ Bin data error (not an array)
) : binItems.length === 0 ? (
) : (
{binItems.map(item => )}
)}
)}
{assignmentsList.length === 0 ? (
đ
No assignments yet
Create your first assignment to get started
) : (
{assignmentsList.map(assignment => (
))}
)}
{showModal &&
setShowModal(false)} onSave={handleAssignmentSaved} />}
{toast && setToast(null)} />}
);
}
// Assignment Detail View Component
function AssignmentDetailView({ assignment, onBack, onEdit, onMoveToBin, toast, setToast }) {
const [loading, setLoading] = useState(false);
const [assignmentData, setAssignmentData] = useState(assignment);
const [error, setError] = useState(null);
const [deletedNotice, setDeletedNotice] = useState(false);
console.log('[AssignmentDetailView] Rendering assignment:', assignmentData);
// STEP 2: Defensive fetch with error handling and deleted_at check
const loadAssignment = async (id) => {
console.log('[AssignmentDetailView] >>> load', id);
setLoading(true);
setError(null);
setDeletedNotice(false);
try {
const result = await api.get(`/assignments/${id}`);
console.log('[AssignmentDetailView] >>> response', result);
if (!result || !result.success) {
console.error('[AssignmentDetailView] >>> Invalid data:', result);
setError(result?.error || 'Invalid data received from server');
return;
}
const data = result.data;
// Check if assignment was deleted (soft-delete)
if (data.deleted_at && data.deleted_at !== '0000-00-00 00:00:00') {
console.warn('[AssignmentDetailView] >>> Assignment is in bin (deleted_at:', data.deleted_at, ')');
setDeletedNotice(true);
return;
}
// Check status
if (data.status === 'in_bin' || data.status === 'deleted') {
console.warn('[AssignmentDetailView] >>> Assignment status indicates deletion:', data.status);
setDeletedNotice(true);
return;
}
console.log('[AssignmentDetailView] >>> Setting assignment data:', data);
setAssignmentData(data);
} catch (error) {
console.error('[AssignmentDetailView] >>> load error', error);
setError('Unable to fetch assignment: ' + error.message);
} finally {
setLoading(false);
}
};
// Reload assignment data on mount
useEffect(() => {
if (assignment && assignment.id) {
loadAssignment(assignment.id);
}
}, [assignment?.id]);
const getCategoryIcon = (category) => ({
'Carpenter': 'đ¨', 'Electrician': 'âĄ', 'Plumber': 'đ§', 'Painter': 'đ¨', 'Supervisor': 'đˇ', 'Other': 'đ'
}[category] || 'đ');
// STEP 4: Error boundary / fallback UI
if (error) {
return (
â ī¸
Something went wrong
{error}
Technical Details
{JSON.stringify({ assignment, error }, null, 2)}
);
}
// Show deleted notice if assignment is in bin
if (deletedNotice) {
return (
đī¸
Assignment Moved to Bin
This assignment has been moved to the bin and will be auto-deleted after 30 days.
);
}
if (loading) {
return (
Loading assignment details...
);
}
return (
{/* Header with back button */}
{/* Assignment Details Card */}
{getCategoryIcon(assignmentData.category)}
{assignmentData.title}
{assignmentData.status}
{/* Info Grid */}
{assignmentData.category}
{assignmentData.assignee_name && (
{assignmentData.assignee_name}
)}
{new Date(assignmentData.created_at).toLocaleDateString()}
{new Date(assignmentData.updated_at).toLocaleDateString()}
{/* Description */}
{assignmentData.description && (
Description
{assignmentData.description}
)}
{/* Custom Fields */}
{assignmentData.custom_fields && Object.keys(assignmentData.custom_fields).length > 0 && (
Additional Information
{Object.entries(assignmentData.custom_fields).map(([key, value]) => (
{value}
))}
)}
{toast &&
setToast(null)} />}
);
}
function AssignmentCard({ assignment, onSelect, onMoveToBin }) {
const getCategoryIcon = (category) => ({
'Carpenter': 'đ¨', 'Electrician': 'âĄ', 'Plumber': 'đ§', 'Painter': 'đ¨', 'Supervisor': 'đˇ', 'Other': 'đ'
}[category] || 'đ');
// STEP 3: Prevent accidental move-to-bin on card click - card opens detail view ONLY
const handleCardClick = (e) => {
// Ignore clicks on buttons or interactive elements
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) {
return; // Let button handle its own click
}
console.log('[AssignmentCard] Clicked card ID:', assignment.id, '- Opening detail view');
onSelect(assignment); // Only open detail view
};
// STEP 3: Explicit move-to-bin handler with event stop propagation
const handleMoveToBin = (e) => {
e.stopPropagation(); // CRITICAL: Prevent card click event from firing
console.log('[AssignmentCard] Move to Bin clicked for ID:', assignment.id);
onMoveToBin(assignment.id);
};
return (
{getCategoryIcon(assignment.category)} {assignment.title}
{assignment.status}
Category: {assignment.category}
{assignment.assignee_name &&
Assigned to: {assignment.assignee_name}
}
{assignment.description || 'No description'}
{assignment.custom_fields && Object.keys(assignment.custom_fields).length > 0 && (
{Object.entries(assignment.custom_fields).map(([key, value]) =>
{key}: {value}
)}
)}
Created: {new Date(assignment.created_at).toLocaleDateString()}
);
}
function BinItemCard({ item, onRestore, onDeletePermanent }) {
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
return (
{item.title}
{item.days_remaining > 0 ? `${item.days_remaining} days left` : 'Expires today!'}
Category: {item.category}
{item.assignee_name &&
Assigned to: {item.assignee_name}
}
Deleted: {new Date(item.deleted_at).toLocaleDateString()}
{!showDeleteConfirm ? (
) : (
)}
);
}
function AssignmentModal({ assignment, fieldConfig, onClose, onSave }) {
const [formData, setFormData] = useState({});
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
// Validate fieldConfig is an array
if (!Array.isArray(fieldConfig)) {
console.error('[AssignmentModal] â Invalid fieldConfig - expected array, got:', typeof fieldConfig, fieldConfig);
return (
e.stopPropagation()}>
â ī¸ Configuration Error
Field configuration error:
The assignment form configuration is invalid (not an array).
Type received: {typeof fieldConfig}
Please reload the page or contact the administrator.
);
}
// Check for empty field config
if (fieldConfig.length === 0) {
console.warn('[AssignmentModal] â ī¸ Empty fieldConfig array');
return (
e.stopPropagation()}>
Loading Configuration...
Configuration loading...
The assignment form fields are still loading.
Please wait a moment and try again.
);
}
useEffect(() => {
const initialData = {};
// Safety check: ensure fieldConfig is an array
if (Array.isArray(fieldConfig)) {
fieldConfig.forEach(field => {
if (assignment) {
if (field.is_system) {
initialData[field.key] = assignment[field.key] || field.default_value || '';
} else if (assignment.custom_fields && assignment.custom_fields[field.key]) {
initialData[field.key] = assignment.custom_fields[field.key];
} else {
initialData[field.key] = field.default_value || '';
}
} else {
initialData[field.key] = field.default_value || '';
}
});
}
setFormData(initialData);
}, [assignment, fieldConfig]);
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
setError(null);
};
const handleSubmit = async (e) => {
e.preventDefault();
const errors = [];
// Safety check: ensure fieldConfig is an array before validation
if (Array.isArray(fieldConfig)) {
fieldConfig.forEach(field => {
if (field.required && field.visible && (!formData[field.key] || formData[field.key].trim() === '')) {
errors.push(`${field.label} is required`);
}
});
}
if (errors.length > 0) { setError(errors.join(', ')); return; }
setSaving(true); setError(null);
try {
const isNew = !assignment;
const result = isNew ? await api.post('/assignments', formData) : await api.put(`/assignments/${assignment.id}`, formData);
if (result.success) { onSave(result.data, isNew); } else { setError(result.error || 'Failed to save assignment'); setSaving(false); }
} catch (error) {
setError('Network error. Please try again.'); setSaving(false);
}
};
const renderField = (field) => {
if (!field.visible) return null;
const value = formData[field.key] || '';
switch (field.type) {
case 'text': return ;
case 'textarea': return ;
case 'select': return (
);
case 'number': return ;
case 'date': return ;
default: return ;
}
};
return (
e.stopPropagation()}>
{assignment ? 'Edit Assignment' : 'New Assignment'}
{error &&
{error}
}
);
}
// Main App Component
function App() {
const [activeTab, setActiveTab] = useState('dashboard');
const [useV2, setUseV2] = useState(true); // Toggle between V1 and V2
const currentUser = IWM_DATA.currentUser;
const renderContent = () => {
switch(activeTab) {
case 'dashboard':
return ;
case 'assignments':
// Use V2 by default, fallback to V1 if needed
return useV2 && window.WorkAssignmentsV2 ? : ;
case 'tasks':
return ;
default:
return ;
}
};
return (
{renderContent()}
);
}
// Render App
const root = ReactDOM.createRoot(document.getElementById('iwm-app'));
root.render(
);