Skip to content

Commit

Permalink
feat: Communications form
Browse files Browse the repository at this point in the history
  • Loading branch information
Grafikart committed Jan 4, 2024
1 parent 7447ebf commit 17626e9
Show file tree
Hide file tree
Showing 15 changed files with 439 additions and 15 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"workbox-core": "^5.1.3",
"workbox-precaching": "^5.1.3",
"workbox-routing": "^5.1.3",
"workbox-strategies": "^5.1.3"
"workbox-strategies": "^5.1.3",
"zod": "^3.22.4"
},
"scripts": {
"dev": "vite",
Expand Down Expand Up @@ -129,6 +130,6 @@
"vitest": "^0.29.7"
},
"volta": {
"node": "16.20.2"
"node": "20.10.0"
}
}
4 changes: 2 additions & 2 deletions src/pages/Home.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { SearchField } from '../ui/SearchField';
import { SurveyCard } from '../ui/SurveyCard';
import { Row } from '../ui/Row';
import { Select } from '../ui/Select';
import { seedSurveyUnits } from '../utils/functions/seeder';
import { seedData } from '../utils/functions/seeder';

export function Home() {
const surveyUnits = useSurveyUnits();
Expand Down Expand Up @@ -65,7 +65,7 @@ export function Home() {
<Typography variant="m" color="textTertiary" as="p">
Vous êtes en mode développement
</Typography>
<Button variant="contained" onClick={seedSurveyUnits}>
<Button variant="contained" onClick={seedData}>
Importer des données de test
</Button>
</Stack>
Expand Down
3 changes: 2 additions & 1 deletion src/pages/SurveyUnitPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AddressCard } from '../ui/SurveyUnit/AddressCard';
import { IdentificationCard } from '../ui/SurveyUnit/IdentificationCard';
import { PersonsCard } from '../ui/SurveyUnit/PersonsCard';
import { ContactsCard } from '../ui/SurveyUnit/ContactsCard';
import { CommunicationsCard } from '../ui/SurveyUnit/CommunicationsCard';

export function SurveyUnitPage() {
const { id } = useParams();
Expand Down Expand Up @@ -38,7 +39,7 @@ export function SurveyUnitPage() {
</Box>
</SwipeableTab>
<SwipeableTab index={2} label={D.goToCommunicationPage}>
c
<CommunicationsCard surveyUnit={surveyUnit} />
</SwipeableTab>
<SwipeableTab index={3} label={D.goToQuestionnairesPage}>
d
Expand Down
10 changes: 10 additions & 0 deletions src/types/pearl.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ declare global {
medium: string;
};

type SurveyUnitCommunicationRequest = {
emiter: 'INTERVIEWER' | 'TOOL';
medium: string;
reason: string;
type: string;
status: { date: number; status: string }[];
};

type SurveyUnit = {
id: string;
persons: SurveyUnitPerson[];
Expand All @@ -97,6 +105,8 @@ declare global {
identificationConfiguration: string;
contactOutcomeConfiguration: string;
contactAttemptConfiguration: string;
communicationRequestConfiguration: boolean;
communicationRequests: SurveyUnitCommunicationRequest[];
};

type Notification = {
Expand Down
1 change: 1 addition & 0 deletions src/ui/RadioLine.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const style = {
export function RadioLine({ value, disabled, label, checked, ...props }) {
return (
<FormControlLabel
disabled={disabled}
sx={style}
value={value}
control={<Radio size="small" sx={{ p: 0 }} />}
Expand Down
200 changes: 200 additions & 0 deletions src/ui/SurveyUnit/CommunicationForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import DialogTitle from '@mui/material/DialogTitle';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import React, { Fragment, useEffect, useMemo, useState } from 'react';
import Stack from '@mui/material/Stack';
import D from 'i18n';
import { useIncrement } from '../../utils/hooks/useIncrement';
import RadioGroup from '@mui/material/RadioGroup';
import { RadioLine } from '../RadioLine';
import Box from '@mui/material/Box';
import { Typography } from '../Typography';
import { getAddressData, getprivilegedPerson, getTitle } from '../../utils/functions';
import { surveyUnitIDBService } from '../../utils/indexeddb/services/surveyUnit-idb-service';
import { COMMUNICATION_REQUEST_FORM_STEPS } from '../../utils/constants';
import { useUser } from '../../utils/hooks/useUser';
import { communicationSchema, recipientSchema, userSchema } from '../../utils/schemas';
import { ValidationError } from '../ValidationError';
import { communicationStatusEnum } from '../../utils/enum/CommunicationEnums';

const max = COMMUNICATION_REQUEST_FORM_STEPS.length - 1;

/**
* Form to add a new contact attempt to a survey unit
*
* @param {() => void} onClose
* @param {SurveyUnit} surveyUnit
* @returns {JSX.Element}
*/
export function CommunicationForm({ onClose, surveyUnit }) {
const { value: currentIndex, decrement, increment } = useIncrement(0, {
min: 0,
max: max,
});
const [isConfirmValid, setConfirmValid] = useState(false);
const step = COMMUNICATION_REQUEST_FORM_STEPS[currentIndex];
/** @var {SurveyUnitCommunicationRequest} communication */
const [communication, setCommunication] = useState({
medium: '',
reason: '',
type: '',
});

const goPreviousStep = e => {
e.preventDefault();
if (step === 0) {
onClose();
return;
}
decrement();
};

const goNextStep = async e => {
e.preventDefault();
if (currentIndex === max) {
const newCommunication = {
...communication,
date: new Date().getTime(),
emiter: 'INTERVIEWER',
status: [{ date: new Date().getTime(), status: communicationStatusEnum.INITIATED.type }],
};
surveyUnitIDBService.addOrUpdateSU({
...surveyUnit,
communicationRequests: [...(surveyUnit.communicationRequests ?? []), newCommunication],
});
onClose();
return;
}
increment();
};

const setValue = value => {
setCommunication({
...communication,
[step.valueName]: value,
});
};

const options = useMemo(() => step?.values, [step]);
const isValid = currentIndex === max ? isConfirmValid : !!communication[step.valueName];
const isFirst = currentIndex === 0;
const isLast = currentIndex === max;

return (
<Dialog maxWidth="s" open={true} onClose={onClose}>
<DialogTitle id="dialogtitle">{step.title}</DialogTitle>
<DialogContent>
<Box>
{options && (
<RadioGroup
value={communication[step.valueName]}
onChange={e => setValue(e.target.value)}
row
aria-labelledby="dialogtitle"
name={name}
>
<Stack gap={1} width={1}>
{options.map(o => (
<RadioLine value={o.value} key={o.value} label={o.label} disabled={o.disabled} />
))}
</Stack>
</RadioGroup>
)}
{isLast && (
<CommunicationConfirmation
communication={communication}
surveyUnit={surveyUnit}
onValidationChange={setConfirmValid}
/>
)}
</Box>
</DialogContent>
<DialogActions>
<Button color="white" variant="contained" onClick={goPreviousStep}>
{isFirst ? D.cancelButton : D.previousButton}
</Button>
<Button disabled={!isValid} variant="contained" onClick={goNextStep}>
{D.confirmButton}
</Button>
</DialogActions>
</Dialog>
);
}

/**
* @param {SurveyUnitCommunicationRequest} communication
* @param {SurveyUnit} surveyUnit
* @param {(v: boolean) => void} onValidationChange
*/
function CommunicationConfirmation({ communication, surveyUnit, onValidationChange }) {
// Extract selected labels
const items = COMMUNICATION_REQUEST_FORM_STEPS.filter(v => v.values).map(
v => v.values.find(value => value.value === communication[v.valueName]).label
);
const address = getAddressData(surveyUnit.address);
const addressLines = Object.values(address).filter(v => !!v);
const recipient = getprivilegedPerson(surveyUnit);
const { user } = useUser();

const userError = userSchema.safeParse(user).error;
const recipientError = recipientSchema.safeParse({ ...address, ...recipient }).error;
const communicationError = communicationSchema.safeParse(communication).error;

useEffect(() => {
onValidationChange(!(userError || recipientError || communicationError));
}, []);

return (
<Stack gap={2} p={2} bgcolor="surfacePrimary.main" minWidth={325} borderRadius={2}>
<div>
<Typography variant="s" color="textTertiary" as="h3">
{D.communicationSummaryContent}
</Typography>
<Box component="ul" p={0} pl={3} m={0}>
{items.map(item => (
<Typography
variant="s"
as="li"
key={item}
color={communicationError ? 'red' : 'textPrimary'}
>
{item}
</Typography>
))}
</Box>
<ValidationError error={communicationError} mt={1} />
</div>
<div>
<Typography variant="s" color="textTertiary" as="h3">
{D.communicationSummaryRecipientAddress}
</Typography>
<Typography variant="s" as="p" color={recipientError ? 'red' : 'textPrimary'}>
{getTitle(recipient.title)} {recipient.firstName} {recipient.lastName}
<br />
{addressLines.map(line => (
<Fragment key={line}>
{line}
<br />
</Fragment>
))}
</Typography>
<ValidationError error={recipientError} mt={1} />
</div>
<div>
<Typography variant="s" color="textTertiary" as="h3">
{D.communicationSummaryInterviewerAddress}
</Typography>
<Typography variant="s" as="p" color={userError ? 'red' : 'textPrimary'}>
{user.firstName} {user.lastName}
<br />
{user.email}
<br />
{user.phoneNumber}
</Typography>
<ValidationError error={userError} mt={1} />
</div>
</Stack>
);
}
113 changes: 113 additions & 0 deletions src/ui/SurveyUnit/CommunicationsCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import { Typography } from '../Typography';
import D from 'i18n';
import { Row } from '../Row';
import Stack from '@mui/material/Stack';
import React from 'react';
import Button from '@mui/material/Button';
import { useToggle } from '../../utils/hooks/useToggle';
import CampaignIcon from '@mui/icons-material/Campaign';
import AddIcon from '@mui/icons-material/Add';
import BuildIcon from '@mui/icons-material/Build';
import DirectionsWalkIcon from '@mui/icons-material/DirectionsWalk';
import Divider from '@mui/material/Divider';
import { formatDate } from '../../utils/functions/date';
import {
findCommunicationMediumValueByType,
findCommunicationReasonValueByType,
findCommunicationStatusValueByType,
findCommunicationTypeValueByType,
} from '../../utils/enum/CommunicationEnums';
import { HEALTHY_COMMUNICATION_REQUEST_STATUS } from '../../utils/constants';
import ClearIcon from '@mui/icons-material/Clear';
import CheckIcon from '@mui/icons-material/Check';
import { CommunicationForm } from './CommunicationForm';
import { canSendCommunication } from '../../utils/functions';

/**
* @param {SurveyUnit} surveyUnit
*/
export function CommunicationsCard({ surveyUnit }) {
const [showModal, toggleModal] = useToggle(false);
const canAddCommunication = canSendCommunication(surveyUnit);

return (
<>
<Card p={2} elevation={0}>
<CardContent>
<Stack gap={3}>
<Row gap={1}>
<CampaignIcon fontSize="large" />
<Typography as="h2" variant="xl" fontWeight={700}>
{D.surveyUnitCommunications}
</Typography>
</Row>
<Button
disabled={!canAddCommunication}
onClick={toggleModal}
variant="contained"
startIcon={<AddIcon />}
>
{D.sendCommunication}
</Button>
<Stack gap={2}>
{surveyUnit.communicationRequests?.map(v => (
<CommunicationItem communication={v} key={v.status[0].date ?? 1} />
))}
</Stack>
</Stack>
</CardContent>
</Card>
{showModal && <CommunicationForm surveyUnit={surveyUnit} onClose={toggleModal} />}
</>
);
}

/**
* @param {SurveyUnitCommunicationRequest} communication
* @constructor
*/
function CommunicationItem({ communication }) {
const IconComponent =
communication.emiter.toUpperCase() === 'INTERVIEWER' ? DirectionsWalkIcon : BuildIcon;
const sortedStatus = [...communication.status].sort((s1, s2) => s1.date > s2.date);
const firstStatus = sortedStatus.at(0);
const lastStatus = sortedStatus.at(-1);
const statusIcon = HEALTHY_COMMUNICATION_REQUEST_STATUS.includes(lastStatus.status) ? (
<CheckIcon color="success" />
) : (
<ClearIcon color="red" />
);

return (
<Row
bgcolor="surfacePrimary.main"
px={1.5}
py={2}
borderRadius={1}
justifyContent="space-between"
>
<Row gap={1.5}>
<IconComponent color="textPrimary" />
<Divider orientation="vertical" flexItem />
<div>
<Typography color="textTertiary" variant="s">
{formatDate(firstStatus.date, true)}
</Typography>
<Typography color="textPrimary" variant="s">
&nbsp;| {findCommunicationMediumValueByType(communication.medium)} -{' '}
{findCommunicationTypeValueByType(communication.type)}
{communication.reason &&
`, ${findCommunicationReasonValueByType(communication.reason)}`}
</Typography>
</div>
</Row>
<Row gap={1.5} typography="s" color="textTertiary.main" fontWeight={600}>
{findCommunicationStatusValueByType(lastStatus.status)} {D.communicationStatusOn}{' '}
{formatDate(lastStatus.date)}
{statusIcon}
</Row>
</Row>
);
}
Loading

0 comments on commit 17626e9

Please sign in to comment.