Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion guard_app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions guard_app/src/api/availability.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import http from '../lib/http';

export interface AvailabilityData {
Expand Down
2 changes: 2 additions & 0 deletions guard_app/src/api/shifts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

// src/api/shifts.ts
import http from '../lib/http';

Expand Down
6 changes: 5 additions & 1 deletion guard_app/src/navigation/AppNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';

import AppTabs from './AppTabs';
import CertificatesScreen from '../screen/CertificatesScreen';
import EditProfileScreen from '../screen/EditProfileScreen';
import LoginScreen from '../screen/loginscreen';
import MessagesScreen from '../screen/MessagesScreen';
Expand All @@ -9,7 +10,7 @@
import ShiftDetailsScreen from '../screen/ShiftDetailsScreen';
import SignupScreen from '../screen/signupscreen';
import SplashScreen from '../screen/SplashScreen';
import DocumentsScreen from '../screen/DocumentsScreen';

Check warning on line 13 in guard_app/src/navigation/AppNavigator.tsx

View workflow job for this annotation

GitHub Actions / ESLint & Prettier

`../screen/DocumentsScreen` import should occur before import of `../screen/EditProfileScreen`

Check warning on line 13 in guard_app/src/navigation/AppNavigator.tsx

View workflow job for this annotation

GitHub Actions / ESLint & Prettier

`../screen/DocumentsScreen` import should occur before import of `../screen/EditProfileScreen`

export type RootStackParamList = {
AppTabs: undefined;
Expand All @@ -21,8 +22,8 @@
Messages: undefined;

Notifications: undefined;
Documents: { docType?: string } | undefined;
Certificates: undefined;
ShiftDetails: { shift: any; refresh?: () => void };

Check warning on line 26 in guard_app/src/navigation/AppNavigator.tsx

View workflow job for this annotation

GitHub Actions / ESLint & Prettier

Unexpected any. Specify a different type

Check warning on line 26 in guard_app/src/navigation/AppNavigator.tsx

View workflow job for this annotation

GitHub Actions / ESLint & Prettier

Unexpected any. Specify a different type
};

const Stack = createNativeStackNavigator<RootStackParamList>();
Expand Down Expand Up @@ -56,9 +57,12 @@
options={{ headerShown: false }}
/>
<Stack.Screen
name="Certificates"
component={CertificatesScreen}
options={{ headerShown: true, title: 'Certificates' }}
name="ShiftDetails"

Check failure on line 63 in guard_app/src/navigation/AppNavigator.tsx

View workflow job for this annotation

GitHub Actions / ESLint & Prettier

No duplicate props allowed

Check failure on line 63 in guard_app/src/navigation/AppNavigator.tsx

View workflow job for this annotation

GitHub Actions / ESLint & Prettier

No duplicate props allowed
component={ShiftDetailsScreen}

Check failure on line 64 in guard_app/src/navigation/AppNavigator.tsx

View workflow job for this annotation

GitHub Actions / ESLint & Prettier

No duplicate props allowed

Check failure on line 64 in guard_app/src/navigation/AppNavigator.tsx

View workflow job for this annotation

GitHub Actions / ESLint & Prettier

No duplicate props allowed
options={{ headerShown: true, title: 'Shift Details' }}

Check failure on line 65 in guard_app/src/navigation/AppNavigator.tsx

View workflow job for this annotation

GitHub Actions / ESLint & Prettier

No duplicate props allowed

Check failure on line 65 in guard_app/src/navigation/AppNavigator.tsx

View workflow job for this annotation

GitHub Actions / ESLint & Prettier

No duplicate props allowed
/>
</Stack.Navigator>
);
Expand Down
197 changes: 197 additions & 0 deletions guard_app/src/screen/CertificatesScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/* eslint-disable react-native/no-inline-styles */
/* eslint-disable @typescript-eslint/no-explicit-any */

import React, { useMemo, useState } from 'react';
import { View, Text, FlatList, Pressable, Modal, TouchableOpacity } from 'react-native';

type DocItem = {
id: string;
name: string;
uploadedAt: string;
};

const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
const formatDMY = (d?: Date) => {
if (!d) return '—';
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
return `${day}/${month}/${year}`;
};

function computeStatus(expiry?: Date) {
if (!expiry) return 'Valid'; // placeholder if backend not ready
const today = startOfDay(new Date());
const exp = startOfDay(expiry);

if (exp < today) return 'Expired';

const msDay = 24 * 60 * 60 * 1000;
const daysLeft = Math.ceil((exp.getTime() - today.getTime()) / msDay);

if (daysLeft <= 30) return 'Expiring';
return 'Valid';
}

export default function CertificatesScreen() {
const docs: DocItem[] = useMemo(
() => [
{ id: '1', name: 'Security License', uploadedAt: '20/01/2026' },
{ id: '2', name: 'CPR', uploadedAt: '18/01/2026' },
{ id: '3', name: 'First Aid', uploadedAt: '10/01/2026' },
],
[],
);

const [expiryDates, setExpiryDates] = useState<Record<string, Date | undefined>>({});
const [pickerDocId, setPickerDocId] = useState<string | null>(null);
const [warningDocId, setWarningDocId] = useState<string | null>(null);

const [isPickerOpen, setIsPickerOpen] = useState(false);

const makeNextDates = (days = 365) => {
const arr: Date[] = [];
const base = new Date();
base.setHours(0, 0, 0, 0);
for (let i = 0; i < days; i++) {
const d = new Date(base);
d.setDate(base.getDate() + i);
arr.push(d);
}
return arr;
};

const dateOptions = useMemo(() => makeNextDates(365), []);

const renderItem = ({ item }: { item: DocItem }) => {
const expiry = expiryDates[item.id];
const status = computeStatus(expiry);

return (
<View
style={{
padding: 14,
borderRadius: 14,
borderWidth: 1,
borderColor: '#e2e2e2',
backgroundColor: 'white',
marginBottom: 12,
}}
>
<View
style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}
>
<Text style={{ fontSize: 16, fontWeight: '700' }}>{item.name}</Text>
<View
style={{
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 999,
borderWidth: 1,
borderColor:
status === 'Expired' ? '#ffb3b3' : status === 'Expiring' ? '#ffe3a3' : '#cfcfcf',
backgroundColor:
status === 'Expired' ? '#ffecec' : status === 'Expiring' ? '#fff7df' : '#f3f3f3',
}}
>
<Text style={{ fontSize: 12, fontWeight: '700' }}>{status}</Text>
</View>
</View>

<View style={{ marginTop: 8 }}>
<Text style={{ opacity: 0.75 }}>Upload date: {item.uploadedAt}</Text>
<Text style={{ opacity: 0.75 }}>Expiry date: {formatDMY(expiry)}</Text>
</View>

<Pressable
onPress={() => {
setPickerDocId(item.id);
setWarningDocId(null);
setIsPickerOpen(true);
}}
style={{
marginTop: 10,
paddingVertical: 10,
borderRadius: 12,
borderWidth: 1,
borderColor: '#e2e2e2',
alignItems: 'center',
}}
>
<Text style={{ fontWeight: '700' }}>Set Expiry Date</Text>
</Pressable>

{warningDocId === item.id ? (
<Text style={{ marginTop: 8, color: '#c00', fontSize: 12 }}>
Expiry date cannot be before today.
</Text>
) : null}
</View>
);
};

return (
<View style={{ flex: 1, padding: 16, backgroundColor: '#fafafa' }}>
<Text style={{ fontSize: 20, fontWeight: '800', marginBottom: 12 }}>Certificates</Text>

{docs.length === 0 ? (
<View style={{ padding: 16, borderRadius: 14, borderWidth: 1, borderColor: '#e2e2e2' }}>
<Text style={{ fontWeight: '800' }}>No documents</Text>
<Text style={{ marginTop: 6, opacity: 0.8 }}>
Upload a document to see it listed here with expiry details.
</Text>
</View>
) : (
<FlatList data={docs} keyExtractor={(x) => x.id} renderItem={renderItem} />
)}

<Modal visible={isPickerOpen} transparent animationType="slide">
<View style={{ flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(0,0,0,0.35)' }}>
<View
style={{
backgroundColor: 'white',
padding: 16,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
maxHeight: '60%',
}}
>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
}}
>
<Text style={{ fontSize: 16, fontWeight: '800' }}>Select Expiry Date</Text>
<TouchableOpacity onPress={() => setIsPickerOpen(false)}>
<Text style={{ fontWeight: '800' }}>Close</Text>
</TouchableOpacity>
</View>

<FlatList
data={dateOptions}
keyExtractor={(d) => d.toISOString()}
renderItem={({ item: d }) => (
<TouchableOpacity
onPress={() => {
if (!pickerDocId) return;
// Since we only show today+ future dates, it's always valid.
setExpiryDates((prev) => ({ ...prev, [pickerDocId]: d }));
setWarningDocId(null);
setIsPickerOpen(false);
setPickerDocId(null);
}}
style={{ paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#eee' }}
>
<Text>{formatDMY(d)}</Text>
</TouchableOpacity>
)}
/>
</View>
</View>
</Modal>
</View>
);
}
10 changes: 8 additions & 2 deletions guard_app/src/screen/ProfileScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-native/no-inline-styles */

import { Ionicons } from '@expo/vector-icons';
import { useEffect, useState } from 'react';
import {
Expand Down Expand Up @@ -195,7 +196,12 @@ export default function ProfileScreen({ navigation, route }: any) {
)}

{/* Certifications */}
<View style={styles.card}>

<TouchableOpacity
activeOpacity={0.85}
style={styles.card}
onPress={() => navigation.navigate('Certificates')}
>
<Text style={styles.cardTitle}>Certifications</Text>
<View style={styles.badgesRow}>
{['Security License', 'CPR', 'First Aid'].map((badge) => (
Expand All @@ -208,7 +214,7 @@ export default function ProfileScreen({ navigation, route }: any) {
</Pressable>
))}
</View>
</View>
</TouchableOpacity>
</ScrollView>
</SafeAreaView>
);
Expand Down
8 changes: 8 additions & 0 deletions guard_app/src/screen/ShiftsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/* eslint-disable react-native/no-inline-styles */
/* eslint-disable @typescript-eslint/no-explicit-any */

// ShiftScreen.tsx
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import { useFocusEffect } from '@react-navigation/native';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import React, { useCallback, useEffect, useState } from 'react';
import {
Expand Down
Loading