diff --git a/package-lock.json b/package-lock.json index f72efbfb..3e6f2097 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "react-circular-progressbar": "^2.1.0", "react-dom": "^18", "react-ga4": "^2.1.0", + "react-toastify": "^10.0.5", "sharp": "^0.33.3" }, "devDependencies": { @@ -5369,6 +5370,18 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index 4a585f81..bc075ace 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react-circular-progressbar": "^2.1.0", "react-dom": "^18", "react-ga4": "^2.1.0", + "react-toastify": "^10.0.5", "sharp": "^0.33.3" }, "devDependencies": { diff --git a/src/components/MarkBulkAttendance.tsx b/src/components/MarkBulkAttendance.tsx index faca6595..6890a3f4 100644 --- a/src/components/MarkBulkAttendance.tsx +++ b/src/components/MarkBulkAttendance.tsx @@ -1,6 +1,6 @@ import { Box, Button, Fade, Modal, Typography } from '@mui/material'; import React, { useEffect } from 'react'; -import Snackbar, { SnackbarOrigin } from '@mui/material/Snackbar'; +import ToastMessage from '@/components/ToastMessage'; import { attendanceStatusList, bulkAttendance, @@ -19,16 +19,14 @@ import Loader from './Loader'; import { getMyCohortMemberList } from '@/services/MyClassDetailsService'; import { useTheme } from '@mui/material/styles'; import { useTranslation } from 'next-i18next'; +import { showToastMessage } from './Toastify'; -interface State extends SnackbarOrigin { - openModal: boolean; -} interface MarkBulkAttendanceProps { open: boolean; onClose: () => void; classId: string; selectedDate: Date; - onSaveSuccess: () => void; + onSaveSuccess?: (isModified?: boolean) => void; } const MarkBulkAttendance: React.FC = ({ @@ -41,24 +39,15 @@ const MarkBulkAttendance: React.FC = ({ const { t } = useTranslation(); const theme = useTheme(); const [loading, setLoading] = React.useState(false); - // const [open, setOpen] = React.useState(false); const [showUpdateButton, setShowUpdateButton] = React.useState(false); const [cohortMemberList, setCohortMemberList] = React.useState>([]); const [presentCount, setPresentCount] = React.useState(0); const [absentCount, setAbsentCount] = React.useState(0); const [bulkAttendanceStatus, setBulkAttendanceStatus] = React.useState(''); - const [isError, setIsError] = React.useState(''); const [isAllAttendanceMarked, setIsAllAttendanceMarked] = React.useState(false); const [numberOfCohortMembers, setNumberOfCohortMembers] = React.useState(0); - const [state, setState] = React.useState({ - openModal: false, - vertical: 'top', - horizontal: 'center', - }); - const { vertical, horizontal, openModal } = state; - // const handleModalToggle = () => setOpen(!open); const modalContainer = { position: 'absolute', top: '50%', @@ -82,7 +71,6 @@ const MarkBulkAttendance: React.FC = ({ setBulkAttendanceStatus( isAllPresent ? 'present' : isAllAbsent ? 'absent' : '' ); - ``; }; const submitBulkAttendanceAction = ( @@ -277,33 +265,38 @@ const MarkBulkAttendance: React.FC = ({ try { const response = await bulkAttendance(data); const resp = response?.data; + if (resp) { setShowUpdateButton(true); - onClose(); setLoading(false); - isError(false); } if (onSaveSuccess) { - onSaveSuccess(); + if (presentCount === 0 && absentCount === 0) { + onSaveSuccess(false); + } else { + onSaveSuccess(true); + } + + onClose(); } } catch (error) { - console.error('Error fetching cohort list:', error); + console.error('Error fetching cohort list:', error); setLoading(false); - setIsError(true); + showToastMessage(t('COMMON.SOMETHING_WENT_WRONG'), 'error'); } - handleClick({ vertical: 'bottom', horizontal: 'center' })(); + // handleClick({ vertical: 'bottom', horizontal: 'center' })(); }; markBulkAttendance(); } }; - const handleClick = (newState: SnackbarOrigin) => () => { - setState({ ...newState, openModal: true }); - }; + // const handleClick = (newState: SnackbarOrigin) => () => { + // setState({ ...newState, openModal: true }); + // }; - const handleClose = () => { - setState({ ...state, openModal: false }); - }; + // const handleClose = () => { + // setState({ ...state, openModal: false }); + // }; return ( @@ -504,33 +497,6 @@ const MarkBulkAttendance: React.FC = ({ - {!isError ? ( - - ) : ( - - )} ); }; diff --git a/src/components/ToastMessage.tsx b/src/components/ToastMessage.tsx index 18c3a69a..edb00f33 100644 --- a/src/components/ToastMessage.tsx +++ b/src/components/ToastMessage.tsx @@ -18,9 +18,11 @@ const DEFAULT_POSITION: Pick = { function ToastMessage({ message, type = 'error', + onClose }: { message: string; type?: ToastTypes; + onClose?: () => void; }) { const [state, setState] = React.useState({ openModal: true, @@ -30,6 +32,7 @@ function ToastMessage({ const handleClose = () => { setState({ ...state, openModal: false }); + onClose && onClose(); }; return ( @@ -38,6 +41,7 @@ function ToastMessage({ open={state.openModal} onClose={handleClose} autoHideDuration={toastAutoHideDuration} + disableWindowBlurListener={true} key={'key_' + generateRandomString(3)} > diff --git a/src/components/Toastify.js b/src/components/Toastify.js new file mode 100644 index 00000000..28c50433 --- /dev/null +++ b/src/components/Toastify.js @@ -0,0 +1,58 @@ +import { toast } from 'react-toastify'; + +const options = { + position: 'bottom-center', + hideProgressBar: true, + closeButton: false, + autoClose: 3000 +}; + +export const showToastMessage = (message, type='success') => { + const commonOptions = { + ...options, + style: { + color: '#fff' + } + }; + + switch(type) { + case 'error': + toast.error(message, { + ...commonOptions, + style: { + ...commonOptions.style, + background: '#FF0000' + } + }); + break; + case 'success': + toast.success(message, { + ...commonOptions, + style: { + ...commonOptions.style, + background: '#019722' + } + }); + break; + case 'info': + toast.info(message, { + ...commonOptions, + style: { + ...commonOptions.style, + background: '#017AFF' + } + }); + break; + case 'warning': + toast.warning(message, { + ...commonOptions, + style: { + ...commonOptions.style, + background: '#FFA500' + } + }); + break; + default: + throw new Error(`Unknown toast type: ${type}`); + } +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 15630d79..6f84c765 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -21,6 +21,8 @@ import customTheme from '../styles/customStyles'; import { telemetryFactory } from '../utils/telemetry'; import { useRouter } from 'next/router'; import { initGA, logPageView } from '../utils/googleAnalytics'; +import { ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; const ColorModeContext = React.createContext({ toggleColorMode: () => {} }); const poppins = Poppins({ @@ -123,6 +125,7 @@ function App({ Component, pageProps }: AppProps) { {/* */} + diff --git a/src/pages/dashboard.tsx b/src/pages/dashboard.tsx index 04dfd5d4..ec956803 100644 --- a/src/pages/dashboard.tsx +++ b/src/pages/dashboard.tsx @@ -1,11 +1,5 @@ 'use client'; -import { - AttendancePercentageProps, - cohort, - cohortAttendancePercentParam, - cohortMemberList, -} from '../utils/Interfaces'; import { Box, Button, @@ -17,51 +11,52 @@ import { Stack, Typography, } from '@mui/material'; +import React, { useEffect } from 'react'; import { CircularProgressbar, buildStyles } from 'react-circular-progressbar'; -import React, { useEffect, useState } from 'react'; import ReactGA from 'react-ga4'; +import { + AttendancePercentageProps, + cohort, + cohortAttendancePercentParam, + cohortMemberList, +} from '../utils/Interfaces'; // import Snackbar, { SnackbarOrigin } from '@mui/material/Snackbar'; +import { format, isAfter, isValid, parse, startOfDay } from 'date-fns'; import { classesMissedAttendancePercentList, getAllCenterAttendance, getCohortAttendance, } from '../services/AttendanceService'; -import { format, isAfter, isValid, parse, startOfDay } from 'date-fns'; import { formatSelectedDate, - getMonthName, getTodayDate, shortDateFormat, - toPascalCase, + toPascalCase } from '../utils/Helper'; -import ArrowForwardSharpIcon from '@mui/icons-material/ArrowForwardSharp'; -import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; -import Divider from '@mui/material/Divider'; -import Header from '../components/Header'; -import Image from 'next/image'; -import Link from 'next/link'; -import Loader from '../components/Loader'; import MarkBulkAttendance from '@/components/MarkBulkAttendance'; import OverviewCard from '@/components/OverviewCard'; import ToastMessage from '@/components/ToastMessage'; +import { showToastMessage } from '@/components/Toastify'; import WeekCalender from '@/components/WeekCalender'; +import { getMyCohortMemberList } from '@/services/MyClassDetailsService'; import { calculatePercentage } from '@/utils/attendanceStats'; +import { logEvent } from '@/utils/googleAnalytics'; +import ArrowForwardSharpIcon from '@mui/icons-material/ArrowForwardSharp'; +import Divider from '@mui/material/Divider'; +import { useTheme } from '@mui/material/styles'; +import { useTranslation } from 'next-i18next'; +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { modifyAttendanceLimit } from '../../app.config'; import calendar from '../assets/images/calendar.svg'; +import Header from '../components/Header'; +import Loader from '../components/Loader'; +import useDeterminePathColor from '../hooks/useDeterminePathColor'; import { cohortList } from '../services/CohortServices'; -import { getMyCohortMemberList } from '@/services/MyClassDetailsService'; import { lowLearnerAttendanceLimit } from './../../app.config'; -import { modifyAttendanceLimit } from '../../app.config'; -import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import useDeterminePathColor from '../hooks/useDeterminePathColor'; -import { useRouter } from 'next/navigation'; -import { useTheme } from '@mui/material/styles'; -import { useTranslation } from 'next-i18next'; -import { logEvent } from '@/utils/googleAnalytics'; - -// interface State extends SnackbarOrigin { -// openModal: boolean; -// } interface DashboardProps { // buttonText: string; @@ -101,7 +96,6 @@ const Dashboard: React.FC = () => { // const { vertical, horizontal, openModal } = state; const router = useRouter(); - const contextId = classId; const theme = useTheme(); const determinePathColor = useDeterminePathColor(); const currentDate = new Date(); @@ -120,8 +114,8 @@ const Dashboard: React.FC = () => { const startDayMonth = startRangeDate.toLocaleString('default', { month: 'long', }); - let endDay = endRangeDate.getDate(); - let endDayMonth = endRangeDate.toLocaleString('default', { + const endDay = endRangeDate.getDate(); + const endDayMonth = endRangeDate.toLocaleString('default', { month: 'long', }); if (startDayMonth == endDayMonth) { @@ -288,7 +282,7 @@ const Dashboard: React.FC = () => { contextId: classId, }, facets: ['contextId'], - sort: ['present_percentage', 'asc'] + sort: ['present_percentage', 'asc'], }; const res = await getCohortAttendance(cohortAttendanceData); const response = res?.data?.result; @@ -400,16 +394,20 @@ const Dashboard: React.FC = () => { setShowDetails(true); }; - const handleModalToggle = () => {setOpen(!open) + const handleModalToggle = () => { + setOpen(!open); logEvent({ action: 'mark/modify-attendance-button-clicked-dashboard', category: 'Dashboard Page', label: 'Mark/ Modify Attendance', - })}; + }); + }; const handleCohortSelection = (event: SelectChangeEvent) => { setClassId(event.target.value as string); - ReactGA.event("cohort-selection-dashboard", { selectedCohortID: event.target.value }); + ReactGA.event('cohort-selection-dashboard', { + selectedCohortID: event.target.value, + }); localStorage.setItem('classId', event.target.value); setHandleSaveHasRun(!handleSaveHasRun); }; @@ -478,7 +476,7 @@ const Dashboard: React.FC = () => { const viewAttendanceHistory = () => { if (classId !== 'all') { router.push('/attendance-history'); - ReactGA.event("month-name-clicked", { selectedCohortID: classId }); + ReactGA.event('month-name-clicked', { selectedCohortID: classId }); } }; @@ -503,14 +501,8 @@ const Dashboard: React.FC = () => { } } const presentPercentage = parseFloat(currentAttendance?.present_percentage); - const pathColor = determinePathColor(presentPercentage); - const truncate = (str: string, length: number) => { - if (str.length <= length) return str; - return str.slice(0, length) + '...'; - }; - const handleMoreDetailsClicked = () => { logEvent({ action: 'more-details-button-clicked', @@ -782,27 +774,17 @@ const Dashboard: React.FC = () => { onClose={handleClose} classId={classId} selectedDate={new Date(selectedDate)} - onSaveSuccess={() => - setHandleSaveHasRun(!handleSaveHasRun) - } + onSaveSuccess={(isModified) => { + if (isModified) { + showToastMessage(t('ATTENDANCE.ATTENDANCE_MODIFIED_SUCCESSFULLY'), 'success'); + } else { + showToastMessage(t('ATTENDANCE.ATTENDANCE_MARKED_SUCCESSFULLY'), 'success'); + } + setHandleSaveHasRun(!handleSaveHasRun); + }} /> )} - {/* */} @@ -952,6 +934,7 @@ const Dashboard: React.FC = () => { {isError && ( )} + {/*