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;
+}