zoho projects api integrated

This commit is contained in:
yashwin-foxy 2025-09-13 15:14:40 +05:30
parent 438654be98
commit a68523567a
6 changed files with 1369 additions and 318 deletions

View File

@ -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',

View File

@ -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 <LoadingSpinner />;
}
if (error) {
return <ErrorState message={error} onRetry={() => dispatch(fetchZohoProjects() as any)} />;
// Error state
if (hasError) {
return <ErrorState onRetry={() => 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 (
<Container>
<ScrollView
@ -118,182 +118,269 @@ const ZohoProjectsDashboardScreen: React.FC = () => {
</View>
<View style={styles.content}>
{/* Sprint Header */}
{/* Projects Overview Header */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Active Sprint</Text>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Projects Overview</Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View>
<Text style={{ fontSize: 14, color: colors.text, fontFamily: fonts.bold }}>{mock.sprintName}</Text>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular, opacity: 0.8 }}>{mock.sprintDates}</Text>
<Text style={{ fontSize: 14, color: colors.text, fontFamily: fonts.bold }}>Total Projects: {stats.totalProjects}</Text>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular, opacity: 0.8 }}>Team Members: {stats.totalTeamMembers}</Text>
</View>
<Chip label={mock.scopeChange > 0 ? 'Scope +' : 'Scope'} value={Math.abs(mock.scopeChange)} dot={mock.scopeChange > 0 ? '#F59E0B' : '#10B981'} />
<Chip label="Completion" value={stats.averageCompletionRate} dot="#10B981" />
</View>
</View>
{/* KPI Cards */}
<View style={styles.kpiGrid}>
<KpiCard label="Backlog" value={mock.backlog} color={colors.text} accent="#3AA0FF" />
<KpiCard label="In Progress" value={mock.inProgress} color={colors.text} accent="#F59E0B" />
<KpiCard label="Completed" value={mock.completed} color={colors.text} accent="#10B981" />
<KpiCard label="Blocked" value={mock.blocked} color={colors.text} accent="#EF4444" />
<KpiCard label="Active Projects" value={stats.activeProjects} color={colors.text} accent="#3AA0FF" />
<KpiCard label="Completed Tasks" value={stats.closedTasks} color={colors.text} accent="#10B981" />
<KpiCard label="High Priority" value={stats.highPriorityTasks} color={colors.text} accent="#F59E0B" />
<KpiCard label="Overdue Tasks" value={stats.overdueTasks} color={colors.text} accent="#EF4444" />
</View>
{/* Trend: Burndown & Velocity (mini bars) */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Sprint Trends</Text>
<View style={styles.trendRow}>
<View style={styles.trendBlock}>
<Text style={[styles.trendTitle, { color: colors.text, fontFamily: fonts.medium }]}>Burndown</Text>
<MiniBars data={mock.burndown} color="#3AA0FF" max={Math.max(...mock.burndown)} />
</View>
<View style={styles.trendBlock}>
<Text style={[styles.trendTitle, { color: colors.text, fontFamily: fonts.medium }]}>Velocity</Text>
<MiniBars data={mock.velocity} color="#10B981" max={Math.max(...mock.velocity)} />
</View>
</View>
</View>
{/* Bugs & Priority Mix */}
{/* Project Status Distribution & Task Status */}
<View style={styles.twoCol}>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Bugs by Severity</Text>
<StackedBar segments={mock.bugSeverityDist} total={mock.bugSeverityDist.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.bugSeverityDist.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Project Status Distribution</Text>
<PieChart
data={projectStatusChartData}
size={120}
colors={colors}
fonts={fonts}
/>
</View>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Tasks by Priority</Text>
<StackedBar segments={mock.priorityDist} total={mock.priorityDist.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.priorityDist.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Task Status Distribution</Text>
<DonutChart
data={taskStatusChartData}
size={120}
colors={colors}
fonts={fonts}
/>
</View>
</View>
{/* Timesheets & Aging */}
{/* Project Types & Task Priority */}
<View style={styles.twoCol}>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Timesheets</Text>
<Text style={{ fontSize: 12, fontFamily: fonts.regular, color: colors.text }}>Total: {mock.timesheets.totalHours}h</Text>
{(() => {
const billablePct = Math.round((mock.timesheets.billableHours / Math.max(1, mock.timesheets.totalHours)) * 100);
return <ProgressRow label={`Billable (${mock.timesheets.billableHours}h)`} value={billablePct} color="#10B981" />;
})()}
{(() => {
const non = mock.timesheets.totalHours - mock.timesheets.billableHours;
const pct = Math.round((non / Math.max(1, mock.timesheets.totalHours)) * 100);
return <ProgressRow label={`Non-billable (${non}h)`} value={pct} color="#F59E0B" />;
})()}
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Project Types</Text>
<StackedBarChart
data={projectTypesChartData}
height={80}
colors={colors}
fonts={fonts}
/>
</View>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Backlog Aging</Text>
<MiniBars data={mock.backlogAging} color="#6366F1" max={Math.max(...mock.backlogAging)} />
<View style={styles.legendRow}>
{['0-7d', '8-14d', '15-30d', '30+d'].map(label => (
<View key={label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: '#6366F1' }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{label}</Text>
</View>
))}
</View>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Task Priority</Text>
<StackedBarChart
data={taskPriorityChartData}
height={80}
colors={colors}
fonts={fonts}
/>
</View>
</View>
{/* Assignee Workload */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Assignee Workload</Text>
{mock.assigneeLoad.map(a => (
<ProgressRow key={a.name} label={a.name} value={a.pct} color="#3AA0FF" />
))}
</View>
{/* Progress: On-time, Quality, Utilization */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Operational Health</Text>
<ProgressRow label="On-time delivery" value={mock.onTimePct} color="#3AA0FF" />
<ProgressRow label="Quality score" value={mock.qualityScore} color="#10B981" />
<ProgressRow label="Resource utilization" value={mock.utilizationPct} color="#F59E0B" />
</View>
{/* Status distribution */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Status Distribution</Text>
<StackedBar segments={mock.statusDist} total={mock.statusDist.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.statusDist.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
{/* Project Pipeline & Milestones */}
<View style={styles.twoCol}>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Project Pipeline Stages</Text>
<CompactPipeline
data={pipelineStages}
colors={colors}
fonts={fonts}
/>
</View>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Milestones Overview</Text>
<View style={styles.statsGrid}>
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.text, fontFamily: fonts.bold }]}>{stats.totalMilestones}</Text>
<Text style={[styles.statLabel, { color: colors.text, fontFamily: fonts.regular }]}>Total</Text>
</View>
))}
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.text, fontFamily: fonts.bold }]}>{stats.openMilestones}</Text>
<Text style={[styles.statLabel, { color: colors.text, fontFamily: fonts.regular }]}>Open</Text>
</View>
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.text, fontFamily: fonts.bold }]}>{stats.closedMilestones}</Text>
<Text style={[styles.statLabel, { color: colors.text, fontFamily: fonts.regular }]}>Closed</Text>
</View>
</View>
</View>
</View>
{/* Kanban Snapshot */}
{/* Issues Overview */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Kanban Snapshot</Text>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Issues Overview</Text>
<View style={styles.badgeRow}>
<Chip label="To Do" value={mock.kanban.todo} dot="#3AA0FF" />
<Chip label="Doing" value={mock.kanban.doing} dot="#F59E0B" />
<Chip label="Review" value={mock.kanban.review} dot="#6366F1" />
<Chip label="Done" value={mock.kanban.done} dot="#10B981" />
<Chip label="Total Issues" value={stats.totalIssues} dot="#6366F1" />
<Chip label="Open Issues" value={stats.openIssues} dot="#EF4444" />
<Chip label="Closed Issues" value={stats.closedIssues} dot="#10B981" />
</View>
</View>
{/* Lists: Risks and Top Clients */}
{/* Issues Distribution & Severity */}
<View style={styles.twoCol}>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Risks</Text>
{mock.risks.map(r => (
<View key={r.id} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{r.title}</Text>
<Text style={[styles.listSecondary, { color: '#EF4444', fontFamily: fonts.regular }]}>{r.impact}</Text>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Issue Status Distribution</Text>
<PieChart
data={issueStatusChartData}
size={120}
colors={colors}
fonts={fonts}
/>
</View>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Issue Severity Distribution</Text>
<DonutChart
data={issueSeverityChartData}
size={120}
colors={colors}
fonts={fonts}
/>
</View>
</View>
{/* Tasks Overview */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Tasks Overview</Text>
<View style={styles.badgeRow}>
<Chip label="Total Tasks" value={stats.totalTasks} dot="#3B82F6" />
<Chip label="Open Tasks" value={stats.openTasks} dot="#F59E0B" />
<Chip label="Closed Tasks" value={stats.closedTasks} dot="#10B981" />
</View>
</View>
{/* Phases Overview */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Phases Overview</Text>
<View style={styles.badgeRow}>
<Chip label="Total Phases" value={stats.totalPhases} dot="#8B5CF6" />
<Chip label="Active Phases" value={stats.activePhases} dot="#3B82F6" />
<Chip label="Completed Phases" value={stats.completedPhases} dot="#10B981" />
</View>
</View>
{/* Phase Status Distribution */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Phase Status Distribution</Text>
<StackedBarChart
data={phaseStatusChartData}
height={80}
colors={colors}
fonts={fonts}
/>
</View>
{/* Work Hours & Task Completion */}
<View style={styles.twoCol}>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Work Hours</Text>
<View style={styles.statsGrid}>
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.text, fontFamily: fonts.bold }]}>{Math.round(stats.totalWorkHours)}h</Text>
<Text style={[styles.statLabel, { color: colors.text, fontFamily: fonts.regular }]}>Total Work</Text>
</View>
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.text, fontFamily: fonts.bold }]}>{Math.round(stats.billableHours)}h</Text>
<Text style={[styles.statLabel, { color: colors.text, fontFamily: fonts.regular }]}>Billable</Text>
</View>
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.text, fontFamily: fonts.bold }]}>{Math.round(stats.nonBillableHours)}h</Text>
<Text style={[styles.statLabel, { color: colors.text, fontFamily: fonts.regular }]}>Non-Billable</Text>
</View>
</View>
</View>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Task Completion</Text>
<View style={styles.statsGrid}>
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.text, fontFamily: fonts.bold }]}>{stats.averageTaskCompletion}%</Text>
<Text style={[styles.statLabel, { color: colors.text, fontFamily: fonts.regular }]}>Avg Completion</Text>
</View>
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.text, fontFamily: fonts.bold }]}>{stats.highPriorityTasks}</Text>
<Text style={[styles.statLabel, { color: colors.text, fontFamily: fonts.regular }]}>High Priority</Text>
</View>
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.text, fontFamily: fonts.bold }]}>{stats.overdueTasks}</Text>
<Text style={[styles.statLabel, { color: colors.text, fontFamily: fonts.regular }]}>Overdue</Text>
</View>
</View>
</View>
</View>
{/* Top Projects & Recent Projects */}
<View style={styles.twoCol}>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Projects by Completion</Text>
{dashboardData.topProjectsByCompletion.map(project => (
<View key={project.id} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{project.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.regular }]}>{project.completion}%</Text>
</View>
))}
</View>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Clients</Text>
{mock.topClients.map(c => (
<View key={c.name} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{c.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.regular }]}>{c.projects} projects</Text>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Recent Projects</Text>
{dashboardData.recentProjects.map(project => (
<View key={project.id} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{project.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.regular }]}>{getTimeAgo(project.createdTime)}</Text>
</View>
))}
</View>
</View>
{/* Milestones Progress */}
{/* Top Issues & Recent Phases */}
<View style={styles.twoCol}>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Issues by Severity</Text>
{dashboardData.topIssuesBySeverity.map(issue => (
<View key={issue.id} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{issue.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.regular }]}>{issue.severity}</Text>
</View>
))}
</View>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Recent Phases</Text>
{dashboardData.recentPhases.map(phase => (
<View key={phase.id} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{phase.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.regular }]}>{getTimeAgo(phase.createdTime)}</Text>
</View>
))}
</View>
</View>
{/* Project Owners Distribution */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Milestones</Text>
{mock.milestones.map(m => (
<View key={m.name} style={{ marginTop: 8 }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 }}>
<Text style={{ fontSize: 12, fontFamily: fonts.regular, color: colors.text }}>{m.name}</Text>
<Text style={{ fontSize: 12, fontFamily: fonts.medium, color: colors.text }}>{m.due}</Text>
</View>
<View style={styles.progressTrack}>
<View style={[styles.progressFill, { width: `${m.progress}%`, backgroundColor: '#3AA0FF' }]} />
</View>
</View>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Project Owners Distribution</Text>
{Object.entries(dashboardData.projectOwnersDistribution).map(([owner, count]) => (
<ProgressRow key={owner} label={owner} value={Math.round((count / stats.totalProjects) * 100)} color="#3B82F6" />
))}
</View>
{/* Team Capacity */}
{/* Team Overview */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Team Capacity</Text>
{mock.teams.map(t => (
<ProgressRow key={t.name} label={t.name} value={t.capacityPct} color="#6366F1" />
))}
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Team Overview</Text>
<View style={styles.statsGrid}>
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.text, fontFamily: fonts.bold }]}>{stats.totalTeamMembers}</Text>
<Text style={[styles.statLabel, { color: colors.text, fontFamily: fonts.regular }]}>Team Members</Text>
</View>
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.text, fontFamily: fonts.bold }]}>{stats.averageCompletionRate}%</Text>
<Text style={[styles.statLabel, { color: colors.text, fontFamily: fonts.regular }]}>Avg Completion</Text>
</View>
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.text, fontFamily: fonts.bold }]}>{stats.totalProjects}</Text>
<Text style={[styles.statLabel, { color: colors.text, fontFamily: fonts.regular }]}>Total Projects</Text>
</View>
</View>
</View>
</View>
</ScrollView>
@ -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 (
<View style={styles.miniBars}>
{data.map((v, i) => (
<View key={i} style={[styles.miniBar, { height: Math.max(6, (v / Math.max(1, max)) * 64), backgroundColor: color }]} />
))}
</View>
);
};
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 (
<View style={[styles.stackedBar, { flexDirection: 'row', marginTop: 8 }]}>
{segments.map(s => (
<View key={s.label} style={{ width: `${(s.value / Math.max(1, total)) * 100}%`, backgroundColor: s.color }} />
))}
</View>
);
};
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 (
<View style={styles.chip}>

View File

@ -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<ZohoProjectsApiResponse>(`${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<ZohoTasksApiResponse>(`${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<ZohoIssuesApiResponse>(`${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<ZohoPhasesApiResponse>(`${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<ZohoProjectsApiResponse>(`${ZOHO_PROJECTS_BASE_URL}/all-projects`, queryParams);
},
};

View File

@ -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<string, number>);
// 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<string, number>);
// 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<string, number>);
// 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<string, number>);
// 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<string, number>);
// 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<string, number>);
// 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<string, number>);
// 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<string, number>);
// 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 };

View File

@ -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<Partial<ZohoProjectsFilters>>) => {
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<ZohoProject[]>) => {
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;

View File

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