import React, { useEffect, useMemo, 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 type { RootState } from '@/store/store';
const ZohoProjectsDashboardScreen: React.FC = () => {
const { colors, fonts } = useTheme();
const dispatch = useDispatch();
const [refreshing, setRefreshing] = useState(false);
const { projects, loading, error } = useSelector((s: RootState) => s.zohoProjects);
useEffect(() => {
// Fetch projects on mount
// Guard: avoid duplicate fetch while loading
if (!loading) {
// @ts-ignore
dispatch(fetchZohoProjects());
}
}, [dispatch]);
const handleRefresh = async () => {
setRefreshing(true);
// @ts-ignore
await dispatch(fetchZohoProjects());
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) {
return ;
}
if (error) {
return dispatch(fetchZohoProjects() as any)} />;
}
return (
}
>
Zoho Projects
{/* Sprint Header */}
Active Sprint
{mock.sprintName}
{mock.sprintDates}
0 ? 'Scope +' : 'Scope'} value={Math.abs(mock.scopeChange)} dot={mock.scopeChange > 0 ? '#F59E0B' : '#10B981'} />
{/* KPI Cards */}
{/* Trend: Burndown & Velocity (mini bars) */}
Sprint Trends
Burndown
Velocity
{/* Bugs & Priority Mix */}
Bugs by Severity
a + b.value, 0)} />
{mock.bugSeverityDist.map(s => (
{s.label}
))}
Tasks by Priority
a + b.value, 0)} />
{mock.priorityDist.map(s => (
{s.label}
))}
{/* Timesheets & Aging */}
Timesheets
Total: {mock.timesheets.totalHours}h
{(() => {
const billablePct = Math.round((mock.timesheets.billableHours / Math.max(1, mock.timesheets.totalHours)) * 100);
return ;
})()}
{(() => {
const non = mock.timesheets.totalHours - mock.timesheets.billableHours;
const pct = Math.round((non / Math.max(1, mock.timesheets.totalHours)) * 100);
return ;
})()}
Backlog Aging
{['0-7d', '8-14d', '15-30d', '30+d'].map(label => (
{label}
))}
{/* Assignee Workload */}
Assignee Workload
{mock.assigneeLoad.map(a => (
))}
{/* Progress: On-time, Quality, Utilization */}
Operational Health
{/* Status distribution */}
Status Distribution
a + b.value, 0)} />
{mock.statusDist.map(s => (
{s.label}
))}
{/* Kanban Snapshot */}
Kanban Snapshot
{/* Lists: Risks and Top Clients */}
Top Risks
{mock.risks.map(r => (
{r.title}
{r.impact}
))}
Top Clients
{mock.topClients.map(c => (
{c.name}
{c.projects} projects
))}
{/* Milestones Progress */}
Milestones
{mock.milestones.map(m => (
{m.name}
{m.due}
))}
{/* Team Capacity */}
Team Capacity
{mock.teams.map(t => (
))}
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
backgroundColor: '#FFFFFF',
},
title: {
fontSize: 24,
},
content: {
padding: 16,
},
kpiGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
kpiCard: {
width: '48%',
borderRadius: 12,
borderWidth: 1,
borderColor: '#E2E8F0',
backgroundColor: '#FFFFFF',
padding: 12,
marginBottom: 12,
},
kpiLabel: {
fontSize: 12,
opacity: 0.8,
},
kpiValueRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 8,
},
card: {
borderRadius: 12,
borderWidth: 1,
padding: 12,
marginTop: 12,
},
cardTitle: {
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,
},
progressLabelWrap: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 6,
},
progressTrack: {
height: 8,
borderRadius: 6,
backgroundColor: '#E5E7EB',
overflow: 'hidden',
},
progressFill: {
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,
},
badgeRow: {
flexDirection: 'row',
flexWrap: 'wrap',
},
chip: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F3F4F6',
paddingVertical: 6,
paddingHorizontal: 10,
borderRadius: 16,
marginRight: 8,
marginTop: 8,
},
chipDot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 },
col: {
flex: 1,
marginRight: 8,
},
listRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 10,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: '#E5E7EB',
},
listPrimary: {
fontSize: 14,
flex: 1,
paddingRight: 8,
},
listSecondary: {
fontSize: 12,
opacity: 0.8,
},
});
export default ZohoProjectsDashboardScreen;
// UI subcomponents (no external deps)
const KpiCard: React.FC<{ label: string; value: number | string; color: string; accent: string }> = ({ label, value, color, accent }) => {
const { fonts } = useTheme();
return (
{label}
{value}
);
};
const MiniBars: React.FC<{ data: number[]; color: string; max: number }> = ({ data, color, max }) => {
return (
{data.map((v, i) => (
))}
);
};
const ProgressRow: React.FC<{ label: string; value: number; color: string }> = ({ label, value, color }) => {
const { fonts } = useTheme();
return (
{label}
{value}%
);
};
const StackedBar: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => {
return (
{segments.map(s => (
))}
);
};
const Chip: React.FC<{ label: string; value: number; dot: string }> = ({ label, value, dot }) => {
const { fonts, colors } = useTheme();
return (
{label}:
{value}
);
};