From eca2198a2d29de40e61c09cf7eaa363a5c951ed7 Mon Sep 17 00:00:00 2001 From: SanjayaGrg Date: Fri, 24 Apr 2026 00:55:50 +1000 Subject: [PATCH 1/2] Add attendance history in shift details modal --- guard_app/src/screen/SettingsScreen.tsx | 1 + guard_app/src/screen/ShiftDetailsScreen.tsx | 206 +++++++++++++++++--- guard_app/src/screen/ShiftsScreen.tsx | 36 +++- 3 files changed, 216 insertions(+), 27 deletions(-) diff --git a/guard_app/src/screen/SettingsScreen.tsx b/guard_app/src/screen/SettingsScreen.tsx index 227bb8399..b7179abca 100644 --- a/guard_app/src/screen/SettingsScreen.tsx +++ b/guard_app/src/screen/SettingsScreen.tsx @@ -107,6 +107,7 @@ export default function SettingsScreen() { try { await LocalStorage.removeToken(); // clear auth tokens await LocalStorage.removePushToken(); // clear push tokens + await LocalStorage.clearAll(); await AsyncStorage.removeItem(PROFILE_STORAGE_KEY); // clear profile data navigation.reset({ index: 0, diff --git a/guard_app/src/screen/ShiftDetailsScreen.tsx b/guard_app/src/screen/ShiftDetailsScreen.tsx index 33718320f..cefcbf2a1 100644 --- a/guard_app/src/screen/ShiftDetailsScreen.tsx +++ b/guard_app/src/screen/ShiftDetailsScreen.tsx @@ -6,7 +6,7 @@ import { AxiosError } from 'axios'; import React, { useEffect, useState } from 'react'; import { Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -import { checkIn, checkOut } from '../api/attendance'; +// import { checkIn, checkOut } from '../api/attendance'; //uncomment when API is ready import LocationVerificationModal from '../components/LocationVerificationModal'; import { getAttendanceForShift, setAttendanceForShift } from '../lib/attendancestore'; import { useAppTheme } from '../theme'; @@ -24,6 +24,11 @@ type AttendanceState = { checkOutTime?: string; }; +type AttendanceHistoryItem = { + label: string; + value: string; +}; + function StatusBadge({ status, color }: { status: string; color: string }) { return {status.toUpperCase()}; } @@ -49,6 +54,10 @@ export default function ShiftDetailsScreen() { const [modalVisible, setModalVisible] = useState(false); const [actionType, setActionType] = useState<'check-in' | 'check-out'>('check-in'); + useEffect(() => { + console.log('Shift details:', shift); + }, [shift]); + useEffect(() => { (async () => { const a = await getAttendanceForShift(shift._id); @@ -66,28 +75,85 @@ export default function ShiftDetailsScreen() { longitude: number; timestamp: number; }) => { + // Uncomment this section when the backend API is ready to handle location-based check-in/check-out. + // try { + // setModalVisible(false); + // console.log('Location:', loc); + + // if (actionType === 'check-in') { + // const res = await checkIn(shift._id, loc); + + // const next: AttendanceState = normalizeAttendance({ + // checkInTime: res.attendance?.checkInTime, + // checkOutTime: undefined, + // }); + + // await setAttendanceForShift(shift._id, next); + // setAttendance(next); + + // Alert.alert('Success', 'Checked in successfully ✅'); + // } else { + // const res = await checkOut(shift._id, loc); + + // const next: AttendanceState = normalizeAttendance({ + // checkInTime: res.attendance?.checkInTime, + // checkOutTime: res.attendance?.checkOutTime, + // }); + + // await setAttendanceForShift(shift._id, next); + // setAttendance(next); + + // Alert.alert('Success', 'Checked out successfully ✅'); + // } + + // if (route.params.refresh) route.params.refresh(); + // } catch (e: unknown) { + // setModalVisible(false); + // let msg; + // if (e instanceof AxiosError) { + // msg = e?.response?.data?.message ?? e?.message ?? 'Action failed'; + // } else { + // msg = 'Action failed'; + // } + + // if (typeof msg === 'string' && msg.toLowerCase().includes('location')) { + // Alert.alert('Location Error', 'You are not at the shift location ❌'); + // } else { + // Alert.alert('Error', msg); + // } + // } + + // locally update attendance (e.g. due to location verification issues in API) try { setModalVisible(false); + console.log('Location:', loc); - if (actionType === 'check-in') { - const res = await checkIn(shift._id, loc); + const now = new Date().toISOString(); + + // checking in/out should only be allowed during shift time - this is a fallback check in case the API doesn't enforce it properly + const nowD = new Date(); + const start = new Date(`${shift.date}T${shift.startTime}`); + const end = new Date(`${shift.date}T${shift.endTime}`); + if (actionType === 'check-in' && (nowD < start || nowD > end)) { + Alert.alert('Not allowed', 'Check-in is only allowed during shift time.'); + return; + } - const next: AttendanceState = normalizeAttendance({ - checkInTime: res.attendance?.checkInTime, - checkOutTime: undefined, - }); + if (actionType === 'check-in') { + const next: AttendanceState = { + checkInTime: now, + checkOutTime: attendance?.checkOutTime, + }; await setAttendanceForShift(shift._id, next); setAttendance(next); Alert.alert('Success', 'Checked in successfully ✅'); } else { - const res = await checkOut(shift._id, loc); - - const next: AttendanceState = normalizeAttendance({ - checkInTime: res.attendance?.checkInTime, - checkOutTime: res.attendance?.checkOutTime, - }); + const next: AttendanceState = { + checkInTime: attendance?.checkInTime, + checkOutTime: now, + }; await setAttendanceForShift(shift._id, next); setAttendance(next); @@ -96,20 +162,9 @@ export default function ShiftDetailsScreen() { } if (route.params.refresh) route.params.refresh(); - } catch (e: unknown) { + } catch { setModalVisible(false); - let msg; - if (e instanceof AxiosError) { - msg = e?.response?.data?.message ?? e?.message ?? 'Action failed'; - } else { - msg = 'Action failed'; - } - - if (typeof msg === 'string' && msg.toLowerCase().includes('location')) { - Alert.alert('Location Error', 'You are not at the shift location ❌'); - } else { - Alert.alert('Error', msg); - } + Alert.alert('Error', 'Action failed'); } }; @@ -121,9 +176,19 @@ export default function ShiftDetailsScreen() { const hasCheckedIn = !!attendance?.checkInTime; const hasCheckedOut = !!attendance?.checkOutTime; + const shouldShowAttendanceHistory = shift.status === 'completed' || hasCheckedIn; const showCheckIn = canDoAttendance && !hasCheckedIn; const showCheckOut = canDoAttendance && hasCheckedIn && !hasCheckedOut; + const attendanceHistory: AttendanceHistoryItem[] = [ + ...(attendance?.checkInTime + ? [{ label: 'Check In', value: attendance.checkInTime }] + : []), + ...(attendance?.checkOutTime + ? [{ label: 'Check Out', value: attendance.checkOutTime }] + : []), + ]; + const handleMessageEmployer = () => { const employerId = shift.createdBy?._id; if (!employerId) { @@ -181,6 +246,47 @@ export default function ShiftDetailsScreen() { + + Location Details + + + Street + {shift.location?.street ?? 'N/A'} + + + + Suburb + {shift.location?.suburb ?? 'N/A'} + + + + State + {shift.location?.state ?? 'N/A'} + + + + Postcode + {shift.location?.postcode ?? 'N/A'} + + + { + shouldShowAttendanceHistory && ( + + Attendance History + + {attendanceHistory.length > 0 ? ( + attendanceHistory.map((item, index) => ( + + {item.label} + {item.value} + + )) + ) : ( + No attendance history available + )} + + )} + {attendance?.checkInTime && ( ✅ Checked in: {attendance.checkInTime} )} @@ -333,4 +439,52 @@ const getStyles = (colors: AppColors) => hintStrong: { fontWeight: '700', }, + section: { + marginTop: 18, + paddingTop: 16, + borderTopWidth: 1, + borderTopColor: colors.border, + }, + sectionTitle: { + fontSize: 16, + fontWeight: '700', + color: colors.text, + marginBottom: 12, + }, + infoRow: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 10, + }, + infoLabel: { + fontSize: 14, + color: colors.muted, + }, + infoValue: { + fontSize: 14, + color: colors.text, + fontWeight: '500', + maxWidth: '65%', + textAlign: 'right', + }, + historyItem: { + backgroundColor: colors.primarySoft, + borderRadius: 10, + padding: 12, + marginBottom: 10, + }, + historyLabel: { + fontSize: 13, + color: colors.muted, + marginBottom: 4, + }, + historyValue: { + fontSize: 14, + color: colors.text, + fontWeight: '700', + }, + emptyHistory: { + fontSize: 14, + color: colors.muted, + }, }); diff --git a/guard_app/src/screen/ShiftsScreen.tsx b/guard_app/src/screen/ShiftsScreen.tsx index d29295033..e8d6f2585 100644 --- a/guard_app/src/screen/ShiftsScreen.tsx +++ b/guard_app/src/screen/ShiftsScreen.tsx @@ -1,6 +1,6 @@ import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'; import { useFocusEffect } from '@react-navigation/native'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, @@ -19,6 +19,7 @@ import { import { getMe } from '../api/auth'; import { applyToShift, listShifts, myShifts, type ShiftDto } from '../api/shifts'; import { useAppTheme } from '../theme'; +import { getAttendanceForShift } from '../lib/attendancestore'; import type { AppColors } from '../theme/colors'; @@ -158,6 +159,20 @@ function ShiftDetailsModal({ }) { const s = getStyles(colors); + const [attendance, setAttendance] = useState<{ + checkInTime?: string; + checkOutTime?: string; + } | null>(null); + + useEffect(() => { + if (!shift || !visible) return; + + (async () => { + const data = await getAttendanceForShift(shift?.id); + setAttendance(data); + })(); + }, [shift, visible]); + if (!shift) return null; const status = 'status' in shift ? shift.status : 'Completed'; @@ -227,6 +242,25 @@ function ShiftDetailsModal({ + {(attendance?.checkInTime || attendance?.checkOutTime) && ( + + Attendance History + + {attendance?.checkInTime && ( + + Check In: + {attendance.checkInTime} + + )} + + {attendance?.checkOutTime && ( + + Check Out: + {attendance.checkOutTime} + + )} + + )} From dabd0af0739a274081ed854810a20b71772e535f Mon Sep 17 00:00:00 2001 From: SanjayaGrg Date: Fri, 24 Apr 2026 11:05:25 +1000 Subject: [PATCH 2/2] Lint fixes --- guard_app/src/screen/ShiftDetailsScreen.tsx | 41 +++++++++------------ 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/guard_app/src/screen/ShiftDetailsScreen.tsx b/guard_app/src/screen/ShiftDetailsScreen.tsx index cefcbf2a1..c789538ab 100644 --- a/guard_app/src/screen/ShiftDetailsScreen.tsx +++ b/guard_app/src/screen/ShiftDetailsScreen.tsx @@ -181,12 +181,8 @@ export default function ShiftDetailsScreen() { const showCheckOut = canDoAttendance && hasCheckedIn && !hasCheckedOut; const attendanceHistory: AttendanceHistoryItem[] = [ - ...(attendance?.checkInTime - ? [{ label: 'Check In', value: attendance.checkInTime }] - : []), - ...(attendance?.checkOutTime - ? [{ label: 'Check Out', value: attendance.checkOutTime }] - : []), + ...(attendance?.checkInTime ? [{ label: 'Check In', value: attendance.checkInTime }] : []), + ...(attendance?.checkOutTime ? [{ label: 'Check Out', value: attendance.checkOutTime }] : []), ]; const handleMessageEmployer = () => { @@ -269,23 +265,22 @@ export default function ShiftDetailsScreen() { {shift.location?.postcode ?? 'N/A'} - { - shouldShowAttendanceHistory && ( - - Attendance History - - {attendanceHistory.length > 0 ? ( - attendanceHistory.map((item, index) => ( - - {item.label} - {item.value} - - )) - ) : ( - No attendance history available - )} - - )} + {shouldShowAttendanceHistory && ( + + Attendance History + + {attendanceHistory.length > 0 ? ( + attendanceHistory.map((item, index) => ( + + {item.label} + {item.value} + + )) + ) : ( + No attendance history available + )} + + )} {attendance?.checkInTime && ( ✅ Checked in: {attendance.checkInTime}