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 3849 #689

Merged
merged 10 commits into from
Dec 6, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,26 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
import { useCallback, useEffect } from 'react';
import styles from './styles';
import RadioButtonList from '../form/RadioButtonList';
import NiPrimaryButton from '../form/PrimaryButton';
import NiErrorMessage from '../ErrorMessage';
import { ErrorStateType } from '../../reducers/error';
import FeatherButton from '../icons/FeatherButton';
import { ICON } from '../../styles/metrics';
import { GREY } from '../../styles/colors';

interface AttendanceSheetDataSelectionFormProps {
interface AttendanceSheetSelectionFormProps {
title: string,
options: { label: string, value: string }[],
setOption: (value: string) => void,
goToNextScreen: () => void,
error: ErrorStateType
error: ErrorStateType,
children: any,
}

const AttendanceSheetDataSelectionForm = ({
const AttendanceSheetSelectionForm = ({
title,
options,
error,
setOption,
goToNextScreen,
}: AttendanceSheetDataSelectionFormProps) => {
error,
children,
}: AttendanceSheetSelectionFormProps) => {
const navigation = useNavigation();

const hardwareBackPress = useCallback(() => {
Expand All @@ -45,12 +42,12 @@ const AttendanceSheetDataSelectionForm = ({
</View>
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
<Text style={styles.title}>{title}</Text>
<RadioButtonList options={options} setOption={setOption}/>
{children}
</ScrollView>
<View style={styles.button}>
<NiErrorMessage message={error.message} show={error.value}/>
<NiPrimaryButton caption={'Suivant'} onPress={goToNextScreen}/>
</View>
</SafeAreaView>;
};
export default AttendanceSheetDataSelectionForm;
export default AttendanceSheetSelectionForm;
6 changes: 4 additions & 2 deletions src/components/UploadMethods/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as ImagePicker from 'expo-image-picker';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
import { GREY, PINK } from '../../styles/colors';
import { INTER_B2B } from '../../core/data/constants';
import { INTER_B2B, SINGLE_COURSES_SUBPROGRAM_IDS } from '../../core/data/constants';
import AttendanceSheets from '../../api/attendanceSheets';
import styles from './styles';
import NiPrimaryButton from '../../components/form/PrimaryButton';
Expand All @@ -19,11 +19,12 @@ import { ICON } from '../../styles/metrics';

interface UploadMethodsProps {
attendanceSheetToAdd: string,
slotsToAdd: string[],
course: CourseType,
goToParent: () => void,
}

const UploadMethods = ({ attendanceSheetToAdd, course, goToParent }: UploadMethodsProps) => {
const UploadMethods = ({ attendanceSheetToAdd, slotsToAdd = [], course, goToParent }: UploadMethodsProps) => {
const navigation = useNavigation();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [camera, setCamera] = useState<boolean>(false);
Expand Down Expand Up @@ -79,6 +80,7 @@ const UploadMethods = ({ attendanceSheetToAdd, course, goToParent }: UploadMetho
file,
course: course._id,
...(course.type === INTER_B2B ? { trainee: attendanceSheetToAdd } : { date: attendanceSheetToAdd }),
...(SINGLE_COURSES_SUBPROGRAM_IDS.includes(course.subProgram._id) && { slots: slotsToAdd }),
});
await AttendanceSheets.upload(data);
}
Expand Down
56 changes: 56 additions & 0 deletions src/components/form/MultipleCheckboxList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { View, Text, TouchableOpacity } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { GREY, PINK } from '../../../styles/colors';
import { DataOptionsType } from '../../../store/attendanceSheets/slice';
import styles from './styles';

interface MultipleCheckboxListProps {
optionsGroups: DataOptionsType[][],
groupTitles: string[],
setOptions: (options: string[]) => void,
checkedList: string[],
}

interface RenderItemProps {
item: DataOptionsType,
checkedList: string[],
onPressCheckbox: (value: string) => void
}

const renderItem = ({ item, checkedList, onPressCheckbox }: RenderItemProps) => {
const iconName = checkedList.includes(item.value) ? 'check-box' : 'check-box-outline-blank';
const iconColor = checkedList.includes(item.value) ? PINK[500] : GREY[600];
const textStyle = checkedList.includes(item.value) ? styles.text : { ...styles.text, color: GREY[600] };

return (
<TouchableOpacity key={item.label} style={styles.itemContainer} onPress={() => onPressCheckbox(item.value)}>
<MaterialIcons style={styles.icon} size={24} name={iconName} color={iconColor} />
<Text style={textStyle}>{item.label}</Text>
</TouchableOpacity>
);
};

const MultipleCheckboxList = ({ optionsGroups, groupTitles, setOptions, checkedList }: MultipleCheckboxListProps) => {
const onPressCheckbox = (value: string) => {
const indexToRemove = checkedList.indexOf(value);
if (indexToRemove !== -1) {
manonpalin marked this conversation as resolved.
Show resolved Hide resolved
checkedList.splice(indexToRemove, 1);
setOptions([...checkedList]);
manonpalin marked this conversation as resolved.
Show resolved Hide resolved
} else {
setOptions([...checkedList, value]);
}
};

return (
<View style={styles.container}>
{optionsGroups.map((options, index) => (
<View key={index} style={styles.groupContainer}>
<Text style={styles.groupLabel}>{groupTitles[index]}</Text>
{options.map(item => renderItem({ item, checkedList, onPressCheckbox }))}
</View>
))}
</View>
);
};

export default MultipleCheckboxList;
30 changes: 30 additions & 0 deletions src/components/form/MultipleCheckboxList/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { StyleSheet } from 'react-native';
import { MARGIN, PADDING } from '../../../styles/metrics';
import { FIRA_SANS_BOLD, FIRA_SANS_REGULAR } from '../../../styles/fonts';

const styles = StyleSheet.create({
itemContainer: {
paddingHorizontal: PADDING.MD,
flexDirection: 'row',
alignItems: 'center',
},
text: {
...FIRA_SANS_REGULAR.MD,
margin: MARGIN.SM,
},
icon: {
marginRight: MARGIN.SM,
},
container: {
margin: MARGIN.SM,
},
groupContainer: {
marginBottom: MARGIN.MD,
},
groupLabel: {
...FIRA_SANS_BOLD.MD,
marginBottom: MARGIN.SM,
},
});

export default styles;
25 changes: 11 additions & 14 deletions src/components/form/RadioButtonList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,38 @@
import { useEffect, useState } from 'react';
import { TouchableOpacity, Text } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import styles from './styles';
import { GREY } from '../../../styles/colors';

type RadioButtonOptionsType = { label: string, value: string };
import { GREY, PINK } from '../../../styles/colors';
import { DataOptionsType } from '../../../store/attendanceSheets/slice';

interface RadioButtonProps {
options: RadioButtonOptionsType[],
options: DataOptionsType[],
setOption: (option: string) => void,
checkedRadioButton: string
}

interface RenderItemProps {
item: { label: string, value: string },
item: DataOptionsType,
checkedRadioButton: string,
onPressCheckbox: (value: string) => void
}

const renderItem = ({ item, checkedRadioButton, onPressCheckbox }: RenderItemProps) => {
const iconName = checkedRadioButton === item.value ? 'radio-button-checked' : 'radio-button-unchecked';
const iconColor = checkedRadioButton === item.value ? GREY[900] : GREY[600];
const iconColor = checkedRadioButton === item.value ? PINK[500] : GREY[600];
const textStyle = checkedRadioButton === item.value ? styles.text : { ...styles.text, color: GREY[600] };

return (
<TouchableOpacity key={item.label} style={styles.container} onPress={() => onPressCheckbox(item.value)}>
<MaterialIcons style={styles.icon} size={20} name={iconName} color={iconColor} />
<MaterialIcons style={styles.icon} size={24} name={iconName} color={iconColor} />
<Text style={textStyle}>{item.label}</Text>
</TouchableOpacity>
);
};

const RadioButtonList = ({ options, setOption }: RadioButtonProps) => {
const [checkedRadioButton, setCheckedRadioButton] = useState<string>('');

useEffect(() => setOption(checkedRadioButton), [setOption, checkedRadioButton]);

const onPressCheckbox = (value: string) => setCheckedRadioButton(prevValue => (prevValue === value ? '' : value));
const RadioButtonList = ({ options, checkedRadioButton, setOption }: RadioButtonProps) => {
const onPressCheckbox = (value: string) => {
setOption(checkedRadioButton === value ? '' : value);
};

return (
<>
Expand Down
7 changes: 5 additions & 2 deletions src/core/helpers/pictures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export const formatImage = async (

export const formatPayload = (payload): FormDataType => {
const data = new FormData();
Object.entries(payload).forEach(([key, value]) => data.append(key, value));

Object.entries(payload).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => data.append(key, v));
} else data.append(key, value);
});
return data;
};
58 changes: 48 additions & 10 deletions src/screens/courses/profile/AdminCourseProfile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { View, BackHandler, Text, ScrollView, TouchableOpacity, Image, FlatList
import { Feather } from '@expo/vector-icons';
import pick from 'lodash/pick';
import uniqBy from 'lodash/uniqBy';
import groupBy from 'lodash/groupBy';
import get from 'lodash/get';
import has from 'lodash/has';
import { SafeAreaView } from 'react-native-safe-area-context';
import { StackScreenProps } from '@react-navigation/stack';
Expand Down Expand Up @@ -49,6 +51,7 @@ import {
useGetCourse,
useSetCourse,
useSetMissingAttendanceSheets,
useSetGroupedSlotsToBeSigned,
useResetAttendanceSheetReducer,
} from '../../../../store/attendanceSheets/hooks';

Expand All @@ -66,14 +69,32 @@ const AdminCourseProfile = ({ route, navigation }: AdminCourseProfileProps) => {
const course = useGetCourse();
const setCourse = useSetCourse();
const setMissingAttendanceSheet = useSetMissingAttendanceSheets();
const setGroupedSlotsToBeSigned = useSetGroupedSlotsToBeSigned();
const resetAttendanceSheetReducer = useResetAttendanceSheetReducer();
const [isSingle, setIsSingle] = useState<boolean>(false);
const [savedAttendanceSheets, setSavedAttendanceSheets] = useState<AttendanceSheetType[]>([]);
const [title, setTitle] = useState<string>('');
const [firstSlot, setFirstSlot] = useState<SlotType | null>(null);
const [noAttendancesMessage, setNoAttendancesMessage] = useState<string>('');

const groupedSlotsToBeSigned = useMemo(() => {
if (!isSingle || !course?.slots.length) return {};
const signedSlots = (savedAttendanceSheets as SingleAttendanceSheetType[])
.map(as => get(as, 'slots', []).map(s => s._id))
.flat();

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 acc;
}, {});
}, [course, isSingle, savedAttendanceSheets]);

const missingAttendanceSheets = useMemo(() => {
if (!course?.slots.length) return [];
if (!course?.slots.length || !firstSlot) return [];

if ([INTRA, INTRA_HOLDING].includes(course?.type)) {
const intraOrIntraHoldingCourseSavedSheets = savedAttendanceSheets as IntraOrIntraHoldingAttendanceSheetType[];
Expand All @@ -95,11 +116,20 @@ const AdminCourseProfile = ({ route, navigation }: AdminCourseProfileProps) => {
const interCourseSavedSheets = savedAttendanceSheets as InterAttendanceSheetType[];
const savedTrainees = interCourseSavedSheets.map(sheet => sheet.trainee?._id);

if (isSingle) {
if (Object.values(groupedSlotsToBeSigned).flat().length) {
return course.trainees!
.map(t => ({ value: t._id, label: formatIdentity(t.identity, LONG_FIRSTNAME_LONG_LASTNAME) }));
}
return [];
}

return [...new Set(
course?.trainees?.map(t => ({ value: t._id, label: formatIdentity(t.identity, LONG_FIRSTNAME_LONG_LASTNAME) }))
.filter(trainee => !savedTrainees.includes(trainee.value))
course?.trainees?.filter(trainee => (!savedTrainees.includes(trainee._id)))
.map(t => ({ value: t._id, label: formatIdentity(t.identity, LONG_FIRSTNAME_LONG_LASTNAME) }))
)];
}, [course, firstSlot, savedAttendanceSheets]);
}, [course, firstSlot, isSingle, savedAttendanceSheets, groupedSlotsToBeSigned]);

const [imagePreview, setImagePreview] =
useState<imagePreviewProps>({ visible: false, id: '', link: '', type: '' });
const [questionnaireQRCode, setQuestionnaireQRCode] = useState<string>('');
Expand Down Expand Up @@ -147,7 +177,8 @@ const AdminCourseProfile = ({ route, navigation }: AdminCourseProfileProps) => {

useEffect(() => {
setMissingAttendanceSheet(missingAttendanceSheets);
}, [missingAttendanceSheets, setMissingAttendanceSheet]);
setGroupedSlotsToBeSigned(groupedSlotsToBeSigned);
}, [missingAttendanceSheets, setMissingAttendanceSheet, groupedSlotsToBeSigned, setGroupedSlotsToBeSigned]);

useEffect(() => () => {
const currentRoute = navigation.getState().routes[navigation.getState().index];
Expand Down Expand Up @@ -236,7 +267,7 @@ const AdminCourseProfile = ({ route, navigation }: AdminCourseProfileProps) => {
);
};

const goToAttendanceSheetUpload = () => navigation.navigate('CreateAttendanceSheet');
const goToAttendanceSheetUpload = () => navigation.navigate('CreateAttendanceSheet', { isSingle });

return course && has(course, 'subProgram.program') && (
<SafeAreaView style={commonStyles.container} edges={['top']}>
Expand All @@ -249,11 +280,18 @@ const AdminCourseProfile = ({ route, navigation }: AdminCourseProfileProps) => {
<Text style={styles.italicText}>{noAttendancesMessage}</Text>}
</View>
{!!missingAttendanceSheets.length && !course.archivedAt && <View style={styles.uploadContainer}>
<Text style={styles.header}>Chargez vos feuilles d&apos;émargements quand elles sont complètes.</Text>
<Text style={styles.header}>
{ isSingle
? 'Pour charger une feuille d\'émargement ou envoyer une demande de signature veuillez cliquer sur le '
+ 'bouton ci-dessous.'
: 'Chargez vos feuilles d\'émargements quand elles sont complètes.'
}
</Text>
<View style={styles.sectionContainer}>
<SecondaryButton caption={'Charger une feuille d\'émargement'} onPress={goToAttendanceSheetUpload}
customStyle={styles.uploadButton} bgColor={course?.companies?.length ? YELLOW[300] : YELLOW[200]}
color={course?.companies?.length ? BLACK : GREY[600]} disabled={!course?.companies?.length}/>
<SecondaryButton caption={isSingle ? 'Emarger des créneaux' : 'Charger une feuille d\'émargement'}
onPress={goToAttendanceSheetUpload} customStyle={styles.uploadButton}
bgColor={course?.companies?.length ? YELLOW[300] : YELLOW[200]} disabled={!course?.companies?.length}
color={course?.companies?.length ? BLACK : GREY[600]} />
{!course.companies?.length &&
<Text style={styles.italicText}>
Au moins une structure doit être rattachée à la formation pour pouvoir ajouter une feuille
Expand Down
Loading
Loading