521 lines
20 KiB
TypeScript
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>
|
|
);
|
|
};
|
|
|
|
|