diff --git a/README.md b/README.md index adb5d9f..c038050 100644 --- a/README.md +++ b/README.md @@ -47,18 +47,31 @@ cp apps/web/env.example apps/web/.env.local # Start the web application pnpm dev:web +# Start the Nest API (fixture-backed common data; default :3001) +pnpm --filter @worksight/common build +pnpm dev:api + # Start the documentation site pnpm dev:docs -# Start both applications +# Start both web + docs (turbo) pnpm dev + +# MVP E2E demo: API + web with shared common fixtures (see docs/mvp/DEMO.md) +pnpm demo ``` Open: - **Web App**: +- **E2E demo page**: (requires API on :3001) +- **API**: (Swagger at `/api`) - **Documentation**: +> Demo data is **fixture-backed** from `@worksight/common` β€” not Supabase. Set +> `NEXT_PUBLIC_USE_API=true` and `NEXT_PUBLIC_API_URL=http://localhost:3001` so +> dashboard/admin pages call Nest instead of in-process fixtures. + ## πŸ“¦ Available Scripts ### Root Scripts diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index f6b776d..a8d9a04 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,12 +1,23 @@ -import { ClassSerializerInterceptor } from '@nestjs/common'; +import { ClassSerializerInterceptor, Logger } from '@nestjs/common'; import { NestFactory, Reflector } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + const logger = new Logger('Bootstrap'); + const port = Number(process.env.PORT ?? 3001); + const corsOrigins = (process.env.CORS_ORIGINS ?? 'http://localhost:3000') + .split(',') + .map(origin => origin.trim()) + .filter(Boolean); + + app.enableCors({ + origin: corsOrigins, + methods: ['GET', 'HEAD', 'OPTIONS'], + }); app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); - await app.listen(process.env.PORT ?? 3000); + const config = new DocumentBuilder() .setTitle('WorkSight') .setDescription('Check your tasks, manage your well-being') @@ -15,5 +26,9 @@ async function bootstrap() { .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); + + await app.listen(port); + logger.log(`API listening on http://localhost:${port} (CORS: ${corsOrigins.join(', ')})`); + logger.log('Data is fixture-backed from @worksight/common β€” not Supabase.'); } bootstrap(); diff --git a/apps/web/.env.example b/apps/web/.env.example index 9b78e34..c3c82cd 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -29,6 +29,11 @@ NEXT_PUBLIC_IS_OFFLINE=false # Server-side offline flag (for server components) IS_OFFLINE=false +# MVP data source (#18): when true, web fetches Nest API instead of in-process fixtures +# /demo always hits the API regardless of this flag +NEXT_PUBLIC_USE_API=false +NEXT_PUBLIC_API_URL=http://localhost:3001 + # App URL for metadata and OpenGraph NEXT_PUBLIC_APP_URL=http://localhost:3000 @@ -55,7 +60,9 @@ NEXT_PUBLIC_ENABLE_ERROR_REPORTING="false" NEXT_PUBLIC_ENABLE_PERFORMANCE_MONITORING="true" # API Configuration -API_BASE_URL="http://localhost:3000/api" +# Nest API (fixture-backed @worksight/common). Web uses NEXT_PUBLIC_* below. +API_BASE_URL="http://localhost:3001" +NEXT_PUBLIC_API_URL="http://localhost:3001" API_TIMEOUT="10000" # Email Configuration (if needed) diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 435c5c1..71ab30e 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -59,12 +59,16 @@ const nextConfig: NextConfig = { }, }; } + const commonDist = path.resolve(__dirname, '../../packages/common/dist'); config.resolve.alias = { ...config.resolve.alias, '@': path.resolve(__dirname, 'src'), '@worksight/assets': path.resolve(__dirname, '../../packages/assets/dist'), - '@worksight/common': path.resolve(__dirname, '../../packages/common/dist') - } + '@worksight/common/data': path.resolve(commonDist, 'data'), + '@worksight/common/types': path.resolve(commonDist, 'types'), + '@worksight/common/utils': path.resolve(commonDist, 'utils'), + '@worksight/common': commonDist, + }; return config; }, diff --git a/apps/web/src/app/admin/surveys/page.tsx b/apps/web/src/app/admin/surveys/page.tsx index 7575478..9a2a728 100644 --- a/apps/web/src/app/admin/surveys/page.tsx +++ b/apps/web/src/app/admin/surveys/page.tsx @@ -41,103 +41,23 @@ import { } from 'lucide-react'; import Link from 'next/link'; import { useEffect, useMemo, useState } from 'react'; - -interface Survey { - id: string; - title: string; - description: string; - status: 'draft' | 'active' | 'paused' | 'completed'; - questionCount: number; - responseCount: number; - createdAt: string; - lastModified: string; - createdBy: string; - category: 'burnout' | 'satisfaction' | 'wellness' | 'feedback'; - targetAudience: 'all' | 'managers' | 'employees' | 'specific'; -} - -const mockSurveys: Survey[] = [ - { - id: '1', - title: 'Burnout Assessment 2025', - description: 'Comprehensive burnout evaluation for all employees', - status: 'active', - questionCount: 15, - responseCount: 247, - createdAt: '2025-01-15', - lastModified: '2025-01-20', - createdBy: 'Admin User', - category: 'burnout', - targetAudience: 'all', - }, - { - id: '2', - title: 'Job Satisfaction Survey', - description: 'Quarterly job satisfaction and engagement survey', - status: 'active', - questionCount: 12, - responseCount: 156, - createdAt: '2025-01-10', - lastModified: '2025-01-18', - createdBy: 'HR Manager', - category: 'satisfaction', - targetAudience: 'employees', - }, - { - id: '3', - title: 'Manager Feedback Survey', - description: 'Leadership effectiveness and team dynamics assessment', - status: 'draft', - questionCount: 8, - responseCount: 0, - createdAt: '2025-01-22', - lastModified: '2025-01-22', - createdBy: 'Admin User', - category: 'feedback', - targetAudience: 'managers', - }, - { - id: '4', - title: 'Wellness Check Q4 2024', - description: 'Mental health and wellness assessment', - status: 'completed', - questionCount: 10, - responseCount: 312, - createdAt: '2024-10-01', - lastModified: '2024-12-31', - createdBy: 'Wellness Team', - category: 'wellness', - targetAudience: 'all', - }, - { - id: '5', - title: 'Remote Work Experience', - description: 'Evaluation of remote work setup and productivity', - status: 'paused', - questionCount: 14, - responseCount: 89, - createdAt: '2025-01-05', - lastModified: '2025-01-19', - createdBy: 'Operations Lead', - category: 'feedback', - targetAudience: 'all', - }, -]; +import { getMvpSurveys, type MvpSurvey } from '@/lib/mvp-data'; function SurveyManagementContent() { const { logout } = useAuth(); - const [surveys, setSurveys] = useState([]); + const [surveys, setSurveys] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [categoryFilter, setCategoryFilter] = useState('all'); const [isLoading, setIsLoading] = useState(true); useEffect(() => { - // Simulate API call - setTimeout(() => { - setSurveys(mockSurveys); - setIsLoading(false); - }, 1000); + const fixtureSurveys = getMvpSurveys(); + if (fixtureSurveys.length === 0) { + throw new Error('Common survey fixtures empty; refusing silent empty fallback'); + } + setSurveys(fixtureSurveys); + setIsLoading(false); }, []); const handleLogout = async () => { diff --git a/apps/web/src/app/admin/users/page.tsx b/apps/web/src/app/admin/users/page.tsx index 453602b..1ceaba4 100644 --- a/apps/web/src/app/admin/users/page.tsx +++ b/apps/web/src/app/admin/users/page.tsx @@ -2,121 +2,55 @@ import { useAuth } from '@/auth'; import { getRoleColor, getUserRoleDisplay } from '@/auth/admin'; -import { UserRole } from '@/auth/types'; import { AdminRoute } from '@/components/admin'; import { SessionTimer } from '@/components/features'; import { AppSidebar } from '@/components/main/sidebar'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, } from '@/components/ui/breadcrumb'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; import { sections } from '@/data/sections'; import { - UserFilters, - UserWithMetrics, - validateUserArray, - validateUserFilters, + UserFilters, + UserWithMetrics, + validateUserArray, + validateUserFilters, } from '@/schemas/user'; +import { fetchUsersWithMetricsFromApi } from '@/lib/mvp-api-bridge'; +import { getUsersWithMetrics } from '@/lib/mvp-data'; +import { isApiDataMode } from '@/lib/worksight-api'; import { - AlertTriangle, - CheckCircle, - Clock, - Edit3, - LogOut, - MoreHorizontal, - Plus, - Search, - Shield, - Users, + AlertTriangle, + CheckCircle, + Clock, + Edit3, + LogOut, + MoreHorizontal, + Plus, + Search, + Shield, + Users, } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; -const mockUsers: UserWithMetrics[] = [ - { - id: '1', - name: 'John Doe', - email: 'john.doe@company.com', - role: UserRole.EMPLOYEE, - department: 'Engineering', - team: 'Frontend', - burnoutScore: 3.2, - lastActive: '2 hours ago', - surveyCompleted: true, - riskLevel: 'low', - tasksCompleted: 24, - }, - { - id: '2', - name: 'Jane Smith', - email: 'jane.smith@company.com', - role: UserRole.TEAM_LEAD, - department: 'Engineering', - team: 'Backend', - burnoutScore: 7.8, - lastActive: '30 minutes ago', - surveyCompleted: true, - riskLevel: 'high', - tasksCompleted: 31, - }, - { - id: '3', - name: 'Bob Wilson', - email: 'bob.wilson@company.com', - role: UserRole.MANAGER, - department: 'Product', - team: 'Design', - burnoutScore: 5.4, - lastActive: '1 day ago', - surveyCompleted: false, - riskLevel: 'medium', - tasksCompleted: 18, - }, - { - id: '4', - name: 'Alice Johnson', - email: 'alice.johnson@company.com', - role: UserRole.EMPLOYEE, - department: 'Marketing', - team: 'Content', - burnoutScore: 2.1, - lastActive: '5 minutes ago', - surveyCompleted: true, - riskLevel: 'low', - tasksCompleted: 42, - }, - { - id: '5', - name: 'Charlie Brown', - email: 'charlie.brown@company.com', - role: UserRole.ADMIN, - department: 'IT', - team: 'DevOps', - burnoutScore: 6.7, - lastActive: '1 hour ago', - surveyCompleted: true, - riskLevel: 'medium', - tasksCompleted: 15, - }, -]; - function UserManagementContent() { const { logout } = useAuth(); const [users, setUsers] = useState([]); @@ -125,25 +59,49 @@ function UserManagementContent() { const [riskFilter, setRiskFilter] = useState('all'); const [isLoading, setIsLoading] = useState(true); const [validationErrors, setValidationErrors] = useState([]); + const [dataSource, setDataSource] = useState<'api' | 'fixtures'>('fixtures'); useEffect(() => { - // Simulate API call with validation - setTimeout(() => { - // Validate mock data using Zod - const validationResult = validateUserArray(mockUsers); + let cancelled = false; + + const applyUsers = (rawUsers: UserWithMetrics[], source: 'api' | 'fixtures') => { + const validationResult = validateUserArray(rawUsers); if (!validationResult.allValid) { const errors = validationResult.invalid.map( - (item) => + item => `User at index ${item.index}: ${item.errors?.map((e: { message: string }) => e.message).join(', ')}` ); setValidationErrors(errors); } - // Use only valid users - setUsers(validationResult.valid.map((item) => item.data!)); - setIsLoading(false); - }, 1000); + if (validationResult.valid.length === 0 && rawUsers.length > 0) { + throw new Error('Employee payloads failed validation; refusing empty fallback'); + } + + if (!cancelled) { + setDataSource(source); + setUsers(validationResult.valid.map(item => item.data!)); + setIsLoading(false); + } + }; + + (async () => { + if (isApiDataMode()) { + try { + const apiUsers = await fetchUsersWithMetricsFromApi(); + applyUsers(apiUsers, 'api'); + return; + } catch (err) { + console.warn('API user load failed; falling back to fixtures', err); + } + } + applyUsers(getUsersWithMetrics(), 'fixtures'); + })(); + + return () => { + cancelled = true; + }; }, []); // Validate filters when they change @@ -179,7 +137,7 @@ function UserManagementContent() { }; const filteredUsers = useMemo(() => { - return users.filter((user) => { + return users.filter(user => { const matchesSearch = user.name?.toLowerCase().includes(searchTerm.toLowerCase()) || user.email?.toLowerCase().includes(searchTerm.toLowerCase()) || @@ -193,7 +151,7 @@ function UserManagementContent() { }, [users, searchTerm, departmentFilter, riskFilter]); const departments = useMemo(() => { - const depts = Array.from(new Set(users.map((u) => u.department))); + const depts = Array.from(new Set(users.map(u => u.department))); return depts.sort(); }, [users]); @@ -244,7 +202,12 @@ function UserManagementContent() {
-

User Management

+
+

User Management

+ + data: {dataSource === 'api' ? 'Nest API' : 'common fixtures'} + +

Manage user accounts, roles, and monitor burnout metrics @@ -296,7 +259,7 @@ function UserManagementContent() {

- {users.filter((u) => u.riskLevel === 'high').length} + {users.filter(u => u.riskLevel === 'high').length}
@@ -308,7 +271,7 @@ function UserManagementContent() {
- {users.filter((u) => u.surveyCompleted).length} + {users.filter(u => u.surveyCompleted).length}
@@ -339,7 +302,7 @@ function UserManagementContent() { setSearchTerm(e.target.value)} + onChange={e => setSearchTerm(e.target.value)} className="pl-10" />
@@ -349,7 +312,7 @@ function UserManagementContent() { All Departments - {departments.map((dept) => ( + {departments.map(dept => ( {dept} @@ -395,7 +358,7 @@ function UserManagementContent() { {user.name ?.split(' ') - .map((n) => n[0]) + .map(n => n[0]) .join('') || 'U'} diff --git a/apps/web/src/app/dashboard/tasks/page.tsx b/apps/web/src/app/dashboard/tasks/page.tsx index e4197a8..6fdd80b 100644 --- a/apps/web/src/app/dashboard/tasks/page.tsx +++ b/apps/web/src/app/dashboard/tasks/page.tsx @@ -10,51 +10,55 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from '@/components/ui/select'; import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from '@/components/ui/table'; import { Textarea } from '@/components/ui/textarea'; import { - DndContext, - DragEndEvent, - DragOverlay, - DragStartEvent, - PointerSensor, - closestCenter, - useSensor, - useSensors, + DndContext, + DragEndEvent, + DragOverlay, + DragStartEvent, + PointerSensor, + closestCenter, + useSensor, + useSensors, } from '@dnd-kit/core'; import { - SortableContext, - arrayMove, - useSortable, - verticalListSortingStrategy, + SortableContext, + arrayMove, + useSortable, + verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { - AlertCircle, - CheckCircle, - Clock, - Flag, - GripVertical, - LayoutGrid, - List, - Plus, + AlertCircle, + CheckCircle, + Clock, + Flag, + GripVertical, + LayoutGrid, + List, + Plus, } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; +import { fetchDashboardTasksFromApi } from '@/lib/mvp-api-bridge'; +import { assignmentLookup } from '@/lib/mvp-data'; +import { isApiDataMode } from '@/lib/worksight-api'; +import type { Assignment } from '@worksight/common/types'; interface Task { id: string; @@ -68,63 +72,33 @@ interface Task { type ViewMode = 'kanban' | 'table'; -// Mock tasks data - in real app this would come from your backend -const initialTasks: Task[] = [ - { - id: '1', - title: 'Implement survey results storage', - description: 'Create Zustand store for survey results with persistence', - status: 'completed', - priority: 'high', - dueDate: '2025-08-24', - storyPoints: 8, - }, - { - id: '2', - title: 'Fix ESLint warnings', - description: 'Resolve remaining TypeScript and linting issues', - status: 'in-progress', - priority: 'medium', - dueDate: '2025-08-25', - storyPoints: 3, - }, - { - id: '3', - title: 'Dashboard navigation improvements', - description: 'Implement client-side routing for dashboard sections', - status: 'in-progress', - priority: 'high', - dueDate: '2025-08-26', - storyPoints: 5, - }, - { - id: '4', - title: 'Mobile responsive design', - description: 'Ensure dashboard works well on mobile devices', - status: 'pending', - priority: 'medium', - dueDate: '2025-08-28', - storyPoints: 13, - }, - { - id: '5', - title: 'User authentication improvements', - description: 'Add password reset and email verification', - status: 'pending', - priority: 'low', - dueDate: '2025-08-30', - storyPoints: 8, - }, - { - id: '6', - title: 'Add task drag and drop', - description: 'Implement kanban board with draggable tasks', - status: 'in-progress', - priority: 'high', - dueDate: '2025-08-25', - storyPoints: 8, - }, -]; +function mapDashboardStatus(status: Assignment['status']): Task['status'] { + if (status === 'completed') return 'completed'; + if (status === 'in_progress') return 'in-progress'; + return 'pending'; +} + +function mapDashboardPriority(priority: Assignment['priority']): Task['priority'] { + if (priority === 'critical' || priority === 'high') return 'high'; + if (priority === 'medium') return 'medium'; + return 'low'; +} + +function getInitialTasksFromCommon(): Task[] { + const assignments = assignmentLookup.all(); + if (assignments.length === 0) { + throw new Error('Common assignment fixtures empty; refusing silent empty fallback'); + } + return assignments.map(assignment => ({ + id: assignment.id, + title: assignment.title ?? assignment.external_id ?? 'Untitled task', + description: [assignment.epic, assignment.sprint, assignment.type].filter(Boolean).join(' Β· '), + status: mapDashboardStatus(assignment.status), + priority: mapDashboardPriority(assignment.priority), + dueDate: assignment.updated_at.toISOString().slice(0, 10), + storyPoints: assignment.points ?? 1, + })); +} const statusConfig = { pending: { icon: Clock, color: 'text-orange-500', bg: 'bg-orange-50', label: 'Pending' }, @@ -147,11 +121,30 @@ const statusOrder: Task['status'][] = ['pending', 'in-progress', 'completed']; export default function TasksPage() { const [viewMode, setViewMode] = useState('kanban'); - const [tasks, setTasks] = useState(initialTasks); + const [tasks, setTasks] = useState(() => + isApiDataMode() ? [] : getInitialTasksFromCommon() + ); const [activeTask, setActiveTask] = useState(null); const [showNewTaskDialog, setShowNewTaskDialog] = useState(false); const [editingTaskId, setEditingTaskId] = useState(null); + useEffect(() => { + if (!isApiDataMode()) return; + let cancelled = false; + (async () => { + try { + const apiTasks = await fetchDashboardTasksFromApi(); + if (!cancelled) setTasks(apiTasks); + } catch (err) { + console.warn('API dashboard tasks failed; falling back to fixtures', err); + if (!cancelled) setTasks(getInitialTasksFromCommon()); + } + })(); + return () => { + cancelled = true; + }; + }, []); + // Get current user's stats if they're an employee const sensors = useSensors( @@ -162,14 +155,14 @@ export default function TasksPage() { }) ); - const completedTasks = tasks.filter((task) => task.status === 'completed'); - const inProgressTasks = tasks.filter((task) => task.status === 'in-progress'); + const completedTasks = tasks.filter(task => task.status === 'completed'); + const inProgressTasks = tasks.filter(task => task.status === 'in-progress'); const totalStoryPoints = tasks.reduce((sum, task) => sum + task.storyPoints, 0); const completedStoryPoints = completedTasks.reduce((sum, task) => sum + task.storyPoints, 0); const handleDragStart = (event: DragStartEvent) => { - const task = tasks.find((t) => t.id === event.active.id); + const task = tasks.find(t => t.id === event.active.id); setActiveTask(task || null); }; @@ -181,7 +174,7 @@ export default function TasksPage() { return; } - const activeTask = tasks.find((t) => t.id === active.id); + const activeTask = tasks.find(t => t.id === active.id); if (!activeTask) { setActiveTask(null); return; @@ -191,25 +184,25 @@ export default function TasksPage() { if (over.id === 'pending' || over.id === 'in-progress' || over.id === 'completed') { const newStatus = over.id as Task['status']; if (activeTask.status !== newStatus) { - setTasks((prev) => - prev.map((task) => (task.id === activeTask.id ? { ...task, status: newStatus } : task)) + setTasks(prev => + prev.map(task => (task.id === activeTask.id ? { ...task, status: newStatus } : task)) ); } } else { // Reordering within same status or between tasks const overId = over.id as string; - const overTask = tasks.find((t) => t.id === overId); + const overTask = tasks.find(t => t.id === overId); if (overTask && activeTask.id !== overTask.id) { - setTasks((prev) => { - const oldIndex = prev.findIndex((t) => t.id === activeTask.id); - const newIndex = prev.findIndex((t) => t.id === overTask.id); + setTasks(prev => { + const oldIndex = prev.findIndex(t => t.id === activeTask.id); + const newIndex = prev.findIndex(t => t.id === overTask.id); const updatedTasks = arrayMove(prev, oldIndex, newIndex); // If moving to a different status group, update the status if (activeTask.status !== overTask.status) { - return updatedTasks.map((task) => + return updatedTasks.map(task => task.id === activeTask.id ? { ...task, status: overTask.status } : task ); } @@ -223,8 +216,8 @@ export default function TasksPage() { }; const toggleTaskStatus = (taskId: string) => { - setTasks((prev) => - prev.map((task) => { + setTasks(prev => + prev.map(task => { if (task.id === taskId) { const currentIndex = statusOrder.indexOf(task.status); const nextIndex = (currentIndex + 1) % statusOrder.length; @@ -240,12 +233,12 @@ export default function TasksPage() { ...newTask, id: Date.now().toString(), // Simple ID generation }; - setTasks((prev) => [...prev, task]); + setTasks(prev => [...prev, task]); setShowNewTaskDialog(false); }; const updateTask = (taskId: string, updates: Partial) => { - setTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, ...updates } : task))); + setTasks(prev => prev.map(task => (task.id === taskId ? { ...task, ...updates } : task))); }; return ( @@ -314,94 +307,128 @@ export default function TasksPage() { label: 'Learn about tasks', href: '/help', }} - illustration={} + illustration={ + + } /> ) : ( -
- - - Total Tasks - - -
{tasks.length}
-

- {totalStoryPoints} story points total -

-
-
- - - - Completed - - - -
{completedTasks.length}
-

- {completedStoryPoints} story points -

-
-
- - - - In Progress - - - -
{inProgressTasks.length}
-

Active work items

-
-
- - - - Completion Rate - - -
- {Math.round((completedTasks.length / tasks.length) * 100)}% -
-

Task completion rate

-
-
-
+
+ + + Total Tasks + + +
{tasks.length}
+

+ {totalStoryPoints} story points total +

+
+
+ + + + Completed + + + +
{completedTasks.length}
+

+ {completedStoryPoints} story points +

+
+
+ + + + In Progress + + + +
{inProgressTasks.length}
+

Active work items

+
+
+ + + + Completion Rate + + +
+ {Math.round((completedTasks.length / tasks.length) * 100)}% +
+

Task completion rate

+
+
+
)} {/* Task Views */} {tasks.length > 0 && ( - - {viewMode === 'kanban' ? ( - - ) : ( - - )} - - - {activeTask ? : null} - - + + {viewMode === 'kanban' ? ( + + ) : ( + + )} + + + {activeTask ? : null} + + )} {/* New Task Dialog */} @@ -427,15 +454,15 @@ function KanbanView({ onToggleStatus: (taskId: string) => void; }) { const tasksByStatus = { - pending: tasks.filter((task) => task.status === 'pending'), - 'in-progress': tasks.filter((task) => task.status === 'in-progress'), - completed: tasks.filter((task) => task.status === 'completed'), + pending: tasks.filter(task => task.status === 'pending'), + 'in-progress': tasks.filter(task => task.status === 'in-progress'), + completed: tasks.filter(task => task.status === 'completed'), }; return (
- {statusOrder.map((status) => { + {statusOrder.map(status => { const statusInfo = statusConfig[status]; const StatusIcon = statusInfo.icon; const statusTasks = tasksByStatus[status]; @@ -486,11 +513,8 @@ function KanbanColumn({ ref={setNodeRef} className="border-muted-foreground/25 max-h-[600px] min-h-[200px] space-y-3 overflow-y-auto rounded-lg border-2 border-dashed p-4" > - task.id)} - strategy={verticalListSortingStrategy} - > - {tasks.map((task) => ( + task.id)} strategy={verticalListSortingStrategy}> + {tasks.map(task => ( ))} @@ -521,10 +545,7 @@ function TableView({ Tasks Table - task.id)} - strategy={verticalListSortingStrategy} - > + task.id)} strategy={verticalListSortingStrategy}> @@ -538,14 +559,14 @@ function TableView({ - {tasks.map((task) => ( + {tasks.map(task => ( setEditingTaskId(editing ? task.id : null)} + setEditing={editing => setEditingTaskId(editing ? task.id : null)} disabled={isDragging} /> ))} @@ -658,13 +679,13 @@ function SortableTableRow({
setEditValues({ ...editValues, title: e.target.value })} + onChange={e => setEditValues({ ...editValues, title: e.target.value })} placeholder="Task title" className="font-medium" />