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