Some checks failed
scrum-manager/pipeline/head There was a failure building this commit
Database fixes: - Add hostPath.type=DirectoryOrCreate so kubelet auto-creates /mnt/data/mysql - Add fsGroup=999 so MySQL process can write to the hostPath volume - Add MYSQL_ROOT_HOST=% to allow backend pods to authenticate as root - Fix liveness/readiness probes to include credentials (-p$MYSQL_ROOT_PASSWORD) - Increase probe initialDelaySeconds (30/60s) for slow first-run init - Add 15s grace sleep in backend initContainer after MySQL TCP is up - Add persistentVolumeReclaimPolicy=Retain to prevent accidental data loss - Explicit accessModes+resources in PVC patch to avoid list merge ambiguity - Add nodeAffinity comment in PV for multi-node cluster guidance Ingress/nginx fixes: - Remove broken rewrite-target=/ that was rewriting all paths (incl /api) to / - Route /socket.io directly to backend for WebSocket support - Add /socket.io/ proxy location to both nginx.conf and K8s ConfigMap Frontend fix: - Persist currentUser to localStorage on login so page refresh no longer clears session and redirects users back to the login page Tooling: - Add k8s/overlays/on-premise/deploy.sh for one-command deployment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
304 lines
14 KiB
TypeScript
304 lines
14 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { apiFetchTasks, apiFetchUsers, apiCreateTask, apiUpdateTask, apiAddActivity, apiAddDependency, apiToggleDependency, apiRemoveDependency, apiCreateUser, apiDeleteUser } from './api';
|
|
import type { Task, User, Status } from './data';
|
|
import { STATUS_LABELS } from './data';
|
|
import { LoginPage } from './Login';
|
|
import { Sidebar } from './Sidebar';
|
|
import { TopNavbar, BottomToggleBar } from './NavBars';
|
|
import { CalendarView, QuickAddPanel } from './Calendar';
|
|
import { KanbanBoard } from './Kanban';
|
|
import { ListView } from './ListView';
|
|
import { TaskDrawer, AddTaskModal } from './TaskDrawer';
|
|
import { DashboardPage } from './Dashboard';
|
|
import { TeamTasksPage, MembersPage } from './Pages';
|
|
import { ReportsPage } from './Reports';
|
|
import { NotificationProvider } from './NotificationContext';
|
|
import './index.css';
|
|
|
|
const PAGE_TITLES: Record<string, string> = {
|
|
dashboard: 'Dashboard', calendar: 'Calendar', kanban: 'Kanban Board',
|
|
mytasks: 'My Tasks', teamtasks: 'Team Tasks', reports: 'Reports', members: 'Members',
|
|
list: 'List View',
|
|
};
|
|
|
|
const VIEW_PAGES = ['calendar', 'kanban', 'list'];
|
|
|
|
export default function App() {
|
|
const now = new Date();
|
|
const [currentUser, setCurrentUser] = useState<User | null>(() => {
|
|
try { const s = localStorage.getItem('currentUser'); return s ? JSON.parse(s) : null; }
|
|
catch { return null; }
|
|
});
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
const [activePage, setActivePage] = useState('calendar');
|
|
const [activeView, setActiveView] = useState('calendar');
|
|
const [activeTask, setActiveTask] = useState<Task | null>(null);
|
|
const [showAddModal, setShowAddModal] = useState(false);
|
|
const [addModalDefaults, setAddModalDefaults] = useState<{ date?: string; status?: Status }>({});
|
|
const [calMonth, setCalMonth] = useState({ year: now.getFullYear(), month: now.getMonth() });
|
|
const [calView, setCalView] = useState('month');
|
|
const [filterUser, setFilterUser] = useState<string | null>(null);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
const [quickAddDay, setQuickAddDay] = useState<{ date: string; rect: { top: number; left: number } } | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// Load data from API when user logs in
|
|
useEffect(() => {
|
|
if (!currentUser) return;
|
|
setLoading(true);
|
|
Promise.all([apiFetchTasks(), apiFetchUsers()])
|
|
.then(([fetchedTasks, fetchedUsers]) => {
|
|
setTasks(fetchedTasks);
|
|
setUsers(fetchedUsers);
|
|
})
|
|
.catch(err => {
|
|
console.error('Failed to load data, using empty state:', err);
|
|
setTasks([]); // Start empty if backend fails
|
|
setUsers([currentUser]);
|
|
})
|
|
.finally(() => setLoading(false));
|
|
}, [currentUser]);
|
|
|
|
if (!currentUser) return <LoginPage onLogin={u => { localStorage.setItem('currentUser', JSON.stringify(u)); setCurrentUser(u); setActivePage('calendar'); setActiveView('calendar'); }} />;
|
|
|
|
const handleNavigate = (page: string) => {
|
|
setActivePage(page);
|
|
if (VIEW_PAGES.includes(page)) setActiveView(page);
|
|
setSidebarOpen(false);
|
|
};
|
|
|
|
const handleViewChange = (view: string) => {
|
|
setActiveView(view);
|
|
if (VIEW_PAGES.includes(view)) setActivePage(view);
|
|
};
|
|
|
|
const handleTaskClick = (t: Task) => setActiveTask(t);
|
|
|
|
const handleDayClick = (date: string, el: HTMLElement) => {
|
|
const rect = el.getBoundingClientRect();
|
|
setQuickAddDay({ date, rect: { top: rect.bottom, left: rect.left } });
|
|
};
|
|
|
|
const handleQuickAdd = async (partial: Partial<Task>) => {
|
|
const tempId = `t${Date.now()}`;
|
|
const newTask: Task = {
|
|
id: tempId,
|
|
title: partial.title || '',
|
|
description: partial.description || '',
|
|
status: partial.status || 'todo',
|
|
priority: partial.priority || 'medium',
|
|
assignee: partial.assignee || currentUser.id,
|
|
reporter: currentUser.id,
|
|
dueDate: partial.dueDate || '',
|
|
tags: partial.tags || [],
|
|
subtasks: [], comments: [], activity: [], dependencies: []
|
|
};
|
|
setTasks(prev => [...prev, newTask]);
|
|
setQuickAddDay(null);
|
|
|
|
try {
|
|
const created = await apiCreateTask({
|
|
title: newTask.title,
|
|
description: newTask.description,
|
|
status: newTask.status,
|
|
priority: newTask.priority,
|
|
assignee: newTask.assignee,
|
|
reporter: newTask.reporter,
|
|
dueDate: newTask.dueDate,
|
|
tags: newTask.tags,
|
|
});
|
|
setTasks(prev => prev.map(t => t.id === tempId ? created : t));
|
|
} catch (err) {
|
|
console.error('Failed to quick-add task:', err);
|
|
}
|
|
};
|
|
|
|
const handleAddTask = async (task: Task) => {
|
|
const tempId = `t${Date.now()}`;
|
|
const newTask = { ...task, id: tempId };
|
|
setTasks(prev => [...prev, newTask]);
|
|
|
|
try {
|
|
const created = await apiCreateTask({
|
|
title: task.title,
|
|
description: task.description,
|
|
status: task.status,
|
|
priority: task.priority,
|
|
assignee: task.assignee,
|
|
reporter: currentUser.id,
|
|
dueDate: task.dueDate,
|
|
tags: task.tags,
|
|
dependencies: (task.dependencies || []).map(d => ({ dependsOnUserId: d.dependsOnUserId, description: d.description })),
|
|
});
|
|
setTasks(prev => prev.map(t => t.id === tempId ? created : t));
|
|
} catch (err) {
|
|
console.error('Failed to add task:', err);
|
|
}
|
|
};
|
|
|
|
const handleUpdateTask = async (updated: Task) => {
|
|
// Optimistic update
|
|
setTasks(prev => prev.map(t => t.id === updated.id ? updated : t));
|
|
setActiveTask(updated);
|
|
|
|
try {
|
|
const result = await apiUpdateTask(updated.id, {
|
|
title: updated.title,
|
|
description: updated.description,
|
|
status: updated.status,
|
|
priority: updated.priority,
|
|
assignee: updated.assignee,
|
|
reporter: updated.reporter,
|
|
dueDate: updated.dueDate,
|
|
tags: updated.tags,
|
|
subtasks: updated.subtasks, // Ensure subtasks are sent if API supports it (it usually does via full update or we need to check apiUpdateTask)
|
|
});
|
|
// Verification: if result is successful, update state with server result (which might have new IDs etc)
|
|
setTasks(prev => prev.map(t => t.id === result.id ? result : t));
|
|
if (activeTask?.id === result.id) setActiveTask(result);
|
|
} catch (err) {
|
|
console.error('Failed to update task:', err);
|
|
// We might want to revert here, but for now let's keep the optimistic state to resolve the "useless" UI issue visually
|
|
}
|
|
};
|
|
|
|
const handleNewTask = () => { setAddModalDefaults({}); setShowAddModal(true); };
|
|
const handleKanbanAdd = (status: Status) => { setAddModalDefaults({ status }); setShowAddModal(true); };
|
|
const handleToggleDone = async (taskId: string) => {
|
|
const task = tasks.find(t => t.id === taskId);
|
|
if (!task) return;
|
|
const newStatus = task.status === 'done' ? 'todo' : 'done';
|
|
try {
|
|
const result = await apiUpdateTask(taskId, { status: newStatus });
|
|
await apiAddActivity(taskId, `🔄 ${currentUser.name} changed status to ${newStatus}`);
|
|
setTasks(prev => prev.map(t => t.id === taskId ? result : t));
|
|
} catch (err) {
|
|
console.error('Failed to toggle done:', err);
|
|
}
|
|
};
|
|
|
|
const handleMoveTask = async (taskId: string, newStatus: Status) => {
|
|
const task = tasks.find(t => t.id === taskId);
|
|
if (!task || task.status === newStatus) return;
|
|
// Optimistic update
|
|
setTasks(prev => prev.map(t => t.id === taskId ? { ...t, status: newStatus } : t));
|
|
try {
|
|
const result = await apiUpdateTask(taskId, { status: newStatus });
|
|
await apiAddActivity(taskId, `🔄 ${currentUser.name} moved task to ${STATUS_LABELS[newStatus]}`);
|
|
setTasks(prev => prev.map(t => t.id === taskId ? result : t));
|
|
} catch (err) {
|
|
console.error('Failed to move task:', err);
|
|
// Revert on failure
|
|
setTasks(prev => prev.map(t => t.id === taskId ? task : t));
|
|
}
|
|
};
|
|
|
|
const handleAddDep = async (taskId: string, dep: { dependsOnUserId: string; description: string }) => {
|
|
try {
|
|
const newDep = await apiAddDependency(taskId, dep);
|
|
setTasks(prev => prev.map(t => t.id === taskId ? { ...t, dependencies: [...(t.dependencies || []), newDep] } : t));
|
|
if (activeTask?.id === taskId) setActiveTask(prev => prev ? { ...prev, dependencies: [...(prev.dependencies || []), newDep] } : prev);
|
|
} catch (err) { console.error('Failed to add dependency:', err); }
|
|
};
|
|
|
|
const handleToggleDep = async (taskId: string, depId: string, resolved: boolean) => {
|
|
try {
|
|
await apiToggleDependency(taskId, depId, resolved);
|
|
const updateDeps = (deps: any[]) => deps.map((d: any) => d.id === depId ? { ...d, resolved } : d);
|
|
setTasks(prev => prev.map(t => t.id === taskId ? { ...t, dependencies: updateDeps(t.dependencies || []) } : t));
|
|
if (activeTask?.id === taskId) setActiveTask(prev => prev ? { ...prev, dependencies: updateDeps(prev.dependencies || []) } : prev);
|
|
} catch (err) { console.error('Failed to toggle dependency:', err); }
|
|
};
|
|
|
|
const handleRemoveDep = async (taskId: string, depId: string) => {
|
|
try {
|
|
await apiRemoveDependency(taskId, depId);
|
|
setTasks(prev => prev.map(t => t.id === taskId ? { ...t, dependencies: (t.dependencies || []).filter((d: any) => d.id !== depId) } : t));
|
|
if (activeTask?.id === taskId) setActiveTask(prev => prev ? { ...prev, dependencies: (prev.dependencies || []).filter((d: any) => d.id !== depId) } : prev);
|
|
} catch (err) { console.error('Failed to remove dependency:', err); }
|
|
};
|
|
|
|
const handleAddUser = async (data: { name: string; email: string; password: string; role: string; dept: string }) => {
|
|
const newUser = await apiCreateUser(data);
|
|
setUsers(prev => [...prev, newUser]);
|
|
};
|
|
|
|
const handleDeleteUser = async (id: string) => {
|
|
await apiDeleteUser(id);
|
|
setUsers(prev => prev.filter(u => u.id !== id));
|
|
// Unassign tasks locally too
|
|
setTasks(prev => prev.map(t => t.assignee === id ? { ...t, assignee: '' } : t).map(t => t.reporter === id ? { ...t, reporter: '' } : t));
|
|
};
|
|
|
|
const displayPage = VIEW_PAGES.includes(activePage) ? activeView : activePage;
|
|
const filteredMyTasks = tasks.filter(t => t.assignee === currentUser.id);
|
|
|
|
const pageTitle = PAGE_TITLES[displayPage] || 'Calendar';
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="app-shell" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
|
|
<p style={{ color: '#818cf8', fontSize: 18 }}>Loading...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<NotificationProvider userId={currentUser.id}>
|
|
<div className="app-shell">
|
|
<TopNavbar title={pageTitle} filterUser={filterUser} onFilterChange={setFilterUser}
|
|
searchQuery={searchQuery} onSearch={setSearchQuery} onNewTask={handleNewTask}
|
|
onOpenSidebar={() => setSidebarOpen(true)} users={users} />
|
|
<div className="app-body">
|
|
<Sidebar currentUser={currentUser} activePage={activePage} onNavigate={handleNavigate}
|
|
onSignOut={() => { localStorage.removeItem('currentUser'); setCurrentUser(null); setActivePage('calendar'); setActiveView('calendar'); setSidebarOpen(false); }}
|
|
isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} users={users} />
|
|
<div className="main-content">
|
|
{displayPage === 'calendar' && (
|
|
<CalendarView tasks={tasks} currentUser={currentUser} calMonth={calMonth} calView={calView}
|
|
onMonthChange={setCalMonth} onViewChange={setCalView} onTaskClick={handleTaskClick}
|
|
onDayClick={handleDayClick} filterUser={filterUser} searchQuery={searchQuery} users={users} />
|
|
)}
|
|
{displayPage === 'kanban' && (
|
|
<KanbanBoard tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
|
onAddTask={handleKanbanAdd} onMoveTask={handleMoveTask} filterUser={filterUser} searchQuery={searchQuery} users={users} />
|
|
)}
|
|
{displayPage === 'list' && (
|
|
<ListView tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
|
filterUser={filterUser} searchQuery={searchQuery} onToggleDone={handleToggleDone} users={users} />
|
|
)}
|
|
{displayPage === 'dashboard' && <DashboardPage tasks={tasks} currentUser={currentUser} users={users} />}
|
|
{displayPage === 'mytasks' && (
|
|
<ListView tasks={filteredMyTasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
|
filterUser={null} searchQuery={searchQuery} onToggleDone={handleToggleDone} users={users} />
|
|
)}
|
|
{displayPage === 'teamtasks' && <TeamTasksPage tasks={tasks} currentUser={currentUser} users={users} />}
|
|
{displayPage === 'reports' && <ReportsPage tasks={tasks} users={users} currentUser={currentUser} />}
|
|
{displayPage === 'members' && <MembersPage tasks={tasks} users={users} currentUser={currentUser} onAddUser={handleAddUser} onDeleteUser={handleDeleteUser} />}
|
|
</div>
|
|
</div>
|
|
|
|
{VIEW_PAGES.includes(activePage) && (
|
|
<BottomToggleBar activeView={activeView} onViewChange={handleViewChange} />
|
|
)}
|
|
|
|
{activeTask && <TaskDrawer task={activeTask} currentUser={currentUser} onClose={() => setActiveTask(null)} onUpdate={handleUpdateTask} onAddDependency={handleAddDep} onToggleDependency={handleToggleDep} onRemoveDependency={handleRemoveDep} users={users} />}
|
|
{showAddModal && <AddTaskModal onClose={() => setShowAddModal(false)} onAdd={handleAddTask} defaultDate={addModalDefaults.date} defaultStatus={addModalDefaults.status} users={users} currentUser={currentUser} />}
|
|
|
|
{quickAddDay && (
|
|
<div style={{ position: 'fixed', inset: 0, zIndex: 199 }} onClick={() => setQuickAddDay(null)}>
|
|
<div style={{ position: 'absolute', top: Math.min(quickAddDay.rect.top, window.innerHeight - 280), left: Math.min(quickAddDay.rect.left, window.innerWidth - 340) }}
|
|
onClick={e => e.stopPropagation()}>
|
|
<QuickAddPanel date={quickAddDay.date} onAdd={handleQuickAdd}
|
|
onOpenFull={() => { setAddModalDefaults({ date: quickAddDay.date }); setShowAddModal(true); setQuickAddDay(null); }}
|
|
onClose={() => setQuickAddDay(null)} users={users} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</NotificationProvider>
|
|
);
|
|
}
|