diff --git a/dashboard/src/components/Card.js b/dashboard/src/components/Card.js index 2c42f3f4b..60f4868f1 100644 --- a/dashboard/src/components/Card.js +++ b/dashboard/src/components/Card.js @@ -1,7 +1,9 @@ import React from 'react'; import HelpButtonAndModal from './HelpButtonAndModal'; -const Card = ({ title, count, unit, children, countId, dataTestId, help }) => { +const Card = ({ title, count, unit, children, countId, dataTestId, help, onClick = null }) => { + const Component = !!onClick ? 'button' : 'div'; + const props = !!onClick ? { onClick, type: 'button', name: 'card', className: 'button-cancel' } : {}; return ( <>
@@ -12,12 +14,12 @@ const Card = ({ title, count, unit, children, countId, dataTestId, help }) => {

)} -
+ {count} {!!unit && {unit}} -
+ {children} diff --git a/dashboard/src/scenes/report/components/ObservationsReport.js b/dashboard/src/scenes/report/components/ObservationsReport.js index 2e6986bf5..da8f83d86 100644 --- a/dashboard/src/scenes/report/components/ObservationsReport.js +++ b/dashboard/src/scenes/report/components/ObservationsReport.js @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; -import dayjs from 'dayjs'; +import { useState } from 'react'; import { useRecoilValue } from 'recoil'; +import { utils, writeFile } from 'xlsx'; import { ModalHeader, ModalBody, ModalContainer, ModalFooter } from '../../../components/tailwind/Modal'; import { FullScreenIcon } from '../../../assets/icons/FullScreenIcon'; import Table from '../../../components/table'; @@ -9,6 +9,9 @@ import DateBloc from '../../../components/DateBloc'; import CreateObservation from '../../../components/CreateObservation'; import Observation from '../../territory-observations/view'; import { territoriesState } from '../../../recoil/territory'; +import { dayjsInstance } from '../../../services/date'; +import { teamsState, usersState } from '../../../recoil/auth'; +import { customFieldsObsSelector } from '../../../recoil/territoryObservations'; export const ObservationsReport = ({ observations, period, selectedTeams }) => { const [fullScreen, setFullScreen] = useState(false); @@ -44,18 +47,70 @@ const ObservationsTable = ({ period, observations, selectedTeams }) => { const [observationToEdit, setObservationToEdit] = useState({}); const [openObservationModaleKey, setOpenObservationModaleKey] = useState(0); const territories = useRecoilValue(territoriesState); + const teams = useRecoilValue(teamsState); + const customFieldsObs = useRecoilValue(customFieldsObsSelector); + const users = useRecoilValue(usersState); + + const exportXlsx = () => { + const wb = utils.book_new(); + const formattedData = utils.json_to_sheet( + observations.map((observation) => { + return { + id: observation._id, + 'Territoire - Nom': territories.find((t) => t._id === observation.territory)?.name, + 'Observé le': dayjsInstance(observation.observedAt).format('YYYY-MM-DD HH:mm'), + Équipe: observation.team ? teams.find((t) => t._id === observation.team)?.name : '', + ...customFieldsObs.reduce((fields, field) => { + if (['date', 'date-with-time'].includes(field.type)) + fields[field.label || field.name] = observation[field.name] + ? dayjsInstance(observation[field.name]).format(field.type === 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm') + : ''; + else if (['boolean'].includes(field.type)) fields[field.label || field.name] = observation[field.name] ? 'Oui' : 'Non'; + else if (['yes-no'].includes(field.type)) fields[field.label || field.name] = observation[field.name]; + else if (Array.isArray(observation[field.name])) fields[field.label || field.name] = observation[field.name].join(', '); + else fields[field.label || field.name] = observation[field.name]; + return fields; + }, {}), + 'Créée par': users.find((u) => u._id === observation.user)?.name, + 'Créée le': dayjsInstance(observation.createdAt).format('YYYY-MM-DD HH:mm'), + 'Mise à jour le': dayjsInstance(observation.updatedAt).format('YYYY-MM-DD HH:mm'), + }; + }) + ); + utils.book_append_sheet(wb, formattedData, 'Observations de territoires'); + + utils.book_append_sheet(wb, utils.json_to_sheet(observations), 'Observations (données brutes)'); + utils.book_append_sheet(wb, utils.json_to_sheet(territories), 'Territoires (données brutes)'); + utils.book_append_sheet(wb, utils.json_to_sheet(selectedTeams), 'Filtres (équipes)'); + const otherFilters = [ + { + 'Période - début': period.startDate, + 'Période - fin': period.endDate, + }, + ]; + utils.book_append_sheet(wb, utils.json_to_sheet(otherFilters), 'Filtres (autres)'); + writeFile( + wb, + `Compte rendu (${dayjsInstance(period.startDate).format('YYYY-MM-DD')} - ${dayjsInstance(period.endDate).format( + 'YYYY-MM-DD' + )}) - Observations de territoires (${observations.length}).xlsx` + ); + }; return ( <>

Observations

+
} + dataTestId={dataTestId} + help={help?.(col.title.capitalize())} + onClick={col.onBlockClick ? col.onBlockClick : null} + />
))} {customFieldsInStats.map((field) => { diff --git a/dashboard/src/scenes/stats/ObservationsStats.js b/dashboard/src/scenes/stats/ObservationsStats.js index b1bf94a45..d9c90af0a 100644 --- a/dashboard/src/scenes/stats/ObservationsStats.js +++ b/dashboard/src/scenes/stats/ObservationsStats.js @@ -1,8 +1,40 @@ -import React from 'react'; +import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { utils, writeFile } from 'xlsx'; import SelectCustom from '../../components/SelectCustom'; import CustomFieldsStats from './CustomFieldsStats'; +import { ModalBody, ModalContainer, ModalFooter, ModalHeader } from '../../components/tailwind/Modal'; +import { teamsState, usersState } from '../../recoil/auth'; +import TagTeam from '../../components/TagTeam'; +import Table from '../../components/table'; +import { dayjsInstance } from '../../services/date'; +import { customFieldsObsSelector } from '../../recoil/territoryObservations'; +import CreateObservation from '../../components/CreateObservation'; +import { filterData } from '../../components/Filters'; +import DateBloc from '../../components/DateBloc'; +import Observation from '../../scenes/territory-observations/view'; -const ObservationsStats = ({ territories, setSelectedTerritories, observations, customFieldsObs }) => { +const ObservationsStats = ({ territories, setSelectedTerritories, observations, customFieldsObs, allFilters }) => { + const [obsModalOpened, setObsModalOpened] = useState(false); + const [sliceField, setSliceField] = useState(null); + const [sliceValue, setSliceValue] = useState(null); + const [slicedData, setSlicedData] = useState([]); + + const onSliceClick = (newSlice, fieldName, observationsConcerned = observations) => { + const newSlicefield = customFieldsObs.find((f) => f.name === fieldName); + setSliceField(newSlicefield); + setSliceValue(newSlice); + const slicedData = + newSlicefield.type === 'boolean' + ? observationsConcerned.filter((p) => (newSlice === 'Non' ? !p[newSlicefield.field] : !!p[newSlicefield.field])) + : filterData( + observationsConcerned, + [{ ...newSlicefield, value: newSlice, type: newSlicefield.field === 'outOfActiveList' ? 'boolean' : newSlicefield.field }], + true + ); + setSlicedData(slicedData); + setObsModalOpened(true); + }; return ( <>

Statistiques des observations de territoire

@@ -25,11 +57,16 @@ const ObservationsStats = ({ territories, setSelectedTerritories, observations, { + setSlicedData(observations); + setObsModalOpened(true); + }, }, ]} help={(label) => @@ -37,6 +74,144 @@ const ObservationsStats = ({ territories, setSelectedTerritories, observations, } totalTitleForMultiChoice={Nombre d'observations concernées} /> + { + setObsModalOpened(false); + }} + observations={slicedData} + sliceField={sliceField} + onAfterLeave={() => { + setSliceField(null); + setSliceValue(null); + setSlicedData([]); + }} + title={`${sliceField?.label ?? 'Observations de territoire'}${sliceValue ? ` : ${sliceValue}` : ''} (${slicedData.length})`} + territories={territories} + allFilters={allFilters} + /> + + ); +}; + +const SelectedObsModal = ({ open, onClose, observations, territories, title, onAfterLeave, allFilters }) => { + const [observationToEdit, setObservationToEdit] = useState({}); + const [openObservationModaleKey, setOpenObservationModaleKey] = useState(0); + const teams = useRecoilValue(teamsState); + const customFieldsObs = useRecoilValue(customFieldsObsSelector); + const users = useRecoilValue(usersState); + + const exportXlsx = () => { + const wb = utils.book_new(); + const formattedData = utils.json_to_sheet( + observations.map((observation) => { + return { + id: observation._id, + 'Territoire - Nom': territories.find((t) => t._id === observation.territory)?.name, + 'Observé le': dayjsInstance(observation.observedAt).format('YYYY-MM-DD HH:mm'), + Équipe: observation.team ? teams.find((t) => t._id === observation.team)?.name : '', + ...customFieldsObs.reduce((fields, field) => { + if (['date', 'date-with-time'].includes(field.type)) + fields[field.label || field.name] = observation[field.name] + ? dayjsInstance(observation[field.name]).format(field.type === 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm') + : ''; + else if (['boolean'].includes(field.type)) fields[field.label || field.name] = observation[field.name] ? 'Oui' : 'Non'; + else if (['yes-no'].includes(field.type)) fields[field.label || field.name] = observation[field.name]; + else if (Array.isArray(observation[field.name])) fields[field.label || field.name] = observation[field.name].join(', '); + else fields[field.label || field.name] = observation[field.name]; + return fields; + }, {}), + 'Créée par': users.find((u) => u._id === observation.user)?.name, + 'Créée le': dayjsInstance(observation.createdAt).format('YYYY-MM-DD HH:mm'), + 'Mise à jour le': dayjsInstance(observation.updatedAt).format('YYYY-MM-DD HH:mm'), + }; + }) + ); + utils.book_append_sheet(wb, formattedData, 'Observations de territoires'); + + utils.book_append_sheet(wb, utils.json_to_sheet(observations), 'Observations (données brutes)'); + utils.book_append_sheet(wb, utils.json_to_sheet(territories), 'Territoires (données brutes)'); + utils.book_append_sheet(wb, utils.json_to_sheet(allFilters.selectedTerritories), 'Filtres (territoires)'); + utils.book_append_sheet(wb, utils.json_to_sheet(allFilters.selectedTeams), 'Filtres (équipes)'); + const otherFilters = [ + { + 'Période - début': allFilters.period.startDate, + 'Période - fin': allFilters.period.endDate, + }, + ]; + utils.book_append_sheet(wb, utils.json_to_sheet(otherFilters), 'Filtres (autres)'); + writeFile( + wb, + `Statistiques (${dayjsInstance(allFilters.period.startDate).format('YYYY-MM-DD')} - ${dayjsInstance(allFilters.period.endDate).format( + 'YYYY-MM-DD' + )}) - ${title}.xlsx` + ); + }; + + return ( + <> + + + {title}{' '} + + + } + /> + +
+ { + setObservationToEdit(obs); + setOpenObservationModaleKey((k) => k + 1); + }} + rowKey={'_id'} + columns={[ + { + title: 'Date', + dataKey: 'observedAt', + render: (obs) => { + // anonymous comment migrated from `report.observations` + // have no time + // have no user assigned either + const time = dayjsInstance(obs.observedAt).format('D MMM HH:mm'); + return ( + <> + + {time === '00:00' && !obs.user ? null : time} + + ); + }, + }, + { title: 'Territoire', dataKey: 'territory', render: (obs) => territories.find((t) => t._id === obs.territory)?.name }, + { title: 'Observation', dataKey: 'entityKey', render: (obs) => , left: true }, + { + title: 'Équipe en charge', + dataKey: 'team', + render: (obs) => , + }, + ]} + /> + + + + + + + ); }; diff --git a/dashboard/src/scenes/stats/index.js b/dashboard/src/scenes/stats/index.js index ba58b8ae3..aa4ea47a6 100644 --- a/dashboard/src/scenes/stats/index.js +++ b/dashboard/src/scenes/stats/index.js @@ -669,6 +669,13 @@ const Stats = () => { setSelectedTerritories={setSelectedTerritories} observations={observations} customFieldsObs={customFieldsObs} + // `allFilters` is for debug purpose only + // TODO: remove when debugged + allFilters={{ + selectedTerritories, + period, + selectedTeams, + }} /> )} {activeTab === 'Comptes-rendus' && }