zoho projects api integrated
This commit is contained in:
parent
438654be98
commit
a68523567a
@ -60,6 +60,13 @@ const getScopeForService = (_serviceKey?: ServiceKey): string => {
|
|||||||
// Zoho Projects
|
// Zoho Projects
|
||||||
'ZohoProjects.projects.READ',
|
'ZohoProjects.projects.READ',
|
||||||
'ZohoProjects.tasklists.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',
|
'ZohoProjects.timesheets.READ',
|
||||||
// Zoho CRM (adjust modules per your needs)
|
// Zoho CRM (adjust modules per your needs)
|
||||||
'ZohoCRM.users.READ',
|
'ZohoCRM.users.READ',
|
||||||
|
|||||||
@ -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 { View, Text, ScrollView, RefreshControl, StyleSheet } from 'react-native';
|
||||||
import Icon from 'react-native-vector-icons/MaterialIcons';
|
import Icon from 'react-native-vector-icons/MaterialIcons';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui';
|
import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui';
|
||||||
import { useTheme } from '@/shared/styles/useTheme';
|
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';
|
import type { RootState } from '@/store/store';
|
||||||
|
|
||||||
const ZohoProjectsDashboardScreen: React.FC = () => {
|
const ZohoProjectsDashboardScreen: React.FC = () => {
|
||||||
@ -13,99 +27,85 @@ const ZohoProjectsDashboardScreen: React.FC = () => {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
// Fetch projects on mount
|
// Fetch all data on mount
|
||||||
// Guard: avoid duplicate fetch while loading
|
dispatch(fetchAllZohoProjectsData({}) as any);
|
||||||
if (!loading) {
|
|
||||||
// @ts-ignore
|
|
||||||
dispatch(fetchZohoProjects());
|
|
||||||
}
|
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
// @ts-ignore
|
await dispatch(fetchAllZohoProjectsData({ refresh: true }) as any);
|
||||||
await dispatch(fetchZohoProjects());
|
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock analytics data (UI only)
|
// Loading state
|
||||||
const mock = useMemo(() => {
|
if (isLoading && stats.totalProjects === 0) {
|
||||||
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) {
|
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
// Error state
|
||||||
return <ErrorState message={error} onRetry={() => dispatch(fetchZohoProjects() as any)} />;
|
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 (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@ -118,182 +118,269 @@ const ZohoProjectsDashboardScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{/* Sprint Header */}
|
{/* Projects Overview Header */}
|
||||||
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
<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 style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<View>
|
<View>
|
||||||
<Text style={{ fontSize: 14, color: colors.text, fontFamily: fonts.bold }}>{mock.sprintName}</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 }}>{mock.sprintDates}</Text>
|
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular, opacity: 0.8 }}>Team Members: {stats.totalTeamMembers}</Text>
|
||||||
</View>
|
</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>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* KPI Cards */}
|
{/* KPI Cards */}
|
||||||
<View style={styles.kpiGrid}>
|
<View style={styles.kpiGrid}>
|
||||||
<KpiCard label="Backlog" value={mock.backlog} color={colors.text} accent="#3AA0FF" />
|
<KpiCard label="Active Projects" value={stats.activeProjects} color={colors.text} accent="#3AA0FF" />
|
||||||
<KpiCard label="In Progress" value={mock.inProgress} color={colors.text} accent="#F59E0B" />
|
<KpiCard label="Completed Tasks" value={stats.closedTasks} color={colors.text} accent="#10B981" />
|
||||||
<KpiCard label="Completed" value={mock.completed} color={colors.text} accent="#10B981" />
|
<KpiCard label="High Priority" value={stats.highPriorityTasks} color={colors.text} accent="#F59E0B" />
|
||||||
<KpiCard label="Blocked" value={mock.blocked} color={colors.text} accent="#EF4444" />
|
<KpiCard label="Overdue Tasks" value={stats.overdueTasks} color={colors.text} accent="#EF4444" />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Trend: Burndown & Velocity (mini bars) */}
|
{/* Project Status Distribution & Task Status */}
|
||||||
<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 */}
|
|
||||||
<View style={styles.twoCol}>
|
<View style={styles.twoCol}>
|
||||||
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
||||||
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Bugs by Severity</Text>
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Project Status Distribution</Text>
|
||||||
<StackedBar segments={mock.bugSeverityDist} total={mock.bugSeverityDist.reduce((a, b) => a + b.value, 0)} />
|
<PieChart
|
||||||
<View style={styles.legendRow}>
|
data={projectStatusChartData}
|
||||||
{mock.bugSeverityDist.map(s => (
|
size={120}
|
||||||
<View key={s.label} style={styles.legendItem}>
|
colors={colors}
|
||||||
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
|
fonts={fonts}
|
||||||
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
|
/>
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
||||||
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Tasks by Priority</Text>
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Task Status Distribution</Text>
|
||||||
<StackedBar segments={mock.priorityDist} total={mock.priorityDist.reduce((a, b) => a + b.value, 0)} />
|
<DonutChart
|
||||||
<View style={styles.legendRow}>
|
data={taskStatusChartData}
|
||||||
{mock.priorityDist.map(s => (
|
size={120}
|
||||||
<View key={s.label} style={styles.legendItem}>
|
colors={colors}
|
||||||
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
|
fonts={fonts}
|
||||||
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
|
/>
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Timesheets & Aging */}
|
{/* Project Types & Task Priority */}
|
||||||
<View style={styles.twoCol}>
|
<View style={styles.twoCol}>
|
||||||
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
||||||
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Timesheets</Text>
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Project Types</Text>
|
||||||
<Text style={{ fontSize: 12, fontFamily: fonts.regular, color: colors.text }}>Total: {mock.timesheets.totalHours}h</Text>
|
<StackedBarChart
|
||||||
{(() => {
|
data={projectTypesChartData}
|
||||||
const billablePct = Math.round((mock.timesheets.billableHours / Math.max(1, mock.timesheets.totalHours)) * 100);
|
height={80}
|
||||||
return <ProgressRow label={`Billable (${mock.timesheets.billableHours}h)`} value={billablePct} color="#10B981" />;
|
colors={colors}
|
||||||
})()}
|
fonts={fonts}
|
||||||
{(() => {
|
/>
|
||||||
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" />;
|
|
||||||
})()}
|
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
||||||
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Backlog Aging</Text>
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Task Priority</Text>
|
||||||
<MiniBars data={mock.backlogAging} color="#6366F1" max={Math.max(...mock.backlogAging)} />
|
<StackedBarChart
|
||||||
<View style={styles.legendRow}>
|
data={taskPriorityChartData}
|
||||||
{['0-7d', '8-14d', '15-30d', '30+d'].map(label => (
|
height={80}
|
||||||
<View key={label} style={styles.legendItem}>
|
colors={colors}
|
||||||
<View style={[styles.legendDot, { backgroundColor: '#6366F1' }]} />
|
fonts={fonts}
|
||||||
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{label}</Text>
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Assignee Workload */}
|
{/* Issues Overview */}
|
||||||
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
||||||
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Assignee Workload</Text>
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Issues Overview</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>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Kanban Snapshot */}
|
|
||||||
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
|
||||||
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Kanban Snapshot</Text>
|
|
||||||
<View style={styles.badgeRow}>
|
<View style={styles.badgeRow}>
|
||||||
<Chip label="To Do" value={mock.kanban.todo} dot="#3AA0FF" />
|
<Chip label="Total Issues" value={stats.totalIssues} dot="#6366F1" />
|
||||||
<Chip label="Doing" value={mock.kanban.doing} dot="#F59E0B" />
|
<Chip label="Open Issues" value={stats.openIssues} dot="#EF4444" />
|
||||||
<Chip label="Review" value={mock.kanban.review} dot="#6366F1" />
|
<Chip label="Closed Issues" value={stats.closedIssues} dot="#10B981" />
|
||||||
<Chip label="Done" value={mock.kanban.done} dot="#10B981" />
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Lists: Risks and Top Clients */}
|
{/* Issues Distribution & Severity */}
|
||||||
<View style={styles.twoCol}>
|
<View style={styles.twoCol}>
|
||||||
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
||||||
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Risks</Text>
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Issue Status Distribution</Text>
|
||||||
{mock.risks.map(r => (
|
<PieChart
|
||||||
<View key={r.id} style={styles.listRow}>
|
data={issueStatusChartData}
|
||||||
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{r.title}</Text>
|
size={120}
|
||||||
<Text style={[styles.listSecondary, { color: '#EF4444', fontFamily: fonts.regular }]}>{r.impact}</Text>
|
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>
|
</View>
|
||||||
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
||||||
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Clients</Text>
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Recent Projects</Text>
|
||||||
{mock.topClients.map(c => (
|
{dashboardData.recentProjects.map(project => (
|
||||||
<View key={c.name} style={styles.listRow}>
|
<View key={project.id} style={styles.listRow}>
|
||||||
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{c.name}</Text>
|
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{project.name}</Text>
|
||||||
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.regular }]}>{c.projects} projects</Text>
|
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.regular }]}>{getTimeAgo(project.createdTime)}</Text>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</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' }]}>
|
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
||||||
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Milestones</Text>
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Project Owners Distribution</Text>
|
||||||
{mock.milestones.map(m => (
|
{Object.entries(dashboardData.projectOwnersDistribution).map(([owner, count]) => (
|
||||||
<View key={m.name} style={{ marginTop: 8 }}>
|
<ProgressRow key={owner} label={owner} value={Math.round((count / stats.totalProjects) * 100)} color="#3B82F6" />
|
||||||
<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>
|
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Team Capacity */}
|
{/* Team Overview */}
|
||||||
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
|
||||||
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Team Capacity</Text>
|
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Team Overview</Text>
|
||||||
{mock.teams.map(t => (
|
<View style={styles.statsGrid}>
|
||||||
<ProgressRow key={t.name} label={t.name} value={t.capacityPct} color="#6366F1" />
|
<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>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@ -352,28 +439,6 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
marginBottom: 8,
|
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: {
|
progressRow: {
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
},
|
},
|
||||||
@ -392,29 +457,6 @@ const styles = StyleSheet.create({
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
borderRadius: 6,
|
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: {
|
twoCol: {
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
},
|
},
|
||||||
@ -453,6 +495,22 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
opacity: 0.8,
|
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;
|
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 ProgressRow: React.FC<{ label: string; value: number; color: string }> = ({ label, value, color }) => {
|
||||||
const { fonts } = useTheme();
|
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();
|
const { fonts, colors } = useTheme();
|
||||||
return (
|
return (
|
||||||
<View style={styles.chip}>
|
<View style={styles.chip}>
|
||||||
|
|||||||
@ -1,8 +1,88 @@
|
|||||||
import http from '@/services/http';
|
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 = {
|
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);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
373
src/modules/zohoProjects/store/selectors.ts
Normal file
373
src/modules/zohoProjects/store/selectors.ts
Normal 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 };
|
||||||
@ -1,73 +1,225 @@
|
|||||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { zohoProjectsAPI } from '../services/zohoProjectsAPI';
|
||||||
export interface ZohoProject {
|
import type {
|
||||||
id: string;
|
ZohoProject,
|
||||||
name: string;
|
ZohoTask,
|
||||||
owner: string;
|
ZohoIssue,
|
||||||
status: 'active' | 'completed' | 'onHold';
|
ZohoPhase,
|
||||||
}
|
ZohoProjectsFilters,
|
||||||
|
ZohoProjectsState,
|
||||||
export interface ZohoProjectsFilters {
|
ZohoApiInfo,
|
||||||
owner: 'all' | string;
|
} from '../types/ZohoProjectsTypes';
|
||||||
status: 'all' | ZohoProject['status'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ZohoProjectsState {
|
|
||||||
projects: ZohoProject[];
|
|
||||||
loading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
filters: ZohoProjectsFilters;
|
|
||||||
lastUpdated: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: ZohoProjectsState = {
|
const initialState: ZohoProjectsState = {
|
||||||
projects: [],
|
projects: [],
|
||||||
loading: false,
|
tasks: [],
|
||||||
error: null,
|
issues: [],
|
||||||
filters: { owner: 'all', status: 'all' },
|
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,
|
lastUpdated: null,
|
||||||
|
filters: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchZohoProjects = createAsyncThunk('zohoProjects/fetch', async () => {
|
// Fetch all projects
|
||||||
// TODO: integrate real service
|
export const fetchZohoProjects = createAsyncThunk(
|
||||||
await new Promise(r => setTimeout(r, 300));
|
'zohoProjects/fetchProjects',
|
||||||
return [
|
async (params?: { refresh?: boolean }) => {
|
||||||
{ id: 'p1', name: 'CRM Revamp', owner: 'Alice', status: 'active' },
|
const response = await zohoProjectsAPI.getAllProjects();
|
||||||
{ id: 'p2', name: 'Mobile App', owner: 'Bob', status: 'completed' },
|
return response.data;
|
||||||
] as ZohoProject[];
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
|
// 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({
|
const zohoProjectsSlice = createSlice({
|
||||||
name: 'zohoProjects',
|
name: 'zohoProjects',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setFilters: (state, action: PayloadAction<Partial<ZohoProjectsFilters>>) => {
|
setFilters: (state, action: PayloadAction<Partial<ZohoProjectsFilters>>) => {
|
||||||
state.filters = { ...state.filters, ...action.payload } as ZohoProjectsFilters;
|
state.filters = { ...state.filters, ...action.payload };
|
||||||
},
|
},
|
||||||
clearError: state => {
|
clearError: (state, action: PayloadAction<'projects' | 'tasks' | 'issues' | 'phases'>) => {
|
||||||
state.error = null;
|
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,
|
resetState: () => initialState,
|
||||||
},
|
},
|
||||||
extraReducers: builder => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
.addCase(fetchZohoProjects.pending, state => {
|
// Projects cases
|
||||||
state.loading = true;
|
.addCase(fetchZohoProjects.pending, (state) => {
|
||||||
state.error = null;
|
state.loading.projects = true;
|
||||||
|
state.error.projects = null;
|
||||||
})
|
})
|
||||||
.addCase(fetchZohoProjects.fulfilled, (state, action: PayloadAction<ZohoProject[]>) => {
|
.addCase(fetchZohoProjects.fulfilled, (state, action) => {
|
||||||
state.loading = false;
|
state.loading.projects = false;
|
||||||
state.projects = action.payload;
|
if (action.payload) {
|
||||||
state.lastUpdated = Date.now();
|
state.projects = action.payload.data.data;
|
||||||
|
state.pagination.projects = action.payload.data.info;
|
||||||
|
state.lastUpdated = new Date().toISOString();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addCase(fetchZohoProjects.rejected, (state, action) => {
|
.addCase(fetchZohoProjects.rejected, (state, action) => {
|
||||||
state.loading = false;
|
state.loading.projects = false;
|
||||||
state.error = action.error.message || 'Failed to load projects';
|
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;
|
export default zohoProjectsSlice;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
399
src/modules/zohoProjects/types/ZohoProjectsTypes.ts
Normal file
399
src/modules/zohoProjects/types/ZohoProjectsTypes.ts
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user