Skip to content

Commit fc57c78

Browse files
Refactor Active and Reports components for improved layout and functionality. Adjust footer padding in Active component and update margin styles in statCard. Enhance Reports component by adding filtering options, improving student risk assessment logic, and refining the Chip component for better user interaction. Update attendance display to handle cases with no sessions and sort student statistics by risk level.
1 parent 6702269 commit fc57c78

2 files changed

Lines changed: 162 additions & 52 deletions

File tree

app/(lecturer)/active.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ export default function Active() {
236236
}}
237237
/>
238238

239-
<View style={[styles.footer, { paddingBottom: spacing.lg + insets.bottom }]}>
239+
<View style={[styles.footer, { paddingBottom: spacing.sm }]}>
240240
<Button title="End Session" variant="danger" onPress={() => router.push('/(lecturer)/end')} />
241241
</View>
242242
</View>
@@ -253,7 +253,7 @@ const styles = StyleSheet.create({
253253
liveRoom: { color: 'rgba(255,255,255,0.78)', fontSize: 13, fontWeight: '600', marginTop: 4 },
254254
statCard: {
255255
marginHorizontal: spacing.lg,
256-
marginTop: -spacing.xl,
256+
marginTop: spacing.md,
257257
backgroundColor: colors.white,
258258
borderRadius: radius.lg,
259259
padding: spacing.lg,

app/(lecturer)/reports.tsx

Lines changed: 160 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { Ionicons } from '@expo/vector-icons';
12
import { useFocusEffect, useRouter } from 'expo-router';
23
import { useCallback, useState } from 'react';
3-
import { ScrollView, StyleSheet, Text, View } from 'react-native';
4+
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
45
import { useSafeAreaInsets } from 'react-native-safe-area-context';
6+
import Svg, { Circle as SvgCircle } from 'react-native-svg';
57
import { GreenHeader } from '../../src/components/GreenHeader';
6-
import { Body, Button, Card, Pill } from '../../src/components/UI';
7-
import { colors, spacing } from '../../src/theme';
8+
import { Body, Button, Card, Caption, Pill } from '../../src/components/UI';
9+
import { colors, radius, shadows, spacing } from '../../src/theme';
810
import { repo } from '../../src/data/repo';
911
import { AttendanceRecord, ClassUnit, Session, User } from '../../src/data/types';
1012
import { buildOverviewCsv, shareCsv } from '../../src/lib/export';
@@ -54,15 +56,23 @@ export default function Reports() {
5456
const unitIds = selectedUnit === 'all' ? units.map(u => u.id) : [selectedUnit];
5557
const relevantUnits = units.filter(u => unitIds.includes(u.id));
5658
const studentIds = Array.from(new Set(relevantUnits.flatMap(u => u.enrolledStudentIds)));
57-
return studentIds.map(sid => {
58-
const su = users.find(x => x.id === sid);
59-
const sessIds = filteredSessions.filter(s => unitIds.includes(s.unitId)).map(s => s.id);
60-
const attended = records.filter(r => r.studentId === sid && sessIds.includes(r.sessionId)).length;
61-
const tot = sessIds.length;
62-
const pct = tot ? Math.round((attended / tot) * 100) : 100;
63-
const risk = pct >= 75 ? 'Good' : pct >= 50 ? 'At Risk' : 'Critical';
64-
return { id: sid, name: su?.name ?? sid, attended, total: tot, pct, risk };
65-
});
59+
return studentIds
60+
.map(sid => {
61+
const su = users.find(x => x.id === sid);
62+
const sessIds = filteredSessions.filter(s => unitIds.includes(s.unitId)).map(s => s.id);
63+
const attended = records.filter(r => r.studentId === sid && sessIds.includes(r.sessionId)).length;
64+
const tot = sessIds.length;
65+
const pct = tot ? Math.round((attended / tot) * 100) : 0;
66+
const risk: 'Good' | 'At Risk' | 'Critical' | 'No data' =
67+
tot === 0 ? 'No data' : pct >= 75 ? 'Good' : pct >= 50 ? 'At Risk' : 'Critical';
68+
return { id: sid, name: su?.name ?? sid, attended, total: tot, pct, risk };
69+
})
70+
.sort((a, b) => {
71+
// Critical first, then At Risk, then Good, then No data; within group by lowest pct
72+
const order = { Critical: 0, 'At Risk': 1, Good: 2, 'No data': 3 } as const;
73+
if (order[a.risk] !== order[b.risk]) return order[a.risk] - order[b.risk];
74+
return a.pct - b.pct;
75+
});
6676
})();
6777

6878
const scrollPadBottom = spacing.lg + Math.max(insets.bottom, 12) + 56;
@@ -71,12 +81,30 @@ export default function Reports() {
7181
<View style={{ flex: 1, backgroundColor: colors.bgCanvas }}>
7282
<GreenHeader title="Reports" centered />
7383
<ScrollView contentContainerStyle={{ padding: spacing.lg, paddingBottom: scrollPadBottom, gap: 14 }}>
74-
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6 }}>
75-
<Chip label="All" active={selectedUnit === 'all'} onPress={() => setSelectedUnit('all')} />
84+
<Caption style={{ marginBottom: -6 }}>FILTER BY CLASS</Caption>
85+
<ScrollView
86+
horizontal
87+
showsHorizontalScrollIndicator={false}
88+
contentContainerStyle={{ gap: 8, paddingVertical: 2 }}
89+
>
90+
<Chip
91+
icon="apps"
92+
label="All classes"
93+
sub={`${units.length} ${units.length === 1 ? 'class' : 'classes'}`}
94+
active={selectedUnit === 'all'}
95+
onPress={() => setSelectedUnit('all')}
96+
/>
7697
{units.map(u => (
77-
<Chip key={u.id} label={u.code} active={selectedUnit === u.id} onPress={() => setSelectedUnit(u.id)} />
98+
<Chip
99+
key={u.id}
100+
icon="book"
101+
label={u.code}
102+
sub={u.name}
103+
active={selectedUnit === u.id}
104+
onPress={() => setSelectedUnit(u.id)}
105+
/>
78106
))}
79-
</View>
107+
</ScrollView>
80108

81109
<Card>
82110
<Text style={{ fontWeight: '700' }}>Per-session attendance</Text>
@@ -111,18 +139,32 @@ export default function Reports() {
111139

112140
<Card>
113141
<Text style={{ fontWeight: '700', marginBottom: 8 }}>Students</Text>
114-
{studentStats.map(s => (
115-
<View key={s.id} style={styles.row}>
116-
<View style={{ flex: 1 }}>
117-
<Text style={{ fontWeight: '600' }}>{s.name}</Text>
118-
<Body muted>{s.id}{s.attended}/{s.total}{s.pct}%</Body>
142+
{studentStats.length === 0 ? (
143+
<Body muted>No students enrolled for this filter.</Body>
144+
) : (
145+
studentStats.map(s => (
146+
<View key={s.id} style={styles.row}>
147+
<View style={{ flex: 1 }}>
148+
<Text style={{ fontWeight: '600' }} numberOfLines={1}>{s.name}</Text>
149+
<Body muted>
150+
{s.id}{s.total === 0 ? 'no sessions yet' : `${s.attended}/${s.total} · ${s.pct}%`}
151+
</Body>
152+
</View>
153+
<Pill
154+
label={s.risk}
155+
tone={
156+
s.risk === 'Good'
157+
? 'success'
158+
: s.risk === 'At Risk'
159+
? 'warn'
160+
: s.risk === 'Critical'
161+
? 'danger'
162+
: 'neutral'
163+
}
164+
/>
119165
</View>
120-
<Pill
121-
label={s.risk}
122-
tone={s.risk === 'Good' ? 'success' : s.risk === 'At Risk' ? 'warn' : 'danger'}
123-
/>
124-
</View>
125-
))}
166+
))
167+
)}
126168
</Card>
127169

128170
<Button
@@ -158,18 +200,47 @@ export default function Reports() {
158200
);
159201
}
160202

161-
function Chip({ label, active, onPress }: any) {
203+
function Chip({
204+
label,
205+
sub,
206+
icon,
207+
active,
208+
onPress,
209+
}: {
210+
label: string;
211+
sub?: string;
212+
icon?: keyof typeof Ionicons.glyphMap;
213+
active?: boolean;
214+
onPress: () => void;
215+
}) {
162216
return (
163-
<Text
217+
<Pressable
164218
onPress={onPress}
165-
style={{
166-
paddingHorizontal: 12, paddingVertical: 6, borderRadius: 999,
167-
backgroundColor: active ? colors.gold : colors.bgSubtle,
168-
color: active ? '#fff' : colors.text, fontWeight: '600', overflow: 'hidden',
169-
}}
219+
hitSlop={6}
220+
style={({ pressed }) => [
221+
styles.chip,
222+
active && styles.chipActive,
223+
!active && shadows.xs,
224+
{ opacity: pressed ? 0.85 : 1, transform: [{ scale: pressed ? 0.98 : 1 }] },
225+
]}
170226
>
171-
{label}
172-
</Text>
227+
{icon ? (
228+
<Ionicons
229+
name={icon}
230+
size={14}
231+
color={active ? colors.white : colors.green}
232+
style={{ marginRight: 6 }}
233+
/>
234+
) : null}
235+
<View>
236+
<Text style={[styles.chipLabel, active && styles.chipLabelActive]}>{label}</Text>
237+
{sub ? (
238+
<Text style={[styles.chipSub, active && styles.chipSubActive]} numberOfLines={1}>
239+
{sub}
240+
</Text>
241+
) : null}
242+
</View>
243+
</Pressable>
173244
);
174245
}
175246

@@ -183,24 +254,63 @@ function LegendDot({ color, label }: any) {
183254
}
184255

185256
function Donut({ percent }: { percent: number }) {
186-
const size = 100;
187-
const r = size / 2 - 10;
188-
const p = Math.round(percent * 100);
257+
const size = 110;
258+
const stroke = 12;
259+
const p = Math.max(0, Math.min(1, percent));
260+
const pct = Math.round(p * 100);
261+
const r = (size - stroke) / 2;
262+
const c = 2 * Math.PI * r;
189263
return (
190-
<View style={{ width: size, height: size, borderRadius: size / 2, borderWidth: 10, borderColor: colors.red, alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
191-
<View style={{
192-
position: 'absolute', top: -5, left: -5, width: size, height: size, borderRadius: size / 2,
193-
borderWidth: 10, borderColor: colors.green,
194-
borderRightColor: percent > 0.25 ? colors.green : 'transparent',
195-
borderBottomColor: percent > 0.5 ? colors.green : 'transparent',
196-
borderLeftColor: percent > 0.75 ? colors.green : 'transparent',
197-
transform: [{ rotate: `${percent * 360}deg` }],
198-
}} />
199-
<Text style={{ fontSize: 20, fontWeight: '800', color: colors.green }}>{p}%</Text>
264+
<View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
265+
<Svg width={size} height={size}>
266+
{/* Background ring */}
267+
<SvgCircle
268+
cx={size / 2}
269+
cy={size / 2}
270+
r={r}
271+
stroke={colors.red}
272+
strokeWidth={stroke}
273+
fill="none"
274+
/>
275+
{/* Foreground arc — rotate -90° to start at 12 o'clock */}
276+
<SvgCircle
277+
cx={size / 2}
278+
cy={size / 2}
279+
r={r}
280+
stroke={colors.green}
281+
strokeWidth={stroke}
282+
fill="none"
283+
strokeDasharray={`${c * p} ${c}`}
284+
strokeLinecap="round"
285+
transform={`rotate(-90 ${size / 2} ${size / 2})`}
286+
/>
287+
</Svg>
288+
<Text style={{ position: 'absolute', fontSize: 20, fontWeight: '800', color: colors.green }}>
289+
{pct}%
290+
</Text>
200291
</View>
201292
);
202293
}
203294

204295
const styles = StyleSheet.create({
205-
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: colors.border },
296+
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: colors.border, gap: spacing.md },
297+
chip: {
298+
flexDirection: 'row',
299+
alignItems: 'center',
300+
paddingVertical: 8,
301+
paddingHorizontal: 12,
302+
borderRadius: radius.md,
303+
backgroundColor: colors.white,
304+
borderWidth: 1,
305+
borderColor: colors.border,
306+
minWidth: 110,
307+
},
308+
chipActive: {
309+
backgroundColor: colors.green,
310+
borderColor: colors.green,
311+
},
312+
chipLabel: { fontWeight: '800', fontSize: 13, color: colors.text, letterSpacing: 0.2 },
313+
chipLabelActive: { color: colors.white },
314+
chipSub: { fontSize: 11, color: colors.textMuted, marginTop: 1, maxWidth: 160 },
315+
chipSubActive: { color: 'rgba(255,255,255,0.85)' },
206316
});

0 commit comments

Comments
 (0)