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

feat: nouveau compte-rendu: possibilité de voir les actions/consultations créées pendant la période #1862

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions dashboard/src/components/ActionModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useHistory, useLocation } from 'react-router-dom';
import { actionsState, allowedActionFieldsInHistory, CANCEL, DONE, prepareActionForEncryption, TODO } from '../recoil/actions';
import { currentTeamState, organisationState, teamsState, userState } from '../recoil/auth';
import { dayjsInstance, now, outOfBoundariesDate } from '../services/date';
import { dayjsInstance, formatDateWithNameOfDay, now, outOfBoundariesDate } from '../services/date';
import API from '../services/api';
import SelectPerson from './SelectPerson';
import SelectStatus from './SelectStatus';
Expand Down Expand Up @@ -274,15 +274,12 @@ function ActionContent({ onClose, action, personId = null, personIds = null, isM
const body = { ...data };
body.teams = Array.isArray(data.teams) ? data.teams : [data.team];
if (!data.teams?.length) return toast.error('Une action doit être associée à au moins une équipe.');
const statusChanged = data.status && action.status !== data.status;
if (statusChanged) {
if ([DONE, CANCEL].includes(data.status)) {
// When status changed to finished (done, cancel) completedAt we set it to now if not set.
body.completedAt = data.completedAt || now();
} else {
// When status just changed to "todo" we set completedAt to null (since it's not done yet).
body.completedAt = null;
}
if ([DONE, CANCEL].includes(data.status)) {
// When status changed to finished (done, cancel) completedAt we set it to now if not set.
body.completedAt = data.completedAt || now();
} else {
// When status just changed to "todo" we set completedAt to null (since it's not done yet).
body.completedAt = null;
}
if (data.completedAt && outOfBoundariesDate(data.completedAt)) return toast.error('La date de complétion est hors limites (entre 1900 et 2100)');
if (data.dueAt && outOfBoundariesDate(data.dueAt)) return toast.error("La date d'échéance est hors limites (entre 1900 et 2100)");
Expand Down Expand Up @@ -350,7 +347,7 @@ function ActionContent({ onClose, action, personId = null, personIds = null, isM
<UserName
className="tw-block tw-text-right tw-text-base tw-font-normal tw-italic"
id={action.user}
wrapper={(name) => ` (créée par ${name})`}
wrapper={(name) => ` (créée par ${name} le ${formatDateWithNameOfDay(action.createdAt)})`}
/>
)}
</>
Expand Down
15 changes: 13 additions & 2 deletions dashboard/src/components/ActionsSortableList.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import useSearchParamState from '../services/useSearchParamState';
import DescriptionIcon from './DescriptionIcon';
import { AgendaMutedIcon } from '../assets/icons/AgendaMutedIcon';

const ActionsSortableList = ({ data, limit }) => {
const ActionsSortableList = ({ data, limit, showCreatedAt }) => {
useTitle('Agenda');
const history = useHistory();
const user = useRecoilValue(userState);
Expand Down Expand Up @@ -154,7 +154,18 @@ const ActionsSortableList = ({ data, limit }) => {
);
},
},
]}
{
title: 'Créée le',
dataKey: 'createdAt',
onSortOrder: setSortOrder,
onSortBy: setSortBy,
sortBy,
sortOrder,
render: (action) => {
return <DateBloc date={action.createdAt} />;
},
},
].filter((e) => showCreatedAt || e.dataKey !== 'createdAt')}
/>
{limit > 0 && <Page page={page} limit={limit} total={total} onChange={({ page }) => setPage(page, true)} />}
</>
Expand Down
197 changes: 118 additions & 79 deletions dashboard/src/scenes/report/components/ActionsOrConsultationsReport.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useMemo } from 'react';
import { mappedIdsToLabels } from '../../../recoil/actions';
import { useHistory } from 'react-router-dom';
import SelectCustom from '../../../components/SelectCustom';
Expand All @@ -10,23 +10,71 @@ import { useLocalStorage } from '../../../services/useLocalStorage';
import { useRecoilValue } from 'recoil';
import { userState } from '../../../recoil/auth';
import { dayjsInstance } from '../../../services/date';
import EyeIcon from '../../../assets/icons/EyeIcon';

export const ActionsOrConsultationsReport = ({ actions, consultations, period }) => {
const [activeTab, setActiveTab] = useLocalStorage('reports-actions-consultation-toggle', 'Actions');
const [fullScreen, setFullScreen] = useState(false);
const [filterStatus, setFilterStatus] = useState([]);
const filteredActions = actions.filter((item) => !filterStatus.length || filterStatus.includes(item.status));
const filteredConsultations = consultations.filter((item) => !filterStatus.length || filterStatus.includes(item.status));
const data = activeTab.includes('Actions') ? actions : consultations;
const filteredData = activeTab.includes('Actions') ? filteredActions : filteredConsultations;
const formatEcheanceLabelPeriod = (period) => {
if (!!period.startDate && !!period.endDate) {
const start = dayjsInstance(period.startDate);
const end = dayjsInstance(period.endDate);
const today = dayjsInstance();
const showYear = start.year() !== end.year() || start.year() !== today.year();
const startFormatted = dayjsInstance(period.startDate).format(showYear ? 'D MMM YYYY' : 'D MMM');
const endFormatted = dayjsInstance(period.endDate).format(showYear ? 'D MMM YYYY' : 'D MMM');
if (startFormatted === endFormatted) return `le ${startFormatted}`;
return `entre le ${startFormatted} et le ${endFormatted}`;
}
return '';
};

export const ActionsOrConsultationsReport = ({
actionsCompletedAt,
actionsCanceledAt,
actionsCreatedAt,
consultationsCompletedAt,
consultationsCanceledAt,
consultationsCreatedAt,
period,
}) => {
const history = useHistory();
const user = useRecoilValue(userState);

const [activeTab, setActiveTab] = useLocalStorage('reports-actions-consultations-todo-done-toggle', 'Faites');
const canSeeMedicalData = ['admin', 'normal'].includes(user.role) && !!user.healthcareProfessional;
const [showActions, setShowActions] = useLocalStorage('reports-actions-consultations-show-actions', true);
const [showConsultations, setShowConsultations] = useLocalStorage('reports-actions-consultations-show-consults', canSeeMedicalData);

const activeTabIndex = useMemo(() => {
if (activeTab.includes('Faites')) return 0;
if (activeTab.includes('Annulées')) return 1;
return 2; // activeTab.includes('Créées')
}, [activeTab, canSeeMedicalData]);

const [fullScreen, setFullScreen] = useState(false);

const actions = useMemo(() => {
if (activeTab.includes('Faites')) return actionsCompletedAt;
if (activeTab.includes(`Annulées`)) return actionsCanceledAt;
return actionsCreatedAt;
}, [activeTab, actionsCompletedAt, actionsCanceledAt, actionsCreatedAt]);

const tabs = canSeeMedicalData
? [`Actions (${filteredActions.length})`, `Consultations (${filteredConsultations.length})`]
: [`Actions (${filteredActions.length})`];
const consultations = useMemo(() => {
if (activeTab.includes('Faites')) return consultationsCompletedAt;
if (activeTab.includes(`Annulées`)) return consultationsCanceledAt;
return consultationsCreatedAt;
}, [activeTab, consultationsCompletedAt, consultationsCanceledAt, consultationsCreatedAt]);

const data = useMemo(() => {
if (showActions && showConsultations) return [...actions, ...consultations];
if (showActions) return actions;
if (showConsultations) return consultations;
return [];
}, [activeTab, showActions, showConsultations, actions, consultations]);

const tabs = [
`Faites (${canSeeMedicalData ? actionsCompletedAt.length + consultationsCompletedAt.length : actionsCompletedAt.length})`,
`Annulées (${canSeeMedicalData ? actionsCanceledAt.length + consultationsCanceledAt.length : actionsCanceledAt.length})`,
`Créées (${canSeeMedicalData ? actionsCreatedAt.length + consultationsCreatedAt.length : actionsCreatedAt.length})`,
];

return (
<>
Expand All @@ -36,15 +84,19 @@ export const ActionsOrConsultationsReport = ({ actions, consultations, period })
className="tw-m-0 tw-flex-wrap tw-justify-start tw-border-b-0 tw-py-0.5 tw-pl-0 [&_button]:tw-text-xl"
tabs={tabs}
renderTab={(caption) => <h3 className="tw-m-0 tw-text-base tw-font-medium">{caption}</h3>}
onClick={(_, index) => setActiveTab(index === 0 ? 'Actions' : 'Consultations')}
activeTabIndex={activeTab.includes('Actions') ? 0 : 1}
onClick={(_, index) => {
if (index === 0) setActiveTab('Faites');
if (index === 1) setActiveTab('Annulées');
if (index === 2) setActiveTab('Créées');
}}
activeTabIndex={activeTabIndex}
/>
<div className="flex-col tw-flex tw-items-center tw-gap-2">
<button
aria-label="Ajouter une action"
className={[
'tw-text-md tw-h-8 tw-w-8 tw-rounded-full tw-font-bold tw-text-white tw-transition hover:tw-scale-125',
activeTab.includes('Actions') ? 'tw-bg-main' : 'tw-bg-blue-900',
activeTab.includes('Consultations') ? 'tw-bg-blue-900' : 'tw-bg-main',
].join(' ')}
onClick={() => {
const searchParams = new URLSearchParams(history.location.search);
Expand All @@ -59,7 +111,7 @@ export const ActionsOrConsultationsReport = ({ actions, consultations, period })
title="Passer les actions/consultations en plein écran"
className={[
'tw-h-6 tw-w-6 tw-rounded-full tw-transition hover:tw-scale-125 disabled:tw-cursor-not-allowed disabled:tw-opacity-30',
activeTab.includes('Actions') ? 'tw-text-main' : 'tw-text-blue-900',
activeTab.includes('Consultations') ? 'tw-text-blue-900' : 'tw-text-main',
].join(' ')}
disabled={!data.length}
onClick={() => setFullScreen(true)}>
Expand All @@ -68,58 +120,37 @@ export const ActionsOrConsultationsReport = ({ actions, consultations, period })
</div>
</div>
<div className="w-full tw-max-w-lg tw-bg-white tw-px-7 tw-pb-1">
<ActionsOrConsultationsFilters setFilterStatus={setFilterStatus} filterStatus={filterStatus} disabled={!data.length} />
</div>
<div className="tw-grow tw-overflow-y-auto tw-border-t tw-border-main tw-border-opacity-20">
<ActionsSortableList data={filteredData} />
</div>
</section>
<section
aria-hidden="true"
className="printonly tw-flex tw-h-full tw-flex-col tw-overflow-hidden tw-rounded-lg tw-border tw-border-zinc-200 tw-shadow">
<div className="tw-flex tw-flex-col tw-items-stretch tw-bg-white tw-px-3 tw-py-3">
<h3 className="tw-m-0 tw-text-base tw-font-medium">Actions ({filteredActions.length})</h3>
{filterStatus.length > 0 && (
<h4 className="tw-m-0 tw-text-base tw-font-medium">
Filtrées par status:{' '}
{mappedIdsToLabels
.filter((s) => filterStatus.includes(s._id))
.map((status) => status.name)
.join(', ')}
</h4>
)}
</div>
<div className="tw-grow tw-overflow-y-auto tw-border-t tw-border-main tw-border-opacity-20">
<ActionsSortableList data={filteredActions} />
</div>
</section>
<section
aria-hidden="true"
className="printonly tw-mt-12 tw-flex tw-h-full tw-flex-col tw-overflow-hidden tw-rounded-lg tw-border tw-border-zinc-200 tw-shadow">
<div className="tw-flex tw-flex-col tw-items-stretch tw-bg-white tw-px-3 tw-py-3">
<h3 className="tw-m-0 tw-text-base tw-font-medium">Consultations ({filteredConsultations.length})</h3>
{filterStatus.length > 0 && (
<h4 className="tw-m-0 tw-text-base tw-font-medium">
Filtrées par status:{' '}
{mappedIdsToLabels
.filter((s) => filterStatus.includes(s._id))
.map((status) => status.name)
.join(', ')}
</h4>
)}
<ActionsOrConsultationsFilters
showActions={showActions}
showConsultations={showConsultations}
setShowActions={setShowActions}
setShowConsultations={setShowConsultations}
numberOfActions={actions.length}
numberOfConsultations={consultations.length}
/>
</div>
<div className="tw-grow tw-overflow-y-auto tw-border-t tw-border-main tw-border-opacity-20">
<ActionsSortableList data={filteredConsultations} />
<ActionsSortableList data={data} />
</div>
</section>

<ModalContainer open={!!fullScreen} className="" size="full" onClose={() => setFullScreen(false)}>
<ModalHeader title={`${activeTab} (${filteredData.length})`} onClose={() => setFullScreen(false)}>
<ModalHeader
title={`${canSeeMedicalData ? 'Actions et Consultations' : 'Actions'} ${activeTab} (${data.length})`}
onClose={() => setFullScreen(false)}>
<div className="tw-mx-auto tw-mt-2 tw-w-full tw-max-w-lg">
<ActionsOrConsultationsFilters setFilterStatus={setFilterStatus} filterStatus={filterStatus} disabled={!data.length} />
<ActionsOrConsultationsFilters
showActions={showActions}
showConsultations={showConsultations}
setShowActions={setShowActions}
setShowConsultations={setShowConsultations}
numberOfActions={actions.length}
numberOfConsultations={consultations.length}
/>
</div>
</ModalHeader>
<ModalBody>
<ActionsSortableList data={filteredData} />
<ActionsSortableList data={data} showCreatedAt />
</ModalBody>
<ModalFooter>
<button type="button" name="cancel" className="button-cancel" onClick={() => setFullScreen(false)}>
Expand All @@ -141,28 +172,36 @@ export const ActionsOrConsultationsReport = ({ actions, consultations, period })
);
};

const ActionsOrConsultationsFilters = ({ setFilterStatus, filterStatus, disabled }) => {
const ActionsOrConsultationsFilters = ({
setShowActions,
setShowConsultations,
showActions,
showConsultations,
numberOfActions,
numberOfConsultations,
}) => {
return (
<>
<div className="tw-flex tw-justify-between">
<div className="tw-flex tw-w-full tw-shrink-0 tw-grow tw-items-center tw-pl-1 tw-pr-2">
<label htmlFor="action-select-status-filter" className="tw-text-xs">
Filtrer par statut
</label>
<div className="tw-w-full">
<SelectCustom
inputId="action-select-status-filter"
options={mappedIdsToLabels}
getOptionValue={(s) => s._id}
getOptionLabel={(s) => s.name}
name="status"
onChange={(s) => setFilterStatus(s.map((s) => s._id))}
isClearable
isDisabled={disabled}
isMulti
value={mappedIdsToLabels.filter((s) => filterStatus.includes(s._id))}
/>
</div>
<div className="tw-flex tw-w-full tw-justify-between tw-gap-x-4">
<div className="tw-flex tw-shrink-0 tw-grow tw-basis-full tw-items-center tw-gap-x-4 tw-pl-1 tw-pr-2">
<button
type="button"
className={[
'tw-inline-flex tw-items-center tw-gap-x-2 tw-rounded-md tw-border tw-border-main/20 tw-py-1 tw-px-4',
showActions ? '' : 'tw-opacity-50',
].join(' ')}
onClick={() => setShowActions((show) => !show)}>
<EyeIcon size={15} strikedThrough={!showActions} /> Actions ({numberOfActions})
</button>
<button
type="button"
className={[
'tw-inline-flex tw-items-center tw-gap-x-2 tw-rounded-md tw-border tw-border-main/20 tw-py-1 tw-px-4',
showConsultations ? '' : 'tw-opacity-50',
].join(' ')}
onClick={() => setShowConsultations((show) => !show)}>
<EyeIcon size={15} strikedThrough={!showConsultations} /> Consultations ({numberOfConsultations})
</button>
</div>
</div>
</>
Expand Down
Loading
Loading