From a68523567a54dbcf1b5d4e9a874bea8344f5031a Mon Sep 17 00:00:00 2001 From: yashwin-foxy Date: Sat, 13 Sep 2025 15:14:40 +0530 Subject: [PATCH] zoho projects api integrated --- src/modules/integrations/screens/ZohoAuth.tsx | 7 + .../screens/ZohoProjectsDashboardScreen.tsx | 582 ++++++++++-------- .../zohoProjects/services/zohoProjectsAPI.ts | 84 ++- src/modules/zohoProjects/store/selectors.ts | 373 +++++++++++ .../zohoProjects/store/zohoProjectsSlice.ts | 242 ++++++-- .../zohoProjects/types/ZohoProjectsTypes.ts | 399 ++++++++++++ 6 files changed, 1369 insertions(+), 318 deletions(-) create mode 100644 src/modules/zohoProjects/store/selectors.ts create mode 100644 src/modules/zohoProjects/types/ZohoProjectsTypes.ts diff --git a/src/modules/integrations/screens/ZohoAuth.tsx b/src/modules/integrations/screens/ZohoAuth.tsx index 5d7e107..19b5665 100644 --- a/src/modules/integrations/screens/ZohoAuth.tsx +++ b/src/modules/integrations/screens/ZohoAuth.tsx @@ -60,6 +60,13 @@ const getScopeForService = (_serviceKey?: ServiceKey): string => { // Zoho Projects 'ZohoProjects.projects.READ', 'ZohoProjects.tasklists.READ', + // Zoho Projects Tasks + 'ZohoProjects.tasks.READ', + // Zoho Projects Issues + 'ZohoProjects.bugs.READ', + // Zoho Projects Milestones + 'ZohoProjects.milestones.READ', + // Zoho Projects Timesheets 'ZohoProjects.timesheets.READ', // Zoho CRM (adjust modules per your needs) 'ZohoCRM.users.READ', diff --git a/src/modules/zohoProjects/screens/ZohoProjectsDashboardScreen.tsx b/src/modules/zohoProjects/screens/ZohoProjectsDashboardScreen.tsx index e8a2903..558916e 100644 --- a/src/modules/zohoProjects/screens/ZohoProjectsDashboardScreen.tsx +++ b/src/modules/zohoProjects/screens/ZohoProjectsDashboardScreen.tsx @@ -1,11 +1,25 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { View, Text, ScrollView, RefreshControl, StyleSheet } from 'react-native'; import Icon from 'react-native-vector-icons/MaterialIcons'; import { useDispatch, useSelector } from 'react-redux'; import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui'; import { useTheme } from '@/shared/styles/useTheme'; -import { fetchZohoProjects } from '@/modules/zohoProjects/store/zohoProjectsSlice'; +import { fetchAllZohoProjectsData } from '@/modules/zohoProjects/store/zohoProjectsSlice'; +import { + selectZohoProjectsStats, + selectDashboardData, + selectIsAnyLoading, + selectHasAnyError, + getTimeAgo, + getStatusColor, +} from '@/modules/zohoProjects/store/selectors'; +import { + PieChart, + DonutChart, + StackedBarChart, + CompactPipeline, +} from '@/shared/components/charts'; import type { RootState } from '@/store/store'; const ZohoProjectsDashboardScreen: React.FC = () => { @@ -13,99 +27,85 @@ const ZohoProjectsDashboardScreen: React.FC = () => { const dispatch = useDispatch(); const [refreshing, setRefreshing] = useState(false); - const { projects, loading, error } = useSelector((s: RootState) => s.zohoProjects); + // Redux state + const stats = useSelector(selectZohoProjectsStats); + const dashboardData = useSelector(selectDashboardData); + const isLoading = useSelector(selectIsAnyLoading); + const hasError = useSelector(selectHasAnyError); useEffect(() => { - // Fetch projects on mount - // Guard: avoid duplicate fetch while loading - if (!loading) { - // @ts-ignore - dispatch(fetchZohoProjects()); - } + // Fetch all data on mount + dispatch(fetchAllZohoProjectsData({}) as any); }, [dispatch]); const handleRefresh = async () => { setRefreshing(true); - // @ts-ignore - await dispatch(fetchZohoProjects()); + await dispatch(fetchAllZohoProjectsData({ refresh: true }) as any); setRefreshing(false); }; - // Mock analytics data (UI only) - const mock = useMemo(() => { - const backlog = 128; - const inProgress = 36; - const completed = 412; - const blocked = 7; - const onTimePct = 84; // percent - const qualityScore = 92; // percent - const utilizationPct = 73; // percent - const burndown = [32, 30, 28, 26, 24, 20, 18]; - const velocity = [18, 22, 19, 24, 26, 23]; - const statusDist = [ - { label: 'Open', value: backlog, color: '#3AA0FF' }, - { label: 'In Progress', value: inProgress, color: '#F59E0B' }, - { label: 'Blocked', value: blocked, color: '#EF4444' }, - { label: 'Done', value: completed, color: '#10B981' }, - ]; - const risks = [ - { id: 'R-1042', title: 'Scope creep in Phase 2', impact: 'High' }, - { id: 'R-1047', title: 'Resource bandwidth next sprint', impact: 'Medium' }, - { id: 'R-1051', title: 'Third-party API rate limits', impact: 'Medium' }, - ]; - const topClients = [ - { name: 'Acme Corp', projects: 12 }, - { name: 'Globex', projects: 9 }, - { name: 'Initech', projects: 7 }, - ]; - // New patterns - const kanban = { todo: 64, doing: 28, review: 12, done: completed }; - const milestones = [ - { name: 'M1: Requirements Freeze', due: 'Aug 25', progress: 100 }, - { name: 'M2: MVP Complete', due: 'Sep 10', progress: 72 }, - { name: 'M3: Beta Release', due: 'Sep 28', progress: 38 }, - ]; - const teams = [ - { name: 'Frontend', capacityPct: 76 }, - { name: 'Backend', capacityPct: 68 }, - { name: 'QA', capacityPct: 54 }, - { name: 'DevOps', capacityPct: 62 }, - ]; - // Zoho Projects specific - const sprintName = 'Sprint 24 - September'; - const sprintDates = 'Sep 01 → Sep 14'; - const scopeChange = +6; // tasks added - const bugSeverityDist = [ - { label: 'Critical', value: 4, color: '#EF4444' }, - { label: 'High', value: 12, color: '#F59E0B' }, - { label: 'Medium', value: 18, color: '#3AA0FF' }, - { label: 'Low', value: 9, color: '#10B981' }, - ]; - const priorityDist = [ - { label: 'P1', value: 8, color: '#EF4444' }, - { label: 'P2', value: 16, color: '#F59E0B' }, - { label: 'P3', value: 22, color: '#3AA0FF' }, - { label: 'P4', value: 12, color: '#10B981' }, - ]; - const timesheets = { totalHours: 436, billableHours: 312 }; - const backlogAging = [42, 31, 18, 12]; // 0-7, 8-14, 15-30, 30+ - const assigneeLoad = [ - { name: 'Aarti', pct: 82 }, - { name: 'Rahul', pct: 74 }, - { name: 'Meera', pct: 66 }, - { name: 'Neeraj', pct: 58 }, - ]; - return { backlog, inProgress, completed, blocked, onTimePct, qualityScore, utilizationPct, burndown, velocity, statusDist, risks, topClients, kanban, milestones, teams, sprintName, sprintDates, scopeChange, bugSeverityDist, priorityDist, timesheets, backlogAging, assigneeLoad }; - }, []); - - if (loading && projects.length === 0) { + // Loading state + if (isLoading && stats.totalProjects === 0) { return ; } - if (error) { - return dispatch(fetchZohoProjects() as any)} />; + // Error state + if (hasError) { + return dispatch(fetchAllZohoProjectsData({}) as any)} />; } + // Prepare chart data + const projectStatusChartData = Object.entries(dashboardData.projectStatusDistribution).map(([label, value]) => ({ + label, + value, + color: getStatusColor(label), + })); + + const taskStatusChartData = Object.entries(dashboardData.taskStatusDistribution).map(([label, value]) => ({ + label, + value, + color: getStatusColor(label), + })); + + const projectTypesChartData = Object.entries(dashboardData.projectTypesDistribution).map(([label, value]) => ({ + label, + value, + color: getStatusColor(label), + })); + + const taskPriorityChartData = Object.entries(dashboardData.taskPriorityDistribution).map(([label, value]) => ({ + label, + value, + color: getStatusColor(label), + })); + + // Pipeline stages data (using project status distribution) + const pipelineStages = Object.entries(dashboardData.projectStatusDistribution).map(([label, value]) => ({ + label, + value, + color: getStatusColor(label), + })); + + // Issue charts data + const issueStatusChartData = Object.entries(dashboardData.issueStatusDistribution).map(([label, value]) => ({ + label, + value, + color: getStatusColor(label), + })); + + const issueSeverityChartData = Object.entries(dashboardData.issueSeverityDistribution).map(([label, value]) => ({ + label, + value, + color: getStatusColor(label), + })); + + // Phase charts data + const phaseStatusChartData = Object.entries(dashboardData.phaseStatusDistribution).map(([label, value]) => ({ + label, + value, + color: getStatusColor(label), + })); + return ( { - {/* Sprint Header */} + {/* Projects Overview Header */} - Active Sprint + Projects Overview - {mock.sprintName} - {mock.sprintDates} + Total Projects: {stats.totalProjects} + Team Members: {stats.totalTeamMembers} - 0 ? 'Scope +' : 'Scope'} value={Math.abs(mock.scopeChange)} dot={mock.scopeChange > 0 ? '#F59E0B' : '#10B981'} /> + {/* KPI Cards */} - - - - + + + + - {/* Trend: Burndown & Velocity (mini bars) */} - - Sprint Trends - - - Burndown - - - - Velocity - - - - - - {/* Bugs & Priority Mix */} + {/* Project Status Distribution & Task Status */} - Bugs by Severity - a + b.value, 0)} /> - - {mock.bugSeverityDist.map(s => ( - - - {s.label} - - ))} - + Project Status Distribution + - Tasks by Priority - a + b.value, 0)} /> - - {mock.priorityDist.map(s => ( - - - {s.label} - - ))} - + Task Status Distribution + - {/* Timesheets & Aging */} + {/* Project Types & Task Priority */} - Timesheets - Total: {mock.timesheets.totalHours}h - {(() => { - const billablePct = Math.round((mock.timesheets.billableHours / Math.max(1, mock.timesheets.totalHours)) * 100); - return ; - })()} - {(() => { - const non = mock.timesheets.totalHours - mock.timesheets.billableHours; - const pct = Math.round((non / Math.max(1, mock.timesheets.totalHours)) * 100); - return ; - })()} + Project Types + - Backlog Aging - - - {['0-7d', '8-14d', '15-30d', '30+d'].map(label => ( - - - {label} - - ))} - + Task Priority + - {/* Assignee Workload */} - - Assignee Workload - {mock.assigneeLoad.map(a => ( - - ))} - - {/* Progress: On-time, Quality, Utilization */} - - Operational Health - - - - - - {/* Status distribution */} - - Status Distribution - a + b.value, 0)} /> - - {mock.statusDist.map(s => ( - - - {s.label} + {/* Project Pipeline & Milestones */} + + + Project Pipeline Stages + + + + Milestones Overview + + + {stats.totalMilestones} + Total - ))} + + {stats.openMilestones} + Open + + + {stats.closedMilestones} + Closed + + - {/* Kanban Snapshot */} + {/* Issues Overview */} - Kanban Snapshot + Issues Overview - - - - + + + - {/* Lists: Risks and Top Clients */} + {/* Issues Distribution & Severity */} - Top Risks - {mock.risks.map(r => ( - - {r.title} - {r.impact} + Issue Status Distribution + + + + Issue Severity Distribution + + + + + {/* Tasks Overview */} + + Tasks Overview + + + + + + + + {/* Phases Overview */} + + Phases Overview + + + + + + + + {/* Phase Status Distribution */} + + Phase Status Distribution + + + + {/* Work Hours & Task Completion */} + + + Work Hours + + + {Math.round(stats.totalWorkHours)}h + Total Work + + + {Math.round(stats.billableHours)}h + Billable + + + {Math.round(stats.nonBillableHours)}h + Non-Billable + + + + + Task Completion + + + {stats.averageTaskCompletion}% + Avg Completion + + + {stats.highPriorityTasks} + High Priority + + + {stats.overdueTasks} + Overdue + + + + + + {/* Top Projects & Recent Projects */} + + + Top Projects by Completion + {dashboardData.topProjectsByCompletion.map(project => ( + + {project.name} + {project.completion}% ))} - Top Clients - {mock.topClients.map(c => ( - - {c.name} - {c.projects} projects + Recent Projects + {dashboardData.recentProjects.map(project => ( + + {project.name} + {getTimeAgo(project.createdTime)} ))} - {/* Milestones Progress */} + {/* Top Issues & Recent Phases */} + + + Top Issues by Severity + {dashboardData.topIssuesBySeverity.map(issue => ( + + {issue.name} + {issue.severity} + + ))} + + + Recent Phases + {dashboardData.recentPhases.map(phase => ( + + {phase.name} + {getTimeAgo(phase.createdTime)} + + ))} + + + + {/* Project Owners Distribution */} - Milestones - {mock.milestones.map(m => ( - - - {m.name} - {m.due} - - - - - + Project Owners Distribution + {Object.entries(dashboardData.projectOwnersDistribution).map(([owner, count]) => ( + ))} - {/* Team Capacity */} + {/* Team Overview */} - Team Capacity - {mock.teams.map(t => ( - - ))} + Team Overview + + + {stats.totalTeamMembers} + Team Members + + + {stats.averageCompletionRate}% + Avg Completion + + + {stats.totalProjects} + Total Projects + + @@ -352,28 +439,6 @@ const styles = StyleSheet.create({ fontSize: 16, marginBottom: 8, }, - trendRow: { - flexDirection: 'row', - }, - trendBlock: { - flex: 1, - paddingRight: 8, - }, - trendTitle: { - fontSize: 12, - opacity: 0.8, - marginBottom: 8, - }, - miniBars: { - flexDirection: 'row', - alignItems: 'flex-end', - }, - miniBar: { - flex: 1, - marginRight: 6, - borderTopLeftRadius: 4, - borderTopRightRadius: 4, - }, progressRow: { marginTop: 8, }, @@ -392,29 +457,6 @@ const styles = StyleSheet.create({ height: '100%', borderRadius: 6, }, - stackedBar: { - height: 12, - borderRadius: 8, - backgroundColor: '#E5E7EB', - overflow: 'hidden', - }, - legendRow: { - flexDirection: 'row', - flexWrap: 'wrap', - marginTop: 8, - }, - legendItem: { - flexDirection: 'row', - alignItems: 'center', - marginRight: 12, - marginTop: 6, - }, - legendDot: { - width: 8, - height: 8, - borderRadius: 4, - marginRight: 6, - }, twoCol: { marginTop: 12, }, @@ -453,6 +495,22 @@ const styles = StyleSheet.create({ fontSize: 12, opacity: 0.8, }, + statsGrid: { + flexDirection: 'row', + justifyContent: 'space-around', + marginTop: 12, + }, + statItem: { + alignItems: 'center', + }, + statValue: { + fontSize: 20, + marginBottom: 4, + }, + statLabel: { + fontSize: 12, + opacity: 0.8, + }, }); export default ZohoProjectsDashboardScreen; @@ -471,15 +529,6 @@ const KpiCard: React.FC<{ label: string; value: number | string; color: string; ); }; -const MiniBars: React.FC<{ data: number[]; color: string; max: number }> = ({ data, color, max }) => { - return ( - - {data.map((v, i) => ( - - ))} - - ); -}; const ProgressRow: React.FC<{ label: string; value: number; color: string }> = ({ label, value, color }) => { const { fonts } = useTheme(); @@ -496,17 +545,8 @@ const ProgressRow: React.FC<{ label: string; value: number; color: string }> = ( ); }; -const StackedBar: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => { - return ( - - {segments.map(s => ( - - ))} - - ); -}; -const Chip: React.FC<{ label: string; value: number; dot: string }> = ({ label, value, dot }) => { +const Chip: React.FC<{ label: string; value: number | string; dot: string }> = ({ label, value, dot }) => { const { fonts, colors } = useTheme(); return ( diff --git a/src/modules/zohoProjects/services/zohoProjectsAPI.ts b/src/modules/zohoProjects/services/zohoProjectsAPI.ts index 80f38ca..d9f9e0b 100644 --- a/src/modules/zohoProjects/services/zohoProjectsAPI.ts +++ b/src/modules/zohoProjects/services/zohoProjectsAPI.ts @@ -1,8 +1,88 @@ import http from '@/services/http'; -import { API_ENDPOINTS } from '@/shared/constants/API_ENDPOINTS'; +import type { + ZohoProjectsApiResponse, + ZohoTasksApiResponse, + ZohoIssuesApiResponse, + ZohoPhasesApiResponse, + ZohoProjectsFilters, +} from '../types/ZohoProjectsTypes'; + +export interface ZohoProjectsSearchParams { + provider?: string; + service?: string; + resource?: string; + portal_id?: string; + page?: number; + limit?: number; + status?: string; + project_type?: string; +} + +const ZOHO_PROJECTS_BASE_URL = '/api/v1/integrations'; export const zohoProjectsAPI = { - getProjects: () => http.get(API_ENDPOINTS.ZOHO_PROJECTS), + // Get all projects with pagination and filters + getAllProjects: (params?: ZohoProjectsSearchParams) => { + const queryParams = { + provider: 'zoho', + service: 'projects', + resource: 'projects', + page: 1, + limit: 20, + ...params, + }; + return http.get(`${ZOHO_PROJECTS_BASE_URL}/all-projects`, queryParams); + }, + + // Get all project tasks with pagination + getAllProjectTasks: (portalId: string, params?: ZohoProjectsSearchParams) => { + const queryParams = { + provider: 'zoho', + portal_id: portalId, + page: 1, + limit: 50, + ...params, + }; + return http.get(`${ZOHO_PROJECTS_BASE_URL}/all-project-tasks`, queryParams); + }, + + // Get all project issues with pagination + getAllProjectIssues: (portalId: string, params?: ZohoProjectsSearchParams) => { + const queryParams = { + provider: 'zoho', + portal_id: portalId, + page: 1, + limit: 50, + ...params, + }; + return http.get(`${ZOHO_PROJECTS_BASE_URL}/all-project-issues`, queryParams); + }, + + // Get all project phases with pagination + getAllProjectPhases: (portalId: string, params?: ZohoProjectsSearchParams) => { + const queryParams = { + provider: 'zoho', + portal_id: portalId, + page: 1, + limit: 50, + ...params, + }; + return http.get(`${ZOHO_PROJECTS_BASE_URL}/all-project-phases`, queryParams); + }, + + // Get projects by filters + getProjectsByFilters: (filters: ZohoProjectsFilters, params?: ZohoProjectsSearchParams) => { + const queryParams = { + provider: 'zoho', + service: 'projects', + resource: 'projects', + page: 1, + limit: 20, + ...filters, + ...params, + }; + return http.get(`${ZOHO_PROJECTS_BASE_URL}/all-projects`, queryParams); + }, }; diff --git a/src/modules/zohoProjects/store/selectors.ts b/src/modules/zohoProjects/store/selectors.ts new file mode 100644 index 0000000..7385e7a --- /dev/null +++ b/src/modules/zohoProjects/store/selectors.ts @@ -0,0 +1,373 @@ +import { createSelector } from '@reduxjs/toolkit'; +import type { RootState } from '@/store/store'; +import type { + ZohoProjectsDashboardStats, + ZohoProject, + ZohoTask, + ZohoApiInfo, +} from '../types/ZohoProjectsTypes'; + +// Base selectors +export const selectZohoProjects = (state: RootState) => state.zohoProjects.projects; +export const selectZohoTasks = (state: RootState) => state.zohoProjects.tasks; +export const selectZohoIssues = (state: RootState) => state.zohoProjects.issues; +export const selectZohoPhases = (state: RootState) => state.zohoProjects.phases; +export const selectZohoProjectsLoading = (state: RootState) => state.zohoProjects.loading; +export const selectZohoProjectsErrors = (state: RootState) => state.zohoProjects.error; +export const selectZohoProjectsPagination = (state: RootState) => state.zohoProjects.pagination; +export const selectZohoProjectsLastUpdated = (state: RootState) => state.zohoProjects.lastUpdated; + +// Derived selectors +export const selectIsAnyLoading = createSelector( + [selectZohoProjectsLoading], + (loading) => Object.values(loading).some(Boolean) +); + +export const selectHasAnyError = createSelector( + [selectZohoProjectsErrors], + (errors) => Object.values(errors).some(error => error !== null) +); + +export const selectZohoProjectsStats = createSelector( + [selectZohoProjects, selectZohoTasks, selectZohoIssues, selectZohoPhases], + (projects, tasks, issues, phases): ZohoProjectsDashboardStats => { + // Ensure arrays are valid + const validProjects = Array.isArray(projects) ? projects : []; + const validTasks = Array.isArray(tasks) ? tasks : []; + const validIssues = Array.isArray(issues) ? issues : []; + const validPhases = Array.isArray(phases) ? phases : []; + + // Project statistics + const totalProjects = validProjects.length; + const activeProjects = validProjects.filter(p => + p.status?.name === 'Active' || p.project_type === 'active' + ).length; + const completedProjects = validProjects.filter(p => + p.is_completed || p.status?.name === 'Completed' + ).length; + + // Task statistics + const totalTasks = validTasks.length; + const openTasks = validTasks.filter(t => + !t.is_completed || t.status?.is_closed_type === false + ).length; + const closedTasks = validTasks.filter(t => + t.is_completed || t.status?.is_closed_type === true + ).length; + + // Milestone statistics from projects + const totalMilestones = validProjects.reduce((sum, p) => + sum + (p.milestones?.open_count || 0) + (p.milestones?.closed_count || 0), 0 + ); + const openMilestones = validProjects.reduce((sum, p) => + sum + (p.milestones?.open_count || 0), 0 + ); + const closedMilestones = validProjects.reduce((sum, p) => + sum + (p.milestones?.closed_count || 0), 0 + ); + + // Issue statistics from issues data + const totalIssues = validIssues.length; + const openIssues = validIssues.filter(i => + !i.status?.is_closed_type || i.status?.name === 'Open' + ).length; + const closedIssues = validIssues.filter(i => + i.status?.is_closed_type || i.status?.name === 'Closed' + ).length; + + // Phase statistics from phases data + const totalPhases = validPhases.length; + const activePhases = validPhases.filter(p => + !p.status?.is_closed || p.status?.name === 'Active' + ).length; + const completedPhases = validPhases.filter(p => + p.status?.is_closed || p.status?.name === 'Completed' + ).length; + + // Average completion rate + const averageCompletionRate = totalProjects > 0 + ? Math.round(validProjects.reduce((sum, p) => sum + (p.percent_complete || 0), 0) / totalProjects) + : 0; + + // Team members count (unique owners) + const uniqueOwners = new Set( + validProjects.map(p => p.owner?.email || p.owner?.name).filter(Boolean) + ); + const totalTeamMembers = uniqueOwners.size; + + // Work hours statistics + const totalWorkHours = validTasks.reduce((sum, t) => { + const workHours = parseFloat(t.owners_and_work?.total_work?.replace(':', '.') || '0'); + return sum + workHours; + }, 0); + + const billableHours = validTasks.reduce((sum, t) => { + const billable = parseFloat(t.log_hours?.billable_hours?.replace(':', '.') || '0'); + return sum + billable; + }, 0); + + const nonBillableHours = validTasks.reduce((sum, t) => { + const nonBillable = parseFloat(t.log_hours?.non_billable_hours?.replace(':', '.') || '0'); + return sum + nonBillable; + }, 0); + + // Task completion statistics + const averageTaskCompletion = totalTasks > 0 + ? Math.round(validTasks.reduce((sum, t) => sum + (t.completion_percentage || 0), 0) / totalTasks) + : 0; + + // Priority and overdue tasks + const highPriorityTasks = validTasks.filter(t => + t.priority === 'high' || t.priority === 'urgent' + ).length; + + const overdueTasks = validTasks.filter(t => { + if (t.is_completed) return false; + const endDate = new Date(t.end_date); + const now = new Date(); + return endDate < now; + }).length; + + // Average phase completion + const averagePhaseCompletion = totalPhases > 0 + ? Math.round(validPhases.reduce((sum, p) => sum + (p.completion_percentage || 0), 0) / totalPhases) + : 0; + + return { + totalProjects, + activeProjects, + completedProjects, + totalTasks, + openTasks, + closedTasks, + totalMilestones, + openMilestones, + closedMilestones, + totalIssues, + openIssues, + closedIssues, + totalPhases, + activePhases, + completedPhases, + averageCompletionRate, + totalTeamMembers, + totalWorkHours, + billableHours, + nonBillableHours, + averageTaskCompletion, + highPriorityTasks, + overdueTasks, + averagePhaseCompletion, + }; + } +); + +export const selectDashboardData = createSelector( + [selectZohoProjects, selectZohoTasks, selectZohoIssues, selectZohoPhases, selectZohoProjectsStats], + (projects, tasks, issues, phases, stats) => { + const validProjects = Array.isArray(projects) ? projects : []; + const validTasks = Array.isArray(tasks) ? tasks : []; + const validIssues = Array.isArray(issues) ? issues : []; + const validPhases = Array.isArray(phases) ? phases : []; + + // Project status distribution + const projectStatusDistribution = validProjects.reduce((acc, project) => { + const status = project.status?.name || 'Unknown'; + acc[status] = (acc[status] || 0) + 1; + return acc; + }, {} as Record); + + // Task status distribution + const taskStatusDistribution = validTasks.reduce((acc, task) => { + const status = task.status?.name || 'Unknown'; + acc[status] = (acc[status] || 0) + 1; + return acc; + }, {} as Record); + + // Project types distribution + const projectTypesDistribution = validProjects.reduce((acc, project) => { + const type = project.project_type || 'Unknown'; + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {} as Record); + + // Top projects by completion + const topProjectsByCompletion = validProjects + .sort((a, b) => (b.percent_complete || 0) - (a.percent_complete || 0)) + .slice(0, 5) + .map(p => ({ + id: p.id, + name: p.name, + completion: p.percent_complete || 0, + owner: p.owner?.full_name || p.owner?.name || 'Unknown', + status: p.status?.name || 'Unknown', + })); + + // Recent projects + const recentProjects = validProjects + .sort((a, b) => new Date(b.created_time).getTime() - new Date(a.created_time).getTime()) + .slice(0, 5) + .map(p => ({ + id: p.id, + name: p.name, + createdTime: p.created_time, + owner: p.owner?.full_name || p.owner?.name || 'Unknown', + status: p.status?.name || 'Unknown', + })); + + // Task priority distribution + const taskPriorityDistribution = validTasks.reduce((acc, task) => { + const priority = task.priority || 'Unknown'; + acc[priority] = (acc[priority] || 0) + 1; + return acc; + }, {} as Record); + + // Project owners distribution + const projectOwnersDistribution = validProjects.reduce((acc, project) => { + const owner = project.owner?.full_name || project.owner?.name || 'Unknown'; + acc[owner] = (acc[owner] || 0) + 1; + return acc; + }, {} as Record); + + // Issue status distribution + const issueStatusDistribution = validIssues.reduce((acc, issue) => { + const status = issue.status?.name || 'Unknown'; + acc[status] = (acc[status] || 0) + 1; + return acc; + }, {} as Record); + + // Issue severity distribution + const issueSeverityDistribution = validIssues.reduce((acc, issue) => { + const severity = issue.severity?.value || 'Unknown'; + acc[severity] = (acc[severity] || 0) + 1; + return acc; + }, {} as Record); + + // Phase status distribution + const phaseStatusDistribution = validPhases.reduce((acc, phase) => { + const status = phase.status?.name || 'Unknown'; + acc[status] = (acc[status] || 0) + 1; + return acc; + }, {} as Record); + + // Top issues by severity + const topIssuesBySeverity = validIssues + .filter(i => i.severity?.value && i.severity.value !== 'None') + .sort((a, b) => { + const severityOrder = { 'Critical': 4, 'High': 3, 'Medium': 2, 'Low': 1 }; + return (severityOrder[b.severity.value as keyof typeof severityOrder] || 0) - + (severityOrder[a.severity.value as keyof typeof severityOrder] || 0); + }) + .slice(0, 5) + .map(i => ({ + id: i.id, + name: i.name, + severity: i.severity?.value || 'Unknown', + status: i.status?.name || 'Unknown', + assignee: i.assignee?.name || 'Unassigned', + })); + + // Recent phases + const recentPhases = validPhases + .sort((a, b) => new Date(b.created_time).getTime() - new Date(a.created_time).getTime()) + .slice(0, 5) + .map(p => ({ + id: p.id, + name: p.name, + createdTime: p.created_time, + status: p.status?.name || 'Unknown', + project: p.project?.name || 'Unknown', + })); + + return { + projectStatusDistribution, + taskStatusDistribution, + projectTypesDistribution, + topProjectsByCompletion, + recentProjects, + taskPriorityDistribution, + projectOwnersDistribution, + issueStatusDistribution, + issueSeverityDistribution, + phaseStatusDistribution, + topIssuesBySeverity, + recentPhases, + }; + } +); + +// Helper function to get time ago +function getTimeAgo(dateString: string): string { + const now = new Date(); + const date = new Date(dateString); + const diffInMs = now.getTime() - date.getTime(); + const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); + + if (diffInHours < 1) return 'Just now'; + if (diffInHours < 24) return `${diffInHours}h`; + const diffInDays = Math.floor(diffInHours / 24); + return `${diffInDays}d`; +} + +// Helper function to get status color +function getStatusColor(status: string): string { + const colorPalette = [ + '#3B82F6', // Bright Blue + '#8B5CF6', // Purple + '#06B6D4', // Cyan + '#F59E0B', // Amber + '#10B981', // Emerald + '#F97316', // Orange + '#22C55E', // Green + '#84CC16', // Lime + '#14B8A6', // Teal + '#059669', // Dark Green + '#EF4444', // Red + '#DC2626', // Dark Red + '#991B1B', // Darker Red + '#9CA3AF', // Gray + '#EC4899', // Pink + '#8B5A2B', // Brown + '#B91C1C', // Lost Red + '#16A34A', // Success Green + '#6366F1', // Indigo + '#7C3AED', // Violet + '#0891B2', // Sky Blue + '#CA8A04', // Gold + '#1F2937', // Dark Gray + '#BE185D', // Rose + '#0D9488', // Emerald Dark + '#7C2D12', // Brown Dark + '#1E40AF', // Blue Dark + '#C2410C', // Orange Dark + '#9333EA', // Purple Dark + '#059669', // Green Dark + '#DC2626', // Hot Red + '#F97316', // Warm Orange + '#6B7280', // Cold Gray + '#EF4444', // Error Red + '#10B981', // Success Green + '#F59E0B', // Warning Orange + '#3B82F6', // Info Blue + '#0077B5', // LinkedIn Blue + '#1877F2', // Facebook Blue + '#1DA1F2', // Twitter Blue + '#E4405F', // Instagram Pink + '#FF0000', // YouTube Red + ]; + + // Create a consistent hash from the status string + let hash = 0; + const normalizedStatus = status.toLowerCase().trim(); + for (let i = 0; i < normalizedStatus.length; i++) { + const char = normalizedStatus.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32-bit integer + } + + // Ensure positive index and get color from palette + const colorIndex = Math.abs(hash) % colorPalette.length; + return colorPalette[colorIndex]; +} + +export { getTimeAgo, getStatusColor }; diff --git a/src/modules/zohoProjects/store/zohoProjectsSlice.ts b/src/modules/zohoProjects/store/zohoProjectsSlice.ts index e704815..25190f7 100644 --- a/src/modules/zohoProjects/store/zohoProjectsSlice.ts +++ b/src/modules/zohoProjects/store/zohoProjectsSlice.ts @@ -1,73 +1,225 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; - -export interface ZohoProject { - id: string; - name: string; - owner: string; - status: 'active' | 'completed' | 'onHold'; -} - -export interface ZohoProjectsFilters { - owner: 'all' | string; - status: 'all' | ZohoProject['status']; -} - -export interface ZohoProjectsState { - projects: ZohoProject[]; - loading: boolean; - error: string | null; - filters: ZohoProjectsFilters; - lastUpdated: number | null; -} +import { zohoProjectsAPI } from '../services/zohoProjectsAPI'; +import type { + ZohoProject, + ZohoTask, + ZohoIssue, + ZohoPhase, + ZohoProjectsFilters, + ZohoProjectsState, + ZohoApiInfo, +} from '../types/ZohoProjectsTypes'; const initialState: ZohoProjectsState = { projects: [], - loading: false, - error: null, - filters: { owner: 'all', status: 'all' }, + tasks: [], + issues: [], + phases: [], + loading: { + projects: false, + tasks: false, + issues: false, + phases: false, + }, + error: { + projects: null, + tasks: null, + issues: null, + phases: null, + }, + pagination: { + projects: { count: 0, moreRecords: false, page: 1 }, + tasks: { count: 0, moreRecords: false, page: 1 }, + issues: { count: 0, moreRecords: false, page: 1 }, + phases: { count: 0, moreRecords: false, page: 1 }, + }, lastUpdated: null, + filters: {}, }; -export const fetchZohoProjects = createAsyncThunk('zohoProjects/fetch', async () => { - // TODO: integrate real service - await new Promise(r => setTimeout(r, 300)); - return [ - { id: 'p1', name: 'CRM Revamp', owner: 'Alice', status: 'active' }, - { id: 'p2', name: 'Mobile App', owner: 'Bob', status: 'completed' }, - ] as ZohoProject[]; -}); +// Fetch all projects +export const fetchZohoProjects = createAsyncThunk( + 'zohoProjects/fetchProjects', + async (params?: { refresh?: boolean }) => { + const response = await zohoProjectsAPI.getAllProjects(); + return response.data; + } +); + +// Fetch all project tasks +export const fetchZohoTasks = createAsyncThunk( + 'zohoProjects/fetchTasks', + async (portalId: string) => { + const response = await zohoProjectsAPI.getAllProjectTasks(portalId); + return response.data; + } +); + +// Fetch all project issues +export const fetchZohoIssues = createAsyncThunk( + 'zohoProjects/fetchIssues', + async (portalId: string) => { + const response = await zohoProjectsAPI.getAllProjectIssues(portalId); + return response.data; + } +); + +// Fetch all project phases +export const fetchZohoPhases = createAsyncThunk( + 'zohoProjects/fetchPhases', + async (portalId: string) => { + const response = await zohoProjectsAPI.getAllProjectPhases(portalId); + return response.data; + } +); + +// Fetch all data (projects, tasks, issues, phases) +export const fetchAllZohoProjectsData = createAsyncThunk( + 'zohoProjects/fetchAllData', + async (params: { refresh?: boolean } = {}, { dispatch }) => { + // First fetch projects + const projectsResponse = await dispatch(fetchZohoProjects(params)); + + if (fetchZohoProjects.fulfilled.match(projectsResponse) && projectsResponse.payload) { + const projects = projectsResponse.payload.data.data; + // Get portal ID from first project + if (projects.length > 0) { + const portalId = projects[0].portal.id; + // Then fetch tasks, issues, and phases in parallel + await Promise.all([ + dispatch(fetchZohoTasks(portalId)), + dispatch(fetchZohoIssues(portalId)), + dispatch(fetchZohoPhases(portalId)), + ]); + } + } + + return projectsResponse.payload || null; + } +); const zohoProjectsSlice = createSlice({ name: 'zohoProjects', initialState, reducers: { setFilters: (state, action: PayloadAction>) => { - state.filters = { ...state.filters, ...action.payload } as ZohoProjectsFilters; + state.filters = { ...state.filters, ...action.payload }; }, - clearError: state => { - state.error = null; + clearError: (state, action: PayloadAction<'projects' | 'tasks' | 'issues' | 'phases'>) => { + if (action.payload === 'projects') { + state.error.projects = null; + } else if (action.payload === 'tasks') { + state.error.tasks = null; + } else if (action.payload === 'issues') { + state.error.issues = null; + } else { + state.error.phases = null; + } + }, + clearAllErrors: (state) => { + state.error = { projects: null, tasks: null, issues: null, phases: null }; }, resetState: () => initialState, }, - extraReducers: builder => { + extraReducers: (builder) => { builder - .addCase(fetchZohoProjects.pending, state => { - state.loading = true; - state.error = null; + // Projects cases + .addCase(fetchZohoProjects.pending, (state) => { + state.loading.projects = true; + state.error.projects = null; }) - .addCase(fetchZohoProjects.fulfilled, (state, action: PayloadAction) => { - state.loading = false; - state.projects = action.payload; - state.lastUpdated = Date.now(); + .addCase(fetchZohoProjects.fulfilled, (state, action) => { + state.loading.projects = false; + if (action.payload) { + state.projects = action.payload.data.data; + state.pagination.projects = action.payload.data.info; + state.lastUpdated = new Date().toISOString(); + } }) .addCase(fetchZohoProjects.rejected, (state, action) => { - state.loading = false; - state.error = action.error.message || 'Failed to load projects'; + state.loading.projects = false; + state.error.projects = action.error.message || 'Failed to load projects'; + }) + // Tasks cases + .addCase(fetchZohoTasks.pending, (state) => { + state.loading.tasks = true; + state.error.tasks = null; + }) + .addCase(fetchZohoTasks.fulfilled, (state, action) => { + state.loading.tasks = false; + if (action.payload) { + state.tasks = action.payload.data.data; + state.pagination.tasks = action.payload.data.info; + state.lastUpdated = new Date().toISOString(); + } + }) + .addCase(fetchZohoTasks.rejected, (state, action) => { + state.loading.tasks = false; + state.error.tasks = action.error.message || 'Failed to load tasks'; + }) + // Issues cases + .addCase(fetchZohoIssues.pending, (state) => { + state.loading.issues = true; + state.error.issues = null; + }) + .addCase(fetchZohoIssues.fulfilled, (state, action) => { + state.loading.issues = false; + if (action.payload) { + state.issues = action.payload.data.data; + state.pagination.issues = action.payload.data.info; + state.lastUpdated = new Date().toISOString(); + } + }) + .addCase(fetchZohoIssues.rejected, (state, action) => { + state.loading.issues = false; + state.error.issues = action.error.message || 'Failed to load issues'; + }) + // Phases cases + .addCase(fetchZohoPhases.pending, (state) => { + state.loading.phases = true; + state.error.phases = null; + }) + .addCase(fetchZohoPhases.fulfilled, (state, action) => { + state.loading.phases = false; + if (action.payload) { + state.phases = action.payload.data.data; + state.pagination.phases = action.payload.data.info; + state.lastUpdated = new Date().toISOString(); + } + }) + .addCase(fetchZohoPhases.rejected, (state, action) => { + state.loading.phases = false; + state.error.phases = action.error.message || 'Failed to load phases'; + }) + // All data cases + .addCase(fetchAllZohoProjectsData.pending, (state) => { + state.loading.projects = true; + state.loading.tasks = true; + state.loading.issues = true; + state.loading.phases = true; + state.error.projects = null; + state.error.tasks = null; + state.error.issues = null; + state.error.phases = null; + }) + .addCase(fetchAllZohoProjectsData.fulfilled, (state) => { + state.loading.projects = false; + state.loading.tasks = false; + state.loading.issues = false; + state.loading.phases = false; + state.lastUpdated = new Date().toISOString(); + }) + .addCase(fetchAllZohoProjectsData.rejected, (state, action) => { + state.loading.projects = false; + state.loading.tasks = false; + state.loading.issues = false; + state.loading.phases = false; + state.error.projects = action.error.message || 'Failed to load data'; }); }, }); -export const { setFilters, clearError, resetState } = zohoProjectsSlice.actions; +export const { setFilters, clearError, clearAllErrors, resetState } = zohoProjectsSlice.actions; export default zohoProjectsSlice; diff --git a/src/modules/zohoProjects/types/ZohoProjectsTypes.ts b/src/modules/zohoProjects/types/ZohoProjectsTypes.ts new file mode 100644 index 0000000..682f251 --- /dev/null +++ b/src/modules/zohoProjects/types/ZohoProjectsTypes.ts @@ -0,0 +1,399 @@ +// Zoho Projects API Types +export interface ZohoUser { + zuid: number; + zpuid: string; + name: string; + email: string; + first_name: string; + last_name: string; + full_name: string; +} + +export interface ZohoProjectStatus { + id: string; + name: string; + color: string; + color_hexcode: string; + is_closed_type: boolean; +} + +export interface ZohoProjectLayout { + id: string; + name: string; + is_default: boolean; + type: string; +} + +export interface ZohoProjectGroup { + id: string; + name: string; + type: string; +} + +export interface ZohoBudgetInfo { + tracking_method: string; +} + +export interface ZohoTaskCounts { + open_count: number; + closed_count: number; +} + +export interface ZohoIssueCounts { + open_count: number; + closed_count: number; +} + +export interface ZohoMilestoneCounts { + open_count: number; + closed_count: number; +} + +export interface ZohoPortal { + id: string; + name: string; + org_name: string; + portal_url: string; +} + +export interface ZohoProject { + id: string; + key: string; + name: string; + project_type: string; + owner: ZohoUser; + is_public_project: boolean; + start_date: string; + end_date: string; + is_strict_project: boolean; + created_time: string; + created_by: ZohoUser; + modified_time: string; + updated_by: ZohoUser; + status: ZohoProjectStatus; + layout: ZohoProjectLayout; + business_hours_id: string; + is_rollup_project: boolean; + budget_info: ZohoBudgetInfo; + project_group: ZohoProjectGroup; + percent_complete: number; + tasks: ZohoTaskCounts; + issues: ZohoIssueCounts; + milestones: ZohoMilestoneCounts; + is_completed: boolean; + portal: ZohoPortal; +} + +export interface ZohoTaskMilestone { + id: string; + name: string; +} + +export interface ZohoTaskList { + id: string; + name: string; +} + +export interface ZohoTaskProject { + id: string; + name: string; +} + +export interface ZohoTaskStatus { + id: string; + name: string; + color: string; + color_hexcode: string; + is_closed_type: boolean; +} + +export interface ZohoTaskOwner { + zuid: number; + zpuid: string; + name: string; + email: string; + first_name: string; + last_name: string; + work_values: string; +} + +export interface ZohoTaskOwnersAndWork { + work_type: string; + total_work: string; + unit: string; + copy_task_duration: boolean; + owners: ZohoTaskOwner[]; + refresh_business_hours: boolean; +} + +export interface ZohoTaskDuration { + value: string; + type: string; +} + +export interface ZohoTaskSequence { + sequence: number; +} + +export interface ZohoTaskLogHours { + billable_hours: string; + non_billable_hours: string; + total_hours: string; +} + +export interface ZohoTaskAssociationInfo { + has_reminder: boolean; + has_recurrence: boolean; + has_comments: boolean; + has_attachments: boolean; + has_forums: boolean; + has_subtasks: boolean; +} + +// Issue types +export interface ZohoIssueProject { + id: string; + name: string; +} + +export interface ZohoIssueAssignee { + zuid: number; + zpuid: string; + name: string; + email: string; + first_name: string; + last_name: string; +} + +export interface ZohoIssueStatus { + id: string; + name: string; + color: string; + color_hexcode: string; + is_closed_type: boolean; +} + +export interface ZohoIssueMilestone { + id: string; + value: string; +} + +export interface ZohoIssueSeverity { + id: string; + value: string; +} + +export interface ZohoIssueClassification { + id: string; + value: string; +} + +export interface ZohoIssueReproducible { + id: string; + value: string; +} + +export interface ZohoIssueModule { + id: string; + value: string; +} + +export interface ZohoIssue { + id: string; + prefix: string; + name: string; + project: ZohoIssueProject; + flag: string; + created_time: string; + created_by: ZohoUser; + status: ZohoIssueStatus; + assignee: ZohoIssueAssignee; + last_updated_time: string; + release_milestone: ZohoIssueMilestone; + affected_milestone: ZohoIssueMilestone; + severity: ZohoIssueSeverity; + classification: ZohoIssueClassification; + is_it_reproducible: ZohoIssueReproducible; + module: ZohoIssueModule; +} + +// Phase types +export interface ZohoPhaseOwner { + zpuid: string; + last_name: string; + is_client_user: boolean; + first_name: string; + email: string; + zuid: number; +} + +export interface ZohoPhaseProject { + name: string; + id: string; +} + +export interface ZohoPhaseStatus { + color_hexcode: string; + name: string; + id: string; + is_closed: boolean; +} + +export interface ZohoPhaseBudgetInfo { + is_workfield_removed: boolean; +} + +export interface ZohoPhase { + id: string; + name: string; + project: ZohoPhaseProject; + owner: ZohoPhaseOwner; + start_date: string; + end_date: string; + created_time: string; + last_modified_time: string; + flag: string; + status_type: string; + status: ZohoPhaseStatus; + budget_info: ZohoPhaseBudgetInfo; +} + +export interface ZohoTask { + id: string; + prefix: string; + name: string; + description: string; + project: ZohoTaskProject; + milestone: ZohoTaskMilestone; + tasklist: ZohoTaskList; + status: ZohoTaskStatus; + priority: string; + owners_and_work: ZohoTaskOwnersAndWork; + start_date: string; + end_date: string; + completed_on?: string; + duration: ZohoTaskDuration; + completion_percentage: number; + sequence: ZohoTaskSequence; + depth: number; + created_time: string; + last_modified_time: string; + is_completed: boolean; + created_via: string; + created_by: ZohoUser; + billing_type: string; + log_hours: ZohoTaskLogHours; + association_info: ZohoTaskAssociationInfo; +} + +export interface ZohoApiInfo { + count: number; + moreRecords: boolean; + page: number; + pageCount?: number; +} + +export interface ZohoProjectsApiResponse { + status: string; + message: string; + data: { + data: ZohoProject[]; + info: ZohoApiInfo; + }; + timestamp: string; +} + +export interface ZohoTasksApiResponse { + status: string; + message: string; + data: { + data: ZohoTask[]; + info: ZohoApiInfo; + }; + timestamp: string; +} + +export interface ZohoIssuesApiResponse { + status: string; + message: string; + data: { + data: ZohoIssue[]; + info: ZohoApiInfo; + }; + timestamp: string; +} + +export interface ZohoPhasesApiResponse { + status: string; + message: string; + data: { + data: ZohoPhase[]; + info: ZohoApiInfo; + }; + timestamp: string; +} + +// Dashboard specific types +export interface ZohoProjectsDashboardStats { + totalProjects: number; + activeProjects: number; + completedProjects: number; + totalTasks: number; + openTasks: number; + closedTasks: number; + totalMilestones: number; + openMilestones: number; + closedMilestones: number; + totalIssues: number; + openIssues: number; + closedIssues: number; + totalPhases: number; + activePhases: number; + completedPhases: number; + averageCompletionRate: number; + totalTeamMembers: number; + totalWorkHours: number; + billableHours: number; + nonBillableHours: number; + averageTaskCompletion: number; + highPriorityTasks: number; + overdueTasks: number; + averagePhaseCompletion: number; +} + +export interface ZohoProjectsFilters { + status?: string; + projectType?: string; + owner?: string; + dateRange?: { + start: string; + end: string; + }; +} + +export interface ZohoProjectsState { + projects: ZohoProject[]; + tasks: ZohoTask[]; + issues: ZohoIssue[]; + phases: ZohoPhase[]; + loading: { + projects: boolean; + tasks: boolean; + issues: boolean; + phases: boolean; + }; + error: { + projects: string | null; + tasks: string | null; + issues: string | null; + phases: string | null; + }; + pagination: { + projects: ZohoApiInfo; + tasks: ZohoApiInfo; + issues: ZohoApiInfo; + phases: ZohoApiInfo; + }; + lastUpdated: string | null; + filters: ZohoProjectsFilters; +}