Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Com 3854 #696

Merged
merged 8 commits into from
Jan 2, 2025
Merged
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
5 changes: 5 additions & 0 deletions src/api/attendanceSheets.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,11 @@ export default {
const headers = new AxiosHeaders({ 'Content-Type': 'multipart/form-data' });
await axiosLogged.post(`${baseURL}/attendancesheets`, data, { headers });
},
sign: async (attendanceSheetId: string, data: FormDataType): Promise<void> => {
const baseURL = await Environment.getBaseUrl();
const headers = new AxiosHeaders({ 'Content-Type': 'multipart/form-data' });
await axiosLogged.put(`${baseURL}/attendancesheets/${attendanceSheetId}/signature`, data, { headers });
},
delete: async (attendanceSheetId: string): Promise<void> => {
const baseURL = await Environment.getBaseUrl();
await axiosLogged.delete(`${baseURL}/attendancesheets/${attendanceSheetId}`);
76 changes: 51 additions & 25 deletions src/components/AttendanceEndScreen/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { View, Text, Image } from 'react-native';
import { View, Text, Image, ScrollView } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { MaterialIcons } from '@expo/vector-icons';
import { IS_WEB, TRAINER } from '../../core/data/constants';
import NiPrimaryButton from '../form/PrimaryButton';
import { PINK } from '../../styles/colors';
import styles from './styles';
@@ -9,31 +10,56 @@ interface AttendanceEndScreenProps {
traineeName: string,
failUpload: boolean,
goToNextScreen: () => void,
mode?: string
}

const AttendanceEndScreen = ({ traineeName, failUpload, goToNextScreen }: AttendanceEndScreenProps) => (
<SafeAreaView style={styles.safeArea} edges={['top']}>
{failUpload
? <View style={styles.errorContainer}>
<Text style={styles.title}>Echec de l&apos;envoi de la demande à {traineeName}</Text>
<MaterialIcons style={styles.icon} size={200} name={'warning'} color={PINK[500]} />
<Text style={styles.text}>Veuillez réitérer votre demande</Text>
const AttendanceEndScreen = ({ traineeName, failUpload, goToNextScreen, mode = TRAINER }: AttendanceEndScreenProps) => {
const renderFailMessage = (text: string) =>
<ScrollView contentContainerStyle={styles.errorContainer} showsVerticalScrollIndicator={IS_WEB}>
<Text style={styles.title}>{text}</Text>
<MaterialIcons style={styles.icon} size={200} name={'warning'} color={PINK[500]} />
<Text style={styles.text}>Veuillez réitérer votre demande</Text>
</ScrollView>;

return (
<SafeAreaView style={styles.safeArea} edges={['top']}>
{mode === TRAINER
? <>
{failUpload
? renderFailMessage(`Echec de l'envoi de la demande à ${traineeName}`)
: <ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={IS_WEB}>
<View>
<Text style={styles.title}>Une demande d&apos;émargement a été envoyée à {traineeName}</Text>
<Text style={styles.text}>
Elle est disponible sur la page de la formation sur son application mobile
</Text>
</View>
<Image source={require('../../../assets/images/aux_fierte.webp')} style={styles.image} />
<Text style={styles.text}>
N&apos;oubliez pas de reporter les émargements dans le tableau prévu à cet effet sur l&apos;application
web Compani
</Text>
</ScrollView>
}
</>
: <>
{failUpload
? renderFailMessage('Echec lors de l\'envoi de votre signature')
: <ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={IS_WEB}>
<Text style={styles.title}>Merci d&apos;avoir émargé les créneaux</Text>
<Image source={require('../../../assets/images/aux_fierte.webp')} style={styles.image} />
<Text style={styles.text}>
Vous pouvez désormais retourner sur votre formation pour émarger d&apos;autres créneaux ou poursuivre
votre apprentissage
</Text>
</ScrollView>
}
</>
}
<View style={styles.footer}>
<NiPrimaryButton caption={'Terminer'} onPress={goToNextScreen} />
</View>
: <>
<View style={styles.upperContainer}>
<Text style={styles.title}>Une demande d&apos;émargement a été envoyée à {traineeName}</Text>
</View>
<Text style={styles.text}>Elle est disponible sur la page de la formation sur son application mobile</Text>
<Image source={require('../../../assets/images/aux_fierte.webp')} style={styles.image} />
<Text style={styles.text}>
N&apos;oubliez pas de reporter les émargements dans le tableau prévu à cet effet sur l&apos;application
web Compani
</Text>
</>
}
<View style={styles.footer}>
<NiPrimaryButton caption={'Terminer'} onPress={goToNextScreen} />
</View>
</SafeAreaView>
);
</SafeAreaView>
);
};
export default AttendanceEndScreen;
10 changes: 5 additions & 5 deletions src/components/AttendanceEndScreen/styles.ts
Original file line number Diff line number Diff line change
@@ -5,9 +5,8 @@ import { PINK } from '../../styles/colors';

const styles = StyleSheet.create({
safeArea: {
flexGrow: 1,
flex: 1,
marginHorizontal: MARGIN.LG,
justifyContent: 'space-between',
},
title: {
...FIRA_SANS_BOLD.LG,
@@ -36,12 +35,13 @@ const styles = StyleSheet.create({
marginHorizontal: MARGIN.MD,
},
errorContainer: {
flex: 1,
flexGrow: 1,
justifyContent: 'space-between',
alignItems: 'center',
},
upperContainer: {
justifyContent: 'flex-start',
container: {
flexGrow: 1,
justifyContent: 'space-between',
},
});

3 changes: 2 additions & 1 deletion src/components/AttendanceSheetSelectionForm/index.tsx
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import { ErrorStateType } from '../../reducers/error';
import FeatherButton from '../icons/FeatherButton';
import { ICON } from '../../styles/metrics';
import { GREY } from '../../styles/colors';
import { IS_WEB } from '../../core/data/constants';

interface AttendanceSheetSelectionFormProps {
title: string,
@@ -41,7 +42,7 @@ const AttendanceSheetSelectionForm = ({
<View style={styles.header}>
<FeatherButton name='arrow-left' onPress={() => navigation.goBack()} size={ICON.MD} color={GREY[600]} />
</View>
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
<ScrollView style={styles.container} showsVerticalScrollIndicator={IS_WEB}>
<Text style={styles.title}>{title}</Text>
{children}
</ScrollView>
3 changes: 2 additions & 1 deletion src/components/AttendanceSheetSummary/index.tsx
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import NiPrimaryButton from '../form/PrimaryButton';
import FeatherButton from '../icons/FeatherButton';
import { ICON } from '../../styles/metrics';
import { GREY } from '../../styles/colors';
import { IS_WEB } from '../../core/data/constants';
import styles from './styles';

interface AttendanceSheetSummaryProps {
@@ -55,7 +56,7 @@ const AttendanceSheetSummary = ({
<View style={styles.header}>
<FeatherButton name='arrow-left' onPress={() => navigation.goBack()} size={ICON.MD} color={GREY[600]} />
</View>
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={IS_WEB}>
<Text style={styles.title}>Emargements pour {traineeName}</Text>
<MultipleCheckboxList optionsGroups={slotsOptions} disabled groupTitles={stepsName} checkedList={checkedList} />
<Image source={{ uri: signature }} style={styles.image} />
2 changes: 1 addition & 1 deletion src/components/AttendanceSignatureContainer/index.tsx
Original file line number Diff line number Diff line change
@@ -116,7 +116,7 @@ const AttendanceSignatureContainer = ({
<iframe
ref={iframeRef}
src={`data:text/html,${encodeURIComponent(htmlContent)}`}
style={{ width: '50%', height: 'auto', aspectRatio: 1, border: '1px solid #ccc' }}
style={{ width: '30%', height: 'auto', aspectRatio: 1, border: '1px solid #ccc' }}
/>
</View>
: <View style={styles.webviewContainer}>
48 changes: 34 additions & 14 deletions src/components/learnerPendingActions/AttendanceSheetCell/index.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,51 @@
import { Text, View, TouchableOpacity } from 'react-native';
import { Feather } from '@expo/vector-icons';
import groupBy from 'lodash/groupBy';
import { useNavigation } from '@react-navigation/native';
import CompaniDate from '../../../core/helpers/dates/companiDates';
import { AttendanceSheetType } from '../../../types/AttendanceSheetTypes';
import Shadow from '../../design/Shadow';
import { DD_MM_YYYY } from '../../../core/data/constants';
import { ICON } from '../../../styles/metrics';
import { GREY } from '../../../styles/colors';
import styles from './styles';
import { useGetCourse, useSetGroupedSlotsToBeSigned } from '../../../store/attendanceSheets/hooks';
import { SlotType } from '../../../types/CourseTypes';

interface AttendanceSheetCellProps {
attendanceSheet: AttendanceSheetType,
}

const AttendanceSheetCell = ({ attendanceSheet }: AttendanceSheetCellProps) => (
<View style={styles.container}>
<TouchableOpacity style={styles.iconContainer}>
<View style={styles.icon}>
<Feather name='pen-tool' size={ICON.LG} color={GREY[700]} />
</View>
<Shadow customStyle={styles.shadow} />
</TouchableOpacity>
<Text style={styles.AttendanceSheetName} lineBreakMode={'tail'} numberOfLines={2}>
const AttendanceSheetCell = ({ attendanceSheet }: AttendanceSheetCellProps) => {
const navigation = useNavigation();
const course = useGetCourse();
const setGroupedSlotsToBeSigned = useSetGroupedSlotsToBeSigned();

const goToSignature = () => {
const groupedSlots = groupBy(attendanceSheet.slots, 'step');
const groupedSlotsToBeSigned = course?.subProgram.steps.reduce<Record<string, SlotType[]>>((acc, step) => {
if (groupedSlots[step._id]) acc[step.name] = groupedSlots[step._id];
return acc;
}, {});

setGroupedSlotsToBeSigned(groupedSlotsToBeSigned!);
navigation.navigate('UpdateAttendanceSheet', { attendanceSheetId: attendanceSheet._id });
};
return (
<View style={styles.container}>
<TouchableOpacity style={styles.iconContainer} onPress={goToSignature}>
<View style={styles.icon}>
<Feather name='pen-tool' size={ICON.LG} color={GREY[700]} />
</View>
<Shadow customStyle={styles.shadow} />
</TouchableOpacity>
<Text style={styles.AttendanceSheetName} lineBreakMode={'tail'} numberOfLines={2}>
À signer {[
...new Set(attendanceSheet.slots!.map(slot => CompaniDate(slot.startDate).format(DD_MM_YYYY)))].join(', ')
}
</Text>
</View>
);
...new Set(attendanceSheet.slots!.map(slot => CompaniDate(slot.startDate).format(DD_MM_YYYY)))].join(', ')
}
</Text>
</View>
);
};

export default AttendanceSheetCell;
12 changes: 7 additions & 5 deletions src/components/steps/LiveCellInfoModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// @ts-nocheck

import { View, Text, FlatList } from 'react-native';
import { View, Text, FlatList, ScrollView } from 'react-native';
import CompaniDate from '../../../core/helpers/dates/companiDates';
import { ascendingSort } from '../../../core/helpers/dates/utils';
import { DD_MM_YYYY } from '../../../core/data/constants';
import { DD_MM_YYYY, IS_WEB } from '../../../core/data/constants';
import NiModal from '../../Modal';
import FeatherButton from '../../icons/FeatherButton';
import { ICON } from '../../../styles/metrics';
@@ -42,9 +42,11 @@ const LiveCellInfoModal = ({ visible, title, stepSlots, onRequestClose }: LiveCe
<FeatherButton name='x-circle' onPress={onRequestClose} size={ICON.LG} color={GREY[500]}
style={styles.closeButton} />
</View>
<FlatList ItemSeparatorComponent={() => <View style={styles.stepInfoSeparator} />} scrollEnabled={true}
data={formatStepSlots(stepSlots)} renderItem={({ item }) => <LiveInfoItem slots={item.slots} />}
keyExtractor={item => item.startDate} />
<ScrollView showsVerticalScrollIndicator={IS_WEB}>
<FlatList ItemSeparatorComponent={() => <View style={styles.stepInfoSeparator} />} scrollEnabled={false}
manonpalin marked this conversation as resolved.
Show resolved Hide resolved
data={formatStepSlots(stepSlots)} renderItem={({ item }) => <LiveInfoItem slots={item.slots} />}
keyExtractor={item => item.startDate} />
</ScrollView>
</NiModal>
);

2 changes: 2 additions & 0 deletions src/navigation/AppNavigation/index.tsx
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import EmailForm from '../../screens/EmailForm';
import LoginCodeForm from '../../screens/LoginCodeForm';
import CreateAccount from '../../screens/CreateAccount';
import CreateAttendanceSheet from '../../screens/courses/profile/CreateAttendanceSheet';
import UpdateAttendanceSheet from '../../screens/courses/profile/UpdateAttendanceSheet';
import BlendedAbout from '../../screens/explore/BlendedAbout';
import ElearningAbout from '../../screens/explore/ELearningAbout';
import LearnerCourseProfile from '../../screens/courses/profile/LearnerCourseProfile';
@@ -42,6 +43,7 @@ const AppNavigation = () => {
ElearningAbout,
AdminCourseProfile,
CreateAttendanceSheet,
UpdateAttendanceSheet,
...Profile,
...Courses,
};
6 changes: 2 additions & 4 deletions src/screens/courses/profile/AdminCourseProfile/index.tsx
Original file line number Diff line number Diff line change
@@ -94,10 +94,8 @@ const AdminCourseProfile = ({ route, navigation }: AdminCourseProfileProps) => {

const groupedSlots = groupBy(course.slots.filter(slot => !signedSlots.includes(slot._id)), 'step');

return course?.subProgram.steps.map(s => s._id).reduce<Record<string, SlotType[]>>((acc, step) => {
if (groupedSlots[step]) {
acc[step] = groupedSlots[step];
}
return course?.subProgram.steps.reduce<Record<string, SlotType[]>>((acc, step) => {
if (groupedSlots[step._id]) acc[step.name] = groupedSlots[step._id];
return acc;
}, {});
}, [course, isSingle, savedAttendanceSheets]);
27 changes: 4 additions & 23 deletions src/screens/courses/profile/CreateAttendanceSheet/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
import { createStackNavigator, StackScreenProps } from '@react-navigation/stack';
import { CompositeScreenProps } from '@react-navigation/native';
import keyBy from 'lodash/keyBy';
import AttendanceSheets from '../../../../api/attendanceSheets';
import { RootStackParamList, RootCreateAttendanceSheetParamList } from '../../../../types/NavigationType';
import { INTER_B2B, DD_MM_YYYY, HH_MM, IS_WEB } from '../../../../core/data/constants';
import { INTER_B2B, DD_MM_YYYY, HH_MM } from '../../../../core/data/constants';
import { errorReducer, initialErrorState, RESET_ERROR, SET_ERROR } from '../../../../reducers/error';
import AttendanceSheetSelectionForm from '../../../../components/AttendanceSheetSelectionForm';
import UploadMethods from '../../../../components/UploadMethods';
@@ -22,6 +21,7 @@ import MultipleCheckboxList from '../../../../components/form/MultipleCheckboxLi
import AttendanceSignatureContainer from '../../../../components/AttendanceSignatureContainer';
import AttendanceSheetSummary from '../../../../components/AttendanceSheetSummary';
import AttendanceEndScreen from '../../../../components/AttendanceEndScreen';
import { generateSignatureFile } from '../helper';

interface CreateAttendanceSheetProps extends CompositeScreenProps<
StackScreenProps<RootStackParamList, 'CreateAttendanceSheet'>,
@@ -54,10 +54,7 @@ const CreateAttendanceSheet = ({ route, navigation }: CreateAttendanceSheetProps
const [errorSlots, dispatchErrorSlots] = useReducer(errorReducer, initialErrorState);
const [errorSignature, dispatchErrorSignature] = useReducer(errorReducer, initialErrorState);
const [errorConfirmation, dispatchErrorConfirmation] = useReducer(errorReducer, initialErrorState);
const stepsById = useMemo(() => keyBy(course?.subProgram.steps, '_id'), [course]);
const stepsName = useMemo(() =>
Object.keys(groupedSlotsToBeSigned).map(stepId => (stepsById[stepId].name)),
[groupedSlotsToBeSigned, stepsById]);
const stepsName = useMemo(() => Object.keys(groupedSlotsToBeSigned), [groupedSlotsToBeSigned]);
const slotsOptions = useMemo(() =>
Object.values(groupedSlotsToBeSigned)
.map(slotGroup => [...slotGroup]
@@ -137,26 +134,10 @@ const CreateAttendanceSheet = ({ route, navigation }: CreateAttendanceSheetProps
}
};

const base64ToBlob = (base64Data: string, contentType: string) => {
const byteCharacters = atob(base64Data.split(',')[1]);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i += 1) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);

return new Blob([byteArray], { type: contentType });
};

const saveAttendances = async () => {
try {
setIsLoading(true);
let file;
const contentType = 'image/png';
if (IS_WEB) {
const blob = base64ToBlob(signature, contentType);
file = new File([blob], `trainer_signature_${course?._id}.png`, { type: contentType });
} else file = { uri: signature, type: contentType, name: `trainer_signature_${course?._id}` };
const file = generateSignatureFile(signature, course?._id, 'trainer');
const data = formatPayload({
signature: file,
course: course?._id,
Loading