zoho projects api integrated
This commit is contained in:
parent
438654be98
commit
a68523567a
@ -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',
|
||||
|
||||
@ -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>
|
||||
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Task Priority</Text>
|
||||
<StackedBarChart
|
||||
data={taskPriorityChartData}
|
||||
height={80}
|
||||
colors={colors}
|
||||
fonts={fonts}
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* Assignee Workload */}
|
||||
{/* Issues Overview */}
|
||||
<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>
|
||||
</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>
|
||||
<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}>
|
||||
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
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';
|
||||
|
||||
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;
|
||||
|
||||
|
||||
|
||||
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