Skip to content

Commit

Permalink
feat: event modal for instructor detail page
Browse files Browse the repository at this point in the history
  • Loading branch information
01001110J committed Dec 19, 2024
1 parent 0cac229 commit 499353c
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ describe('InstructorsDetailPage', () => {
test('Should render the calendar if the flag is provided', async () => {
getConfig.mockImplementation(() => ({ enable_instructor_calendar: true }));

const { getByText } = renderWithProviders(
const { getByText, getAllByText } = renderWithProviders(
<MemoryRouter initialEntries={['/instructors/instructor']}>
<Route path="/instructors/:instructorUsername">
<InstructorsDetailPage />
Expand All @@ -130,7 +130,7 @@ describe('InstructorsDetailPage', () => {
{ preloadedState: mockStore },
);

fireEvent.click(getByText('Availability'));
fireEvent.click(getAllByText('Availability')[0]);

await waitFor(() => {
expect(getByText('Today')).toBeInTheDocument();
Expand Down Expand Up @@ -160,4 +160,25 @@ describe('InstructorsDetailPage', () => {

expect(calendarTab).not.toBeInTheDocument();
});

test('Should render event modal', () => {
getConfig.mockImplementation(() => ({ enable_instructor_calendar: true }));

const { getByText, getAllByText } = renderWithProviders(
<MemoryRouter initialEntries={['/instructors/instructor']}>
<Route path="/instructors/:instructorUsername">
<InstructorsDetailPage />
</Route>
</MemoryRouter>,
{ preloadedState: mockStore },
);

const newEventBtn = getByText('New event');
fireEvent.click(newEventBtn);
expect(getAllByText('New event')).toHaveLength(2);

waitFor(() => {
expect(getByText('New Event')).toBeInTheDocument();
});
});
});
84 changes: 72 additions & 12 deletions src/features/Instructors/InstructorsDetailPage/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,25 @@ import {
Tab,
} from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { CalendarExpanded } from 'react-paragon-topaz';
import { startOfMonth, endOfMonth } from 'date-fns';
import { startOfMonth, endOfMonth, endOfDay } from 'date-fns';
import { logError } from '@edx/frontend-platform/logging';
import { CalendarExpanded, Button, AddEventModal } from 'react-paragon-topaz';

import Table from 'features/Main/Table';
import { fetchClassesData } from 'features/Classes/data/thunks';
import { resetClassesTable, updateCurrentPage } from 'features/Classes/data/slice';
import { fetchInstructorsData, fetchEventsData, resetEvents } from 'features/Instructors/data';
import {
fetchInstructorsData,
fetchEventsData,
resetEvents,
updateEvents,
} from 'features/Instructors/data';
import { createInstructorEvent } from 'features/Instructors/data/api';
import { columns } from 'features/Instructors/InstructorDetailTable/columns';
import { initialPage, RequestStatus } from 'features/constants';
import { initialPage, RequestStatus, AVAILABILITY_VALUES } from 'features/constants';

import { useInstitutionIdQueryParam } from 'hooks';
import { setTimeInUTC, stringToDateType } from 'helpers';

import LinkWithQuery from 'features/Main/LinkWithQuery';

Expand All @@ -40,25 +48,33 @@ const defaultInstructorInfo = {
instructorName: '',
};

const generateValueLabelPairs = (options) => options.reduce((accumulator, option) => {
accumulator[option.value] = option.label;
return accumulator;
}, {});

const typeEventOptions = [
{ label: 'Not available', value: AVAILABILITY_VALUES.notAvailable },
{ label: 'Available', value: AVAILABILITY_VALUES.available },
{ label: 'Prep Time', value: AVAILABILITY_VALUES.prepTime },
];

const eventTitles = generateValueLabelPairs(typeEventOptions);

const InstructorsDetailPage = () => {
const history = useHistory();
const dispatch = useDispatch();
const { instructorUsername } = useParams();

const addQueryParam = useInstitutionIdQueryParam();
const events = useSelector((state) => state.instructors.events.data);

const institutionRef = useRef(undefined);
const [currentPage, setCurrentPage] = useState(initialPage);
const [eventsList, setEventsList] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [currentPage, setCurrentPage] = useState(initialPage);
const [rangeDates, setRangeDates] = useState(initialState);

const getRangeDate = useCallback((range) => {
setRangeDates({
start_date: range.start.toISOString(),
end_date: range.end.toISOString(),
});
}, [setRangeDates]);

const institution = useSelector((state) => state.main.selectedInstitution);
const classes = useSelector((state) => state.classes.table);
const instructorInfo = useSelector((state) => state.instructors.table.data)
Expand All @@ -72,6 +88,38 @@ const InstructorsDetailPage = () => {
dispatch(updateCurrentPage(targetPage));
};

const handleToggleModal = () => setIsModalOpen(!isModalOpen);

const getRangeDate = useCallback((range) => {
setRangeDates({
start_date: range.start.toISOString(),
end_date: range.end.toISOString(),
});
}, [setRangeDates]);

const createNewEvent = async (eventData) => {
try {
const endTypeDate = stringToDateType(eventData.endDate);
const eventDataRequest = {
instructor_id: instructorInfo.instructorId,
title: eventTitles[eventData.availability || 'available'],
availability: eventData.availability || 'available',
start: setTimeInUTC(stringToDateType(eventData.startDate), eventData.startHour),
end: setTimeInUTC(endOfDay(endTypeDate), eventData.endHour),
recurrence: eventData.recurrence.value,
};

const { data: newEvent } = await createInstructorEvent(eventDataRequest);
dispatch(updateEvents([...events, newEvent]));

if (eventDataRequest.recurrence) {
dispatch(fetchEventsData(rangeDates, events));
}
} catch (error) {
logError(error);
}
};

useEffect(() => {
if (institution.id) {
dispatch(fetchInstructorsData(institution.id, initialPage, { instructor: instructorUsername }));
Expand Down Expand Up @@ -155,6 +203,18 @@ const InstructorsDetailPage = () => {
{
showInstructorCalendar && (
<Tab eventKey="availability" title="Availability" tabClassName="text-decoration-none">
<AddEventModal
isOpen={isModalOpen}
onClose={handleToggleModal}
onSave={createNewEvent}
/>
<div className="d-flex justify-content-between align-items-baseline bg-primary px-3 py-2 rounded-top">
<h4 className="text-white">Availability</h4>
<Button variant="inverse-primary" onClick={handleToggleModal}>
<i className="fa-light fa-plus pr-2" />
New event
</Button>
</div>
<div className="p-3 bg-white mb-5 rounded-bottom container-calendar">
<CalendarExpanded
eventsList={eventsList}
Expand Down
9 changes: 9 additions & 0 deletions src/features/Instructors/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,17 @@ function getEventsByInstructor(params) {
);
}

function createInstructorEvent(eventData) {
const params = new URLSearchParams(eventData).toString();

return getAuthenticatedHttpClient().post(
`${getConfig().COURSE_OPERATIONS_API_V2_BASE_URL}/events/?${params}`,
);
}

export {
handleInstructorsEnrollment,
handleNewInstructor,
getEventsByInstructor,
createInstructorEvent,
};
3 changes: 2 additions & 1 deletion src/features/Instructors/data/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { reducer, resetEvents } from 'features/Instructors/data/slice';
export { reducer, resetEvents, updateEvents } from 'features/Instructors/data/slice';

export {
fetchInstructorsData,
assignInstructors,
Expand Down
10 changes: 10 additions & 0 deletions src/features/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,13 @@ export const allResultsOption = {
label: 'Show all search results',
value: 'all_results',
};

/**
* Values for availability.
* @constant {Object}
*/
export const AVAILABILITY_VALUES = {
notAvailable: 'not_available',
available: 'available',
prepTime: 'prep_time',
};
46 changes: 46 additions & 0 deletions src/helpers/__test__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
formatUTCDate,
getInitials,
setAssignStaffRole,
stringToDateType,
setTimeInUTC,
} from 'helpers';

import { assignStaffRole } from 'features/Main/data/api';
Expand Down Expand Up @@ -120,3 +122,47 @@ describe('setAssignStaffRole', () => {
expect(window.open).toHaveBeenCalledWith(url, '_blank', 'noopener,noreferrer');
});
});

describe('stringToDateType', () => {
test('Should convert a string date in YYYY-MM-DD format to a Date object', () => {
const input = '2024-12-18';
const result = stringToDateType(input);

expect(result instanceof Date).toBe(true);

expect(result.getFullYear()).toBe(2024);
expect(result.getMonth()).toBe(11);
expect(result.getDate()).toBe(18);
});

test('Should handle invalid date strings gracefully', () => {
const input = 'invalid-date';
const result = stringToDateType(input);

expect(result.toString()).toBe('Invalid Date');
});
});

describe('setTimeInUTC', () => {
test('Should set time correctly with provided time string', () => {
const date = '2024-09-17T00:00:00Z';
const timeString = '15:37';
const result = setTimeInUTC(date, timeString);

expect(result).toBe('2024-09-17T15:37:00.000Z');
});

test('Should maintain original time if no time string is provided', () => {
const date = '2024-09-17T12:34:56Z';
const result = setTimeInUTC(date);
expect(result).toBe('2024-09-17T12:34:56.000Z');
});

test('Should handle edge case of date string input', () => {
const date = new Date('2024-09-17');
const timeString = '15:37';
const result = setTimeInUTC(date, timeString);

expect(result).toBe('2024-09-17T15:37:00.000Z');
});
});
28 changes: 28 additions & 0 deletions src/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,31 @@ export const formatSelectOptions = (options) => {
value: option.id,
}));
};

/**
* Sets the time of a given date in UTC format. If no time is provided, the current time of the date is maintained.
*
* @param {Date | string} date - The date object or date string to modify.
* @param {string} [timeString] - Optional time in the format 'HH:MM'. If provided, it sets the hours and minutes.
* @returns {string} - The date in ISO string format with the time adjusted to UTC.
*/
export const setTimeInUTC = (date, timeString) => {
const newDate = new Date(date);

if (timeString) {
const [hours, minutes] = timeString.split(':').map(Number);
newDate.setUTCHours(hours);
newDate.setUTCMinutes(minutes);
newDate.setSeconds(0);
}

return newDate?.toISOString();
};

/**
* Transform string to date type
*
* @param {string} date - The string with format 'YYYY-MM-DD' to transform
* @returns {Date} - The Date in format 'YYYY/MM/DD'
*/
export const stringToDateType = (date) => new Date(date.replace(/-/g, '/'));

0 comments on commit 499353c

Please sign in to comment.