Skip to content

Commit

Permalink
VKT(Frontend & Backend): Examiner exam event overview page
Browse files Browse the repository at this point in the history
  • Loading branch information
jrkkp committed Nov 11, 2024
1 parent c2bab90 commit c2752f1
Show file tree
Hide file tree
Showing 21 changed files with 716 additions and 25 deletions.
32 changes: 31 additions & 1 deletion backend/vkt/db/4_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -392,10 +392,28 @@ SELECT exam_event_id, (SELECT max(person_id) FROM person),
'[email protected]', '0404040404', null, null, null, null
FROM exam_event;

-- Insert enrollment appointment
-- Insert examiner
INSERT INTO examiner(version, oid, email, phone_number, last_name, first_name, nickname, exam_language_finnish, exam_language_swedish, is_public)
VALUES (1, '1.2.246.init-1', '[email protected]', '04040404040', 'Tessilä', 'Testi', 'Tessa', true, true, true);

-- Insert municipality
INSERT INTO municipality(version, code, name_fi, name_sv)
VALUES (1, 'oul', 'Oulu', 'Oulu');

-- insert examiner_exam_event
INSERT INTO examiner_exam_event(version, date, language, examiner_id, is_hidden, registration_closes, max_participants, municipality_id, location)
VALUES (
1,
now() + interval '5 weeks',
'FI',
1,
false,
now() + interval '2 weeks',
10,
1,
'tylypahka'
);

-- Insert enrollment appointment
INSERT INTO enrollment_appointment(person_id, examiner_id,
skill_oral, skill_textual, skill_understanding,
Expand All @@ -406,3 +424,15 @@ VALUES (null, 1,
true, true, true, true,
'CONTACT_CREATED', true,
'[email protected]', '0404040404', null, null, null, null);

-- Insert enrollment appointment
INSERT INTO enrollment_appointment(person_id, examiner_id, examiner_exam_event_id,
skill_oral, skill_textual, skill_understanding,
partial_exam_speaking, partial_exam_speech_comprehension, partial_exam_writing, partial_exam_reading_comprehension,
status, digital_certificate_consent, email, phone_number, street, postal_code, town, country, first_name, last_name)
VALUES (1, 1, 2,
true, true, true,
true, true, true, true,
'WAITING_AUTHENTICATION', true,
'[email protected]', '0404040404', null, null, null, null,
'Teppo', 'Testinen');
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package fi.oph.vkt.api.examiner;

import fi.oph.vkt.api.dto.clerk.ClerkExamEventListDTO;
import fi.oph.vkt.api.dto.examiner.ExaminerExamEventDTO;
import fi.oph.vkt.service.ExaminerExamEventService;
import io.swagger.v3.oas.annotations.Operation;
import java.util.List;

import jakarta.annotation.Resource;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -13,11 +17,20 @@
@RequestMapping(value = "/api/v1/tv/{oid}/examEvent", produces = MediaType.APPLICATION_JSON_VALUE)
public class ExaminerExamEventController {

@Resource
private ExaminerExamEventService examinerExamEventService;

private static final String TAG_EXAMINER_EXAM_EVENT = "Exam event API for examiners";

@GetMapping
@Operation(tags = TAG_EXAMINER_EXAM_EVENT, summary = "List all exam events")
public List<ClerkExamEventListDTO> list(@PathVariable String oid) {
return List.of();
}

@GetMapping(path = "/{examEventId:\\d+}")
@Operation(tags = TAG_EXAMINER_EXAM_EVENT, summary = "Get exam event and enrollments")
public ExaminerExamEventDTO getExamEvent(@PathVariable final long examEventId) {
return examinerExamEventService.getExamEvent(examEventId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package fi.oph.vkt.repository;

import fi.oph.vkt.model.ExaminerExamEvent;
import org.springframework.stereotype.Repository;

@Repository
public interface ExaminerExamEventRepository extends BaseRepository<ExaminerExamEvent> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package fi.oph.vkt.service;

import fi.oph.vkt.api.dto.examiner.ExaminerExamEventDTO;
import fi.oph.vkt.audit.AuditService;
import fi.oph.vkt.audit.VktOperation;
import fi.oph.vkt.model.ExaminerExamEvent;
import fi.oph.vkt.repository.ExaminerExamEventRepository;
import fi.oph.vkt.util.ExaminerUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class ExaminerExamEventService {

private final ExaminerExamEventRepository examinerExamEventRepository;
private final Environment environment;

private final AuditService auditService;

@Transactional(readOnly = true)
public ExaminerExamEventDTO getExamEvent(final long examEventId) {
final ExaminerExamEventDTO examEventDTO = getExamEventWithoutAudit(examEventId);

auditService.logById(VktOperation.GET_EXAM_EVENT, examEventId);
return examEventDTO;
}

private ExaminerExamEventDTO getExamEventWithoutAudit(final long examEventId) {
final ExaminerExamEvent examEvent = examinerExamEventRepository.getReferenceById(examEventId);
final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api");

return ExaminerUtil.toExaminerExamEventDTO(examEvent, baseUrlAPI);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ClerkEnrollmentAppointmentDetailsFields } from 'components/clerkEnrollm
import { ControlButtons } from 'components/clerkEnrollment/overview/ControlButtons';
import { useClerkTranslation, useCommonTranslation } from 'configs/i18n';
import { useAppDispatch, useAppSelector } from 'configs/redux';
import { EnrollmentStatus, UIMode } from 'enums/app';
import { EnrollmentAppointmentStatus, UIMode } from 'enums/app';
import { ClerkEnrollmentTextFieldEnum } from 'enums/clerkEnrollment';
import { useNavigationProtection } from 'hooks/useNavigationProtection';
import { ClerkEnrollmentAppointment } from 'interfaces/clerkEnrollment';
Expand All @@ -17,10 +17,7 @@ import {
resetClerkEnrollmentDetailsUpdate,
updateClerkEnrollmentAppointment,
} from 'redux/reducers/clerkEnrollmentAppointment';
import {
changeClerkEnrollmentStatus,
resetClerkEnrollmentStatusChange,
} from 'redux/reducers/clerkExamEventOverview';
import { resetClerkEnrollmentStatusChange } from 'redux/reducers/clerkExamEventOverview';
import { clerkEnrollmentDetailsSelector } from 'redux/selectors/clerkEnrollmentDetails';
import { clerkExamEventOverviewSelector } from 'redux/selectors/clerkExamEventOverview';
import { EnrollmentUtils } from 'utils/enrollment';
Expand All @@ -37,7 +34,7 @@ export const ClerkEnrollmentAppointmentDetails = ({
const { status, paymentRefundStatus } = useAppSelector(
clerkEnrollmentDetailsSelector,
);
const { examEvent, clerkEnrollmentChangeStatus } = useAppSelector(
const { clerkEnrollmentChangeStatus } = useAppSelector(
clerkExamEventOverviewSelector,
);

Expand Down Expand Up @@ -189,12 +186,6 @@ export const ClerkEnrollmentAppointmentDetails = ({
};

const handleCancelEnrollmentButtonClick = () => {
const statusChange = {
id: enrollmentDetails.id,
version: enrollmentDetails.version,
newStatus: EnrollmentStatus.CANCELED,
};

showDialog({
title: t('cancelEnrollmentDialog.header'),
severity: Severity.Warning,
Expand All @@ -207,8 +198,7 @@ export const ClerkEnrollmentAppointmentDetails = ({
{
title: translateCommon('yes'),
variant: Variant.Contained,
action: () =>
dispatch(changeClerkEnrollmentStatus({ statusChange, examEvent })),
action: () => '', // TODO
},
],
});
Expand Down Expand Up @@ -248,7 +238,9 @@ export const ClerkEnrollmentAppointmentDetails = ({
variant={Variant.Contained}
color={Color.Error}
onClick={handleCancelEnrollmentButtonClick}
disabled={enrollmentDetails.status === EnrollmentStatus.CANCELED}
disabled={
enrollmentDetails.status === EnrollmentAppointmentStatus.CANCELED
}
>
{t('cancelEnrollment')}
</CustomButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
useCommonTranslation,
} from 'configs/i18n';
import { useAppDispatch, useAppSelector } from 'configs/redux';
import { EnrollmentStatus, PaymentStatus } from 'enums/app';
import { EnrollmentAppointmentStatus, PaymentStatus } from 'enums/app';
import { ClerkEnrollmentTextFieldEnum } from 'enums/clerkEnrollment';
import {
ClerkEnrollmentAppointment,
Expand Down Expand Up @@ -463,9 +463,8 @@ export const ClerkEnrollmentAppointmentDetailsFields = ({

const displayPaymentInformation =
[
EnrollmentStatus.COMPLETED,
EnrollmentStatus.AWAITING_PAYMENT,
EnrollmentStatus.EXPECTING_PAYMENT_UNFINISHED_ENROLLMENT,
EnrollmentAppointmentStatus.COMPLETED,
EnrollmentAppointmentStatus.EXPECTING_PAYMENT_UNFINISHED_ENROLLMENT,
].includes(enrollment.status) || enrollment.payments.length > 0;

const displayPaymentHistory = enrollment.payments.length > 1;
Expand Down Expand Up @@ -653,7 +652,8 @@ export const ClerkEnrollmentAppointmentDetailsFields = ({
{enrollment.payments.length > 0 && (
<PaymentDetails payment={enrollment.payments[0]} />
)}
{enrollment.status === EnrollmentStatus.AWAITING_PAYMENT && (
{enrollment.status ===
EnrollmentAppointmentStatus.AWAITING_PAYMENT && (
<div className="columns flex-start">
<CustomButton
color={Color.Secondary}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { CustomTable } from 'shared/components';

import { ExaminerEnrollmentListingHeader } from 'components/examinerEnrollment/listing/ExaminerEnrollmentListingHeader';
import { ExaminerEnrollmentListingRow } from 'components/examinerEnrollment/listing/ExaminerEnrollmentListingRow';
import { ClerkEnrollmentAppointment } from 'interfaces/clerkEnrollment';

interface ExaminerEnrollmentListingProps {
enrollments: Array<ClerkEnrollmentAppointment>;
examEventId: number;
}

const getRowDetailsWithExamEventId = (examEventId: number) => {
const getRowDetails = (enrollment: ClerkEnrollmentAppointment) => {
return (
<ExaminerEnrollmentListingRow
enrollment={enrollment}
examEventId={examEventId}
/>
);
};

return getRowDetails;
};

export const ExaminerEnrollmentListing = ({
enrollments,
examEventId,
}: ExaminerEnrollmentListingProps) => (
<CustomTable
className="table-layout-auto"
data={enrollments}
header={<ExaminerEnrollmentListingHeader />}
getRowDetails={getRowDetailsWithExamEventId(examEventId)}
size="small"
stickyHeader
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { TableCell, TableHead, TableRow } from '@mui/material';

import { useClerkTranslation } from 'configs/i18n';

export const ExaminerEnrollmentListingHeader = () => {
const { t } = useClerkTranslation({
keyPrefix: 'vkt.component.clerkEnrollmentListing.header',
});

return (
<TableHead className="heading-text">
<TableRow>
<TableCell>{t('firstName')}</TableCell>
<TableCell>{t('lastName')}</TableCell>
<TableCell>{t('examEventCoverage')}</TableCell>
<TableCell>{t('registrationTime')}</TableCell>
<TableCell />
</TableRow>
</TableHead>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { TableCell, TableRow } from '@mui/material';
import { useNavigate } from 'react-router';
import { Text } from 'shared/components';

import { useClerkTranslation } from 'configs/i18n';
import { AppRoutes } from 'enums/app';
import { ClerkEnrollmentAppointment } from 'interfaces/clerkEnrollment';
import { DateTimeUtils } from 'utils/dateTime';

const examCodes = {
writingPartialExam: 'KI',
readingComprehensionPartialExam: 'TY',
speakingPartialExam: 'PU',
speechComprehensionPartialExam: 'PY',
};

function pick<T extends object, K extends keyof T>(object: T, keys: Array<K>) {
return keys.reduce((obj, key) => {
if (object && object.hasOwnProperty(key)) {
obj[key] = object[key];
}

return obj;
}, {} as Partial<T>);
}

export const ExaminerEnrollmentListingRow = ({
enrollment,
examEventId,
}: {
enrollment: ClerkEnrollmentAppointment;
examEventId: number;
}) => {
// I18n
const { t } = useClerkTranslation({
keyPrefix: 'vkt.component.clerkEnrollmentListing.row',
});
const navigate = useNavigate();

const getSelectedPartialExamsText = () => {
const partialExams = pick(enrollment, [
'writingPartialExam',
'readingComprehensionPartialExam',
'speakingPartialExam',
'speechComprehensionPartialExam',
]);

if (Object.values(partialExams).some((value) => !value)) {
return Object.keys(partialExams)
.filter((key) => partialExams[key as keyof typeof examCodes])
.map((key) => examCodes[key as keyof typeof examCodes])
.join(', ');
}

return t('fullExam');
};

const onClick = () => {
navigate(
AppRoutes.ClerkEnrollmentOverviewPage.replace(
/:examEventId/,
`${examEventId}`,
),
);
};

return (
<>
<TableRow
data-testid={`enrollments-table__id-${enrollment.id}-row`}
onClick={onClick}
className="cursor-pointer"
>
<TableCell>
<Text>{enrollment.lastName}</Text>
</TableCell>
<TableCell>
<Text>{enrollment.firstName}</Text>
</TableCell>
<TableCell>
<Text>{getSelectedPartialExamsText()}</Text>
</TableCell>
<TableCell>
<Text>{DateTimeUtils.renderDateTime(enrollment.enrollmentTime)}</Text>
</TableCell>
<TableCell sx={{ width: '20%' }} align="right"></TableCell>
</TableRow>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import {
useKoodistoMunicipalitiesTranslation,
} from 'configs/i18n';
import { useAppDispatch, useAppSelector } from 'configs/redux';
import { AppRoutes, EnrollmentStatus, ExamLanguage } from 'enums/app';
import {
AppRoutes,
EnrollmentAppointmentStatus,
ExamLanguage,
} from 'enums/app';
import { ExaminerExamEvent } from 'interfaces/examinerExamEvent';
import { setExaminerExamEventLanguageFilter } from 'redux/reducers/examinerDetails';
import { examinerDetailsSelector } from 'redux/selectors/examinerDetails';
Expand Down Expand Up @@ -69,7 +73,7 @@ const ExaminerExamEventListingRow = ({

// TODO Clarify which enrollments should be counted here
const participantsCount = enrollments.filter(
(e) => e.status === EnrollmentStatus.COMPLETED,
(e) => e.status === EnrollmentAppointmentStatus.COMPLETED,
).length;

return (
Expand Down
Loading

0 comments on commit c2752f1

Please sign in to comment.