Centralized_Rreporting_System/src/modules/zohoProjects/screens/ZohoProjectsDashboardScreen.tsx
2025-09-05 18:09:40 +05:30

521 lines
20 KiB
TypeScript

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 <LoadingSpinner />;
}
if (error) {
return <ErrorState message={error} onRetry={() => dispatch(fetchZohoProjects() as any)} />;
}
return (
<Container>
<ScrollView
style={styles.container}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
>
<View style={styles.header}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>Zoho Projects</Text>
<Icon name="insights" size={24} color={colors.primary} />
</View>
<View style={styles.content}>
{/* Sprint Header */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Active Sprint</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>
</View>
<Chip label={mock.scopeChange > 0 ? 'Scope +' : 'Scope'} value={Math.abs(mock.scopeChange)} dot={mock.scopeChange > 0 ? '#F59E0B' : '#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" />
</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 */}
<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>
</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>
</View>
</View>
{/* Timesheets & Aging */}
<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" />;
})()}
</View>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Backlog Aging</Text>
<MiniBars data={mock.backlogAging} color="#6366F1" max={Math.max(...mock.backlogAging)} />
<View style={styles.legendRow}>
{['0-7d', '8-14d', '15-30d', '30+d'].map(label => (
<View key={label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: '#6366F1' }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{label}</Text>
</View>
))}
</View>
</View>
</View>
{/* Assignee Workload */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Assignee Workload</Text>
{mock.assigneeLoad.map(a => (
<ProgressRow key={a.name} label={a.name} value={a.pct} color="#3AA0FF" />
))}
</View>
{/* Progress: On-time, Quality, Utilization */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Operational Health</Text>
<ProgressRow label="On-time delivery" value={mock.onTimePct} color="#3AA0FF" />
<ProgressRow label="Quality score" value={mock.qualityScore} color="#10B981" />
<ProgressRow label="Resource utilization" value={mock.utilizationPct} color="#F59E0B" />
</View>
{/* Status distribution */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Status Distribution</Text>
<StackedBar segments={mock.statusDist} total={mock.statusDist.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.statusDist.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</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}>
<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" />
</View>
</View>
{/* Lists: Risks and Top Clients */}
<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>
</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>
</View>
))}
</View>
</View>
{/* Milestones Progress */}
<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>
))}
</View>
{/* Team Capacity */}
<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" />
))}
</View>
</View>
</ScrollView>
</Container>
);
};
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 (
<View style={styles.kpiCard}>
<Text style={[styles.kpiLabel, { color, fontFamily: fonts.regular }]}>{label}</Text>
<View style={styles.kpiValueRow}>
<Text style={{ color, fontSize: 22, fontFamily: fonts.bold }}>{value}</Text>
<View style={{ width: 10, height: 10, borderRadius: 5, backgroundColor: accent }} />
</View>
</View>
);
};
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();
return (
<View style={styles.progressRow}>
<View style={styles.progressLabelWrap}>
<Text style={{ fontSize: 12, fontFamily: fonts.regular }}>{label}</Text>
<Text style={{ fontSize: 12, fontFamily: fonts.medium }}>{value}%</Text>
</View>
<View style={styles.progressTrack}>
<View style={[styles.progressFill, { width: `${value}%`, backgroundColor: color }]} />
</View>
</View>
);
};
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 { fonts, colors } = useTheme();
return (
<View style={styles.chip}>
<View style={[styles.chipDot, { backgroundColor: dot }]} />
<Text style={{ fontSize: 12, fontFamily: fonts.regular, color: colors.text }}>{label}: </Text>
<Text style={{ fontSize: 12, fontFamily: fonts.medium, color: colors.text }}>{value}</Text>
</View>
);
};