From 8e52718c2c9908352398283fde0373898b7bb609 Mon Sep 17 00:00:00 2001 From: kuyacarlo <106532351+kuyacarlo@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:50:14 +0800 Subject: [PATCH 1/2] feat(web): wire dashboard views to @worksight/common fixtures (#16) Populate employees, tasks, and survey admin/dashboard views from common data/utils so the MVP no longer depends on duplicated inline mocks. Co-authored-by: Cursor --- apps/web/next.config.ts | 8 +- apps/web/src/app/admin/surveys/page.tsx | 96 +----- apps/web/src/app/admin/users/page.tsx | 101 ++---- apps/web/src/app/dashboard/tasks/page.tsx | 88 ++---- apps/web/src/app/tasks/page.tsx | 63 +--- apps/web/src/components/dashboard/stats.tsx | 15 +- apps/web/src/data/employees.ts | 53 +--- apps/web/src/lib/mvp-data.ts | 323 ++++++++++++++++++++ apps/web/tsconfig.json | 3 + docs/handoffs/2026-07-25-wire-web-common.md | 24 +- packages/common/src/data/employees.ts | 46 +++ 11 files changed, 468 insertions(+), 352 deletions(-) create mode 100644 apps/web/src/lib/mvp-data.ts 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..bd36f97 100644 --- a/apps/web/src/app/admin/users/page.tsx +++ b/apps/web/src/app/admin/users/page.tsx @@ -2,7 +2,6 @@ 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'; @@ -35,6 +34,7 @@ import { validateUserArray, validateUserFilters, } from '@/schemas/user'; +import { getUsersWithMetrics } from '@/lib/mvp-data'; import { AlertTriangle, CheckCircle, @@ -49,74 +49,6 @@ import { } 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([]); @@ -127,23 +59,24 @@ function UserManagementContent() { const [validationErrors, setValidationErrors] = useState([]); useEffect(() => { - // Simulate API call with validation - setTimeout(() => { - // Validate mock data using Zod - const validationResult = validateUserArray(mockUsers); + // Load employees from @worksight/common fixtures + const fixtureUsers = getUsersWithMetrics(); + const validationResult = validateUserArray(fixtureUsers); + + if (!validationResult.allValid) { + const errors = validationResult.invalid.map( + (item) => + `User at index ${item.index}: ${item.errors?.map((e: { message: string }) => e.message).join(', ')}` + ); + setValidationErrors(errors); + } - if (!validationResult.allValid) { - const errors = validationResult.invalid.map( - (item) => - `User at index ${item.index}: ${item.errors?.map((e: { message: string }) => e.message).join(', ')}` - ); - setValidationErrors(errors); - } + if (validationResult.valid.length === 0 && fixtureUsers.length > 0) { + throw new Error('Common employee fixtures failed validation; refusing empty fallback'); + } - // Use only valid users - setUsers(validationResult.valid.map((item) => item.data!)); - setIsLoading(false); - }, 1000); + setUsers(validationResult.valid.map((item) => item.data!)); + setIsLoading(false); }, []); // Validate filters when they change diff --git a/apps/web/src/app/dashboard/tasks/page.tsx b/apps/web/src/app/dashboard/tasks/page.tsx index e4197a8..6e34b05 100644 --- a/apps/web/src/app/dashboard/tasks/page.tsx +++ b/apps/web/src/app/dashboard/tasks/page.tsx @@ -55,6 +55,8 @@ import { } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; +import { assignmentLookup } from '@/lib/mvp-data'; +import type { Assignment } from '@worksight/common/types'; interface Task { id: string; @@ -68,63 +70,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,7 +119,7 @@ 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(() => getInitialTasksFromCommon()); const [activeTask, setActiveTask] = useState(null); const [showNewTaskDialog, setShowNewTaskDialog] = useState(false); const [editingTaskId, setEditingTaskId] = useState(null); diff --git a/apps/web/src/app/tasks/page.tsx b/apps/web/src/app/tasks/page.tsx index c870fb4..680f24e 100644 --- a/apps/web/src/app/tasks/page.tsx +++ b/apps/web/src/app/tasks/page.tsx @@ -45,60 +45,9 @@ import { import { CSS } from '@dnd-kit/utilities'; import { AlertCircle, CheckSquare, Clock, GripVertical, LogOut, Plus, Search } from 'lucide-react'; import { useCallback, useMemo, useState } from 'react'; +import { getMvpTasks, type MvpTask } from '@/lib/mvp-data'; -interface Task { - id: string; - title: string; - description: string; - status: 'todo' | 'in-progress' | 'completed'; - priority: 'low' | 'medium' | 'high'; - dueDate: string; - estimatedHours: number; - order: number; -} - -const mockTasks: Task[] = [ - { - id: '1', - title: 'Implement user authentication', - description: 'Set up Supabase auth with OAuth providers and offline fallback', - status: 'completed', - priority: 'high', - dueDate: '2025-08-20', - estimatedHours: 8, - order: 0, - }, - { - id: '2', - title: 'Design task management interface', - description: 'Create a clean, intuitive interface for managing work tasks', - status: 'in-progress', - priority: 'medium', - dueDate: '2025-08-21', - estimatedHours: 6, - order: 1, - }, - { - id: '3', - title: 'Add burnout tracking metrics', - description: 'Implement features to track and analyze work burnout patterns', - status: 'todo', - priority: 'high', - dueDate: '2025-08-23', - estimatedHours: 12, - order: 2, - }, - { - id: '4', - title: 'Setup CI/CD pipeline', - description: 'Configure automated testing and deployment workflows', - status: 'todo', - priority: 'low', - dueDate: '2025-08-25', - estimatedHours: 4, - order: 3, - }, -]; +type Task = MvpTask; interface SortableTaskProps { task: Task; @@ -260,7 +209,13 @@ function SortableTask({ task, onTaskUpdate }: SortableTaskProps) { function TasksContent() { const { user, logout } = useAuth(); const [searchTerm, setSearchTerm] = useState(''); - const [tasks, setTasks] = useState(mockTasks.sort((a, b) => a.order - b.order)); + const [tasks, setTasks] = useState(() => { + const fixtureTasks = getMvpTasks(); + if (fixtureTasks.length === 0) { + throw new Error('Common assignment fixtures empty; refusing silent empty fallback'); + } + return [...fixtureTasks].sort((a, b) => a.order - b.order); + }); const sensors = useSensors( useSensor(PointerSensor), diff --git a/apps/web/src/components/dashboard/stats.tsx b/apps/web/src/components/dashboard/stats.tsx index 2956dc3..d3bd1a3 100644 --- a/apps/web/src/components/dashboard/stats.tsx +++ b/apps/web/src/components/dashboard/stats.tsx @@ -5,14 +5,15 @@ import { isOfflineMode } from '@/auth/offline'; import { SurveyResultsCard } from '@/components/dashboard/survey-results-card'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Employees } from '@/data/employees'; import { + findEmployeeByEmail, getAfterHoursActivities, getDataSourceUsageStats, + getEmployeeCount, getEmployeeProductivityStats, getTeamMetaStats, getWeekendActivities, -} from '@/data/work-tracking'; +} from '@/lib/mvp-data'; import { useEffect, useState } from 'react'; export function DashboardStats() { @@ -24,12 +25,10 @@ export function DashboardStats() { setIsOffline(isOfflineMode()); }, []); - // Get current user's stats if they're an employee - const currentEmployee = user - ? Object.entries(Employees).find(([_, emp]) => emp.email === user.email) - : null; + // Get current user's stats if they're an employee (common fixtures) + const currentEmployee = user?.email ? findEmployeeByEmail(user.email) : null; const currentEmployeeStats = currentEmployee - ? getEmployeeProductivityStats(currentEmployee[0]) + ? getEmployeeProductivityStats(currentEmployee.id) : null; const teamMetaStats = getTeamMetaStats(); @@ -121,7 +120,7 @@ export function DashboardStats() { Team Members -
{Object.keys(Employees).length}
+
{getEmployeeCount()}

4sight employees

diff --git a/apps/web/src/data/employees.ts b/apps/web/src/data/employees.ts index b7c180a..69d97b9 100644 --- a/apps/web/src/data/employees.ts +++ b/apps/web/src/data/employees.ts @@ -1,3 +1,9 @@ +import { getEmployeesRecord } from '@/lib/mvp-data'; + +/** + * Compatibility shim: employees now come from @worksight/common via mvp-data. + * Prefer importing from `@/lib/mvp-data` or `@worksight/common/data` directly. + */ export type EmployeeProfile = { [key: string]: { internal_id: string; @@ -12,52 +18,7 @@ export type EmployeeProfile = { }; }; -export const Employees: EmployeeProfile = { - '08b6fc43-77e6-4fcf-8ed8-dafc16b4b025': { - internal_id: 'E001', - email: 'sjc.71415@gmail.com', - name: 'John Carlo Santos', - role: 'fullstack', - manager_id: '', - date_joined: new Date('2025-08-24'), - department: 'Infra', - created_at: new Date('2025-08-24T16:51:38.176858+00:00'), - updated_at: new Date('2025-08-24T16:51:38.176858+00:00'), - }, - '58f36d76-f382-41b4-ad3e-f8192958d12b': { - internal_id: 'E002', - email: 'dagsmagalona@gmail.com', - name: 'Adriel M. Magalona', - role: 'frontend', - manager_id: '', - date_joined: new Date('2025-08-24'), - department: 'Engineering', - created_at: new Date('2025-08-24T16:51:38.176858+00:00'), - updated_at: new Date('2025-08-24T16:51:38.176858+00:00'), - }, - '71400e28-3c2a-4694-8124-8fbb9a0b66d8': { - internal_id: 'E003', - email: 'kielethanlanzanas@gmail.com', - name: 'Kiel Ethan L. Lanzanas', - role: 'data-ml', - manager_id: '', - date_joined: new Date('2025-08-24'), - department: 'Data', - created_at: new Date('2025-08-24T16:51:38.176858+00:00'), - updated_at: new Date('2025-08-24T16:51:38.176858+00:00'), - }, - '88165ccb-2c80-455a-9ace-466a30448f67': { - internal_id: 'E004', - email: 'ebenerado@gmail.com', - name: 'Ellah D. Benerado', - role: 'data-ml', - manager_id: '', - date_joined: new Date('2025-08-24'), - department: 'Data', - created_at: new Date('2025-08-24T16:51:38.176858+00:00'), - updated_at: new Date('2025-08-24T16:51:38.176858+00:00'), - }, -}; +export const Employees: EmployeeProfile = getEmployeesRecord(); // Also export with the old name for backward compatibility export const employees = Employees; diff --git a/apps/web/src/lib/mvp-data.ts b/apps/web/src/lib/mvp-data.ts new file mode 100644 index 0000000..7fbe514 --- /dev/null +++ b/apps/web/src/lib/mvp-data.ts @@ -0,0 +1,323 @@ +/** + * MVP data bridge: map @worksight/common fixtures/utils into web view models. + * Keeps dashboard/admin pages free of duplicated local mocks. + */ +import { UserRole } from '@/auth/types'; +import type { UserWithMetrics } from '@/schemas/user'; +import { + Activities, + Assignments, + DataSources, + Employees, + SurveyQuestionnaire, + SurveyResponseList, + Surveys, + Teams, +} from '@worksight/common/data'; +import type { + Activity, + Assignment, + EmployeeProfile, + Survey as CommonSurvey, +} from '@worksight/common/types'; +import { + ActivityLookup, + AssignmentLookup, + EmployeeLookup, + SourceLookup, + SurveyMetadataLookup, + SurveyQuestionLookup, + SurveyResponsesLookup, + TeamLookup, +} from '@worksight/common/utils'; + +export const employeeLookup = new EmployeeLookup(Employees); +export const teamLookup = new TeamLookup(Teams); +export const assignmentLookup = new AssignmentLookup(Assignments); +export const activityLookup = new ActivityLookup(Activities); +export const sourceLookup = new SourceLookup(DataSources); +export const surveyLookup = new SurveyMetadataLookup(Surveys); +export const surveyQuestionLookup = new SurveyQuestionLookup(SurveyQuestionnaire); +export const surveyResponseMetaLookup = new SurveyResponsesLookup(SurveyResponseList); + +const ROLE_MAP: Record = { + employee: UserRole.EMPLOYEE, + team_lead: UserRole.TEAM_LEAD, + manager: UserRole.MANAGER, + admin: UserRole.ADMIN, + super_admin: UserRole.SUPER_ADMIN, +}; + +function assertFixturesPopulated(): void { + if (Employees.length === 0) { + throw new Error('@worksight/common Employees fixture is empty'); + } + if (Assignments.length === 0) { + throw new Error('@worksight/common Assignments fixture is empty'); + } + if (Surveys.length === 0) { + throw new Error('@worksight/common Surveys fixture is empty'); + } +} + +assertFixturesPopulated(); + +function departmentLabel(employee: EmployeeProfile): string { + const first = employee.department.find((d) => d && d.length > 0); + return first || 'unassigned'; +} + +function teamLabel(employee: EmployeeProfile): string { + if (employee.team) { + const team = teamLookup.getById(employee.team); + if (team) return team.name; + } + const managed = teamLookup.getByManager(employee.id); + if (managed[0]) return managed[0].name; + const memberTeam = Teams.find((t) => t.member_ids.includes(employee.id)); + return memberTeam?.name ?? departmentLabel(employee); +} + +function mapRole(role: string): UserRole { + return ROLE_MAP[role] ?? UserRole.EMPLOYEE; +} + +function relativeTime(date: Date): string { + const deltaMs = Date.now() - date.getTime(); + const minutes = Math.max(1, Math.floor(deltaMs / 60_000)); + if (minutes < 60) return `${minutes} minutes ago`; + const hours = Math.floor(minutes / 60); + if (hours < 48) return `${hours} hours ago`; + const days = Math.floor(hours / 24); + return `${days} days ago`; +} + +function riskFromBurnout(score: number): 'low' | 'medium' | 'high' { + if (score >= 7) return 'high'; + if (score >= 4) return 'medium'; + return 'low'; +} + +/** Admin users table rows from common employees + task/survey lookupt. */ +export function getUsersWithMetrics(): UserWithMetrics[] { + return employeeLookup + .all() + .filter((e) => e.role !== 'guest') + .map((employee) => { + const stats = assignmentLookup.getStats(employee.id); + const burnoutScore = Math.min( + 10, + Math.round(((100 - stats.workLifeBalanceScore) / 10) * 10) / 10 + ); + const surveyCompleted = surveyResponseMetaLookup + .all() + .some((r) => r.employee_id === employee.id || r.employee_id === employee.internal_id); + + return { + id: employee.id, + name: employee.name, + email: employee.email, + role: mapRole(employee.role), + department: departmentLabel(employee), + team: teamLabel(employee), + burnoutScore, + lastActive: relativeTime(employee.updated_at), + surveyCompleted, + riskLevel: riskFromBurnout(burnoutScore), + tasksCompleted: stats.completedTasks, + }; + }); +} + +export type MvpTask = { + id: string; + title: string; + description: string; + status: 'todo' | 'in-progress' | 'completed'; + priority: 'low' | 'medium' | 'high'; + dueDate: string; + estimatedHours: number; + order: number; + assigneeId?: string; +}; + +function mapAssignmentStatus(status: Assignment['status']): MvpTask['status'] { + if (status === 'in_progress') return 'in-progress'; + if (status === 'completed') return 'completed'; + return 'todo'; +} + +function mapAssignmentPriority(priority: Assignment['priority']): MvpTask['priority'] { + if (priority === 'critical' || priority === 'high') return 'high'; + if (priority === 'medium') return 'medium'; + return 'low'; +} + +/** Task board rows from common Assignments. */ +export function getMvpTasks(): MvpTask[] { + return assignmentLookup.all().map((assignment, index) => ({ + id: assignment.id, + title: assignment.title ?? assignment.external_id ?? 'Untitled task', + description: [assignment.epic, assignment.sprint, assignment.type] + .filter(Boolean) + .join(' · '), + status: mapAssignmentStatus(assignment.status), + priority: mapAssignmentPriority(assignment.priority), + dueDate: assignment.updated_at.toISOString().slice(0, 10), + estimatedHours: Math.max(1, Math.round((assignment.points ?? 1) * 0.5)), + order: index, + assigneeId: assignment.employee_id, + })); +} + +export type MvpSurvey = { + 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'; +}; + +function surveyTitle(survey: CommonSurvey): string { + return `Wellness Survey (${survey.num_questions} questions)`; +} + +/** Admin survey list from common Surveys + questions + response metadata. */ +export function getMvpSurveys(): MvpSurvey[] { + const creators = new Map(employeeLookup.all().map((e) => [e.id, e.name])); + const responseCounts = surveyResponseMetaLookup.getStats().submissionsPerSurvey; + + return surveyLookup.all().map((survey) => { + const questionCount = + surveyQuestionLookup.getStats(survey.id).totalQuestions || survey.num_questions; + return { + id: survey.id, + title: surveyTitle(survey), + description: 'Burnout and wellness assessment from @worksight/common fixtures', + status: 'active' as const, + questionCount, + responseCount: responseCounts[survey.id] ?? SurveyResponseList.length, + createdAt: survey.created_at.toISOString().slice(0, 10), + lastModified: survey.created_at.toISOString().slice(0, 10), + createdBy: creators.get(survey.created_by) ?? survey.created_by, + category: 'burnout' as const, + targetAudience: 'all' as const, + }; + }); +} + +export function findEmployeeByEmail(email: string): EmployeeProfile | null { + return employeeLookup.all().find((e) => e.email === email) ?? null; +} + +export function getEmployeeProductivityStats(employeeId: string) { + return assignmentLookup.getStats(employeeId); +} + +export function getAfterHoursActivities(): Activity[] { + return activityLookup.getAfterHoursActivities(); +} + +export function getWeekendActivities(): Activity[] { + return activityLookup.getWeekendActivities(); +} + +export function getTeamMetaStats() { + const allActivities = activityLookup.all(); + const metaActivities = allActivities.filter( + (a) => + a.description.toLowerCase().includes('meta') || + a.description.toLowerCase().includes('ironic') || + a.description.toLowerCase().includes('4th wall') || + a.description.toLowerCase().includes('recursive') + ); + + const allAssignments = assignmentLookup.all(); + const metaAssignments = allAssignments.filter( + (a) => + a.epic === 'FOURTH-WALL-BREAKS' || + (a.title?.toLowerCase().includes('meta') ?? false) || + (a.title?.toLowerCase().includes('ironic') ?? false) + ); + + return { + totalMetaActivities: metaActivities.length, + totalMetaAssignments: metaAssignments.length, + metaActivityPercentage: + allActivities.length > 0 ? (metaActivities.length / allActivities.length) * 100 : 0, + selfAwarenessLevel: metaActivities.length + metaAssignments.length, + fourthWallIntegrity: Math.max(0, 100 - metaActivities.length * 5), + }; +} + +export function getDataSourceUsageStats() { + const sources = sourceLookup.all(); + const sourceStats = {} as Record< + string, + { + name: string; + assignments: number; + activities: number; + total: number; + types: string[]; + } + >; + + for (const source of sources) { + const assignments = assignmentLookup.filter({ source_id: source.id }).count(); + const activities = activityLookup.filter({ source_id: source.id }).count(); + if (assignments === 0 && activities === 0) continue; + sourceStats[source.id] = { + name: source.name, + assignments, + activities, + total: assignments + activities, + types: source.type, + }; + } + + return sourceStats; +} + +export function getEmployeeCount(): number { + return employeeLookup.count(); +} + +/** Legacy record shape used by offline auth / api helpers. */ +export function getEmployeesRecord(): Record< + string, + { + internal_id: string; + email: string; + name: string; + role: string; + manager_id: string; + date_joined: Date; + department: string; + created_at: Date; + updated_at: Date; + } +> { + return Object.fromEntries( + employeeLookup.all().map((e) => [ + e.id, + { + internal_id: e.internal_id, + email: e.email, + name: e.name, + role: e.role, + manager_id: e.manager_id ?? '', + date_joined: e.date_joined, + department: departmentLabel(e), + created_at: e.created_at, + updated_at: e.updated_at, + }, + ]) + ); +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 85c0b83..68b68b6 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -31,6 +31,9 @@ "@worksight/common": [ "../../packages/common/src" ], + "@worksight/common/*": [ + "../../packages/common/src/*" + ], "@/*": [ "./src/*" ] diff --git a/docs/handoffs/2026-07-25-wire-web-common.md b/docs/handoffs/2026-07-25-wire-web-common.md index 2acc661..456905f 100644 --- a/docs/handoffs/2026-07-25-wire-web-common.md +++ b/docs/handoffs/2026-07-25-wire-web-common.md @@ -1,29 +1,29 @@ # HANDOFF — Wire web to common (#16) -**Status:** Planned +**Status:** In progress (MVP slice) **Branch:** `feat/mvp-wire-web` **Issue(s):** #16 **Last updated:** 2026-07-25 ## Bottom line -Dashboard/UI uses `@worksight/common` **data + types + utils** (employees, tasks, survey, burnout, datasources) instead of duplicated local mocks. +Dashboard/UI uses `@worksight/common` **data + types + utils** (employees, tasks, survey) instead of duplicated local mocks. ## Current state - Package exports: `@worksight/common`, `/data`, `/types`, `/utils` -- Fixtures in `packages/common/src/data/*.ts` -- Web landing is marketing-only; limited common import (api uses `Roles` from types) - -## Hook points -- `apps/web/src/app/**` dashboard routes -- `apps/web/src/store(s)/**`, `data/`, `hooks/` -- `next.config.ts` transpilePackages already aware of common +- **Slice landed:** `apps/web/src/lib/mvp-data.ts` bridges common → dashboard stats, `/tasks`, `/dashboard/tasks`, `/admin/users`, `/admin/surveys` +- Local `@/data/employees` re-exports common via compatibility shim +- Managers (E001–E004) added to common `Employees` so assignments/teams resolve ## How to verify ```bash +pnpm --filter @worksight/common build +pnpm --filter @worksight/web type-check +pnpm --filter @worksight/web build pnpm --filter @worksight/web dev -# open dashboard; employees/tasks populated from common fixtures +# open /dashboard, /dashboard/tasks, /admin/users, /admin/surveys ``` ## Done means -- [ ] ≥ employees + tasks + survey views import from common -- [ ] No silent empty fallbacks when fixtures exist +- [x] ≥ employees + tasks + survey views import from common +- [x] No silent empty fallbacks when fixtures exist +- [ ] Remaining local fixtures (`work-tracking`, `surveys`, survey form) fully retired diff --git a/packages/common/src/data/employees.ts b/packages/common/src/data/employees.ts index 1e1a2f1..aa88e4b 100644 --- a/packages/common/src/data/employees.ts +++ b/packages/common/src/data/employees.ts @@ -1,6 +1,52 @@ import { EmployeeProfile, Team } from '../types'; export const Employees: EmployeeProfile[] = [ + // Managers (referenced by Teams + Assignments) + { + id: '08b6fc43-77e6-4fcf-8ed8-dafc16b4b025', + internal_id: 'E001', + email: 'sjc.71415@gmail.com', + name: 'John Carlo Santos', + role: 'manager', + date_joined: new Date('2025-08-24'), + department: ['backend'], + created_at: new Date('2025-08-24T16:51:38.176858+00:00'), + updated_at: new Date('2025-08-24T16:51:38.176858+00:00'), + }, + { + id: '58f36d76-f382-41b4-ad3e-f8192958d12b', + internal_id: 'E002', + email: 'dagsmagalona@gmail.com', + name: 'Adriel M. Magalona', + role: 'manager', + date_joined: new Date('2025-08-24'), + department: ['sysadmin'], + created_at: new Date('2025-08-24T16:51:38.176858+00:00'), + updated_at: new Date('2025-08-24T16:51:38.176858+00:00'), + }, + { + id: '71400e28-3c2a-4694-8124-8fbb9a0b66d8', + internal_id: 'E003', + email: 'kielethanlanzanas@gmail.com', + name: 'Kiel Ethan L. Lanzanas', + role: 'manager', + date_joined: new Date('2025-08-24'), + department: ['data'], + created_at: new Date('2025-08-24T16:51:38.176858+00:00'), + updated_at: new Date('2025-08-24T16:51:38.176858+00:00'), + }, + { + id: '88165ccb-2c80-455a-9ace-466a30448f67', + internal_id: 'E004', + email: 'ebenerado@gmail.com', + name: 'Ellah D. Benerado', + role: 'manager', + date_joined: new Date('2025-08-24'), + department: ['data'], + created_at: new Date('2025-08-24T16:51:38.176858+00:00'), + updated_at: new Date('2025-08-24T16:51:38.176858+00:00'), + }, + // Team members under John Carlo Santos (Infra Manager) { id: 'e5a1b2c3-d4e5-f6g7-h8i9-j0k1l2m3n4o5', From ab0f1895e6b1b612a576ba4ce21c5d9c6b263804 Mon Sep 17 00:00:00 2001 From: kuyacarlo <106532351+kuyacarlo@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:34:35 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(demo):=20E2E=20path=20for=20web=20?= =?UTF-8?q?=E2=86=94=20API=20=E2=86=94=20common=20fixtures=20(#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a runnable demo (pnpm demo /docs/mvp/DEMO.md) so Nest and Next agree on the @worksight/common contract: public /demo page, API CORS on :3001, and an optional NEXT_PUBLIC_USE_API toggle for admin/tasks views. Fixture-backed only. Co-authored-by: Cursor --- README.md | 15 +- apps/api/src/main.ts | 19 +- apps/web/.env.example | 9 +- apps/web/src/app/admin/users/page.tsx | 128 +++--- apps/web/src/app/dashboard/tasks/page.tsx | 470 ++++++++++++---------- apps/web/src/app/demo/page.tsx | 236 +++++++++++ apps/web/src/app/tasks/page.tsx | 106 +++-- apps/web/src/lib/mvp-api-bridge.ts | 205 ++++++++++ apps/web/src/lib/worksight-api.ts | 92 +++++ docs/handoffs/2026-07-25-demo-path.md | 44 +- docs/mvp/DEMO.md | 90 +++++ docs/mvp/README.md | 34 +- package.json | 3 + scripts/demo.sh | 75 ++++ turbo.json | 4 +- 15 files changed, 1213 insertions(+), 317 deletions(-) create mode 100644 apps/web/src/app/demo/page.tsx create mode 100644 apps/web/src/lib/mvp-api-bridge.ts create mode 100644 apps/web/src/lib/worksight-api.ts create mode 100644 docs/mvp/DEMO.md create mode 100755 scripts/demo.sh 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/src/app/admin/users/page.tsx b/apps/web/src/app/admin/users/page.tsx index bd36f97..1ceaba4 100644 --- a/apps/web/src/app/admin/users/page.tsx +++ b/apps/web/src/app/admin/users/page.tsx @@ -8,44 +8,46 @@ 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'; @@ -57,26 +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(() => { - // Load employees from @worksight/common fixtures - const fixtureUsers = getUsersWithMetrics(); - const validationResult = validateUserArray(fixtureUsers); - - if (!validationResult.allValid) { - const errors = validationResult.invalid.map( - (item) => - `User at index ${item.index}: ${item.errors?.map((e: { message: string }) => e.message).join(', ')}` - ); - setValidationErrors(errors); - } + let cancelled = false; - if (validationResult.valid.length === 0 && fixtureUsers.length > 0) { - throw new Error('Common employee fixtures failed validation; refusing empty fallback'); - } + const applyUsers = (rawUsers: UserWithMetrics[], source: 'api' | 'fixtures') => { + const validationResult = validateUserArray(rawUsers); + + if (!validationResult.allValid) { + const errors = validationResult.invalid.map( + item => + `User at index ${item.index}: ${item.errors?.map((e: { message: string }) => e.message).join(', ')}` + ); + setValidationErrors(errors); + } - setUsers(validationResult.valid.map((item) => item.data!)); - setIsLoading(false); + 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 @@ -112,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()) || @@ -126,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]); @@ -177,7 +202,12 @@ function UserManagementContent() {
-

User Management

+
+

User Management

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

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

- {users.filter((u) => u.riskLevel === 'high').length} + {users.filter(u => u.riskLevel === 'high').length}
@@ -241,7 +271,7 @@ function UserManagementContent() {
- {users.filter((u) => u.surveyCompleted).length} + {users.filter(u => u.surveyCompleted).length}
@@ -272,7 +302,7 @@ function UserManagementContent() { setSearchTerm(e.target.value)} + onChange={e => setSearchTerm(e.target.value)} className="pl-10" />
@@ -282,7 +312,7 @@ function UserManagementContent() { All Departments - {departments.map((dept) => ( + {departments.map(dept => ( {dept} @@ -328,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 6e34b05..6fdd80b 100644 --- a/apps/web/src/app/dashboard/tasks/page.tsx +++ b/apps/web/src/app/dashboard/tasks/page.tsx @@ -10,52 +10,54 @@ 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 { @@ -87,7 +89,7 @@ function getInitialTasksFromCommon(): Task[] { if (assignments.length === 0) { throw new Error('Common assignment fixtures empty; refusing silent empty fallback'); } - return assignments.map((assignment) => ({ + 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(' · '), @@ -119,11 +121,30 @@ const statusOrder: Task['status'][] = ['pending', 'in-progress', 'completed']; export default function TasksPage() { const [viewMode, setViewMode] = useState('kanban'); - const [tasks, setTasks] = useState(() => getInitialTasksFromCommon()); + 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( @@ -134,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); }; @@ -153,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; @@ -163,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 ); } @@ -195,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; @@ -212,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 ( @@ -286,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 */} @@ -399,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]; @@ -458,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 => ( ))} @@ -493,10 +545,7 @@ function TableView({ Tasks Table - task.id)} - strategy={verticalListSortingStrategy} - > + task.id)} strategy={verticalListSortingStrategy}> @@ -510,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} /> ))} @@ -630,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" />