From 4c84e3243c04d074044aa6be3f3d6cf5dc06217b Mon Sep 17 00:00:00 2001 From: Nicolas KREMER Date: Tue, 17 Dec 2024 18:01:26 +0100 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20mise=20=C3=A0=20jour=20du=20tableau?= =?UTF-8?q?=20des=20effectifs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../specific.routes/organisme.routes.ts | 222 +++++++-- server/src/http/server.ts | 13 +- ui/common/utils/stringUtils.ts | 5 + ui/components/Table/TableWithApi.tsx | 273 ++++++++++ ui/components/skeletons/RowsSkeleton.tsx | 55 +-- .../OrganismesDoublonsList.tsx | 2 +- ui/modules/effectifs/EffectifsPage.tsx | 385 +++++++-------- ui/modules/indicateurs/IndicateursForm.tsx | 2 +- ui/modules/indicateurs/NewTable.tsx | 178 ++++--- .../effectifs/engine/EffectifsTable.tsx | 467 ------------------ .../effectifsTable/EffectifsColumns.tsx | 162 ++++++ .../EffectifsFilterComponent.tsx | 51 ++ .../effectifsTable/EffectifsFilterPanel.tsx | 93 ++++ .../engine/effectifsTable/EffectifsTable.tsx | 211 ++++++++ ui/modules/organismes/OrganismesTable.tsx | 1 + 15 files changed, 1314 insertions(+), 806 deletions(-) create mode 100644 ui/components/Table/TableWithApi.tsx delete mode 100644 ui/modules/mon-espace/effectifs/engine/EffectifsTable.tsx create mode 100644 ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsColumns.tsx create mode 100644 ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsFilterComponent.tsx create mode 100644 ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsFilterPanel.tsx create mode 100644 ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsTable.tsx diff --git a/server/src/http/routes/specific.routes/organisme.routes.ts b/server/src/http/routes/specific.routes/organisme.routes.ts index bc2a08b87..a7442d888 100644 --- a/server/src/http/routes/specific.routes/organisme.routes.ts +++ b/server/src/http/routes/specific.routes/organisme.routes.ts @@ -1,5 +1,4 @@ -import { compact, get } from "lodash-es"; -import { ObjectId } from "mongodb"; +import { ObjectId } from "bson"; import { getAnneesScolaireListFromDate, getSIFADate, @@ -7,58 +6,185 @@ import { requiredFieldsSifa, } from "shared"; -import { isEligibleSIFA } from "@/common/actions/sifa.actions/sifa.actions"; -import { effectifsDECADb, effectifsDb, organismesDb } from "@/common/model/collections"; +// import { isEligibleSIFA } from "@/common/actions/sifa.actions/sifa.actions"; +import { effectifsDb, organismesDb } from "@/common/model/collections"; -export async function getOrganismeEffectifs(organismeId: ObjectId, sifa = false) { +export async function getOrganismeEffectifs( + organismeId, + sifa = false, + options: { + pageIndex: number; + pageSize: number; + search: string; + filters: any; + sortField: string | null; + sortOrder: string | null; + } = { pageIndex: 0, pageSize: 10, search: "", filters: {}, sortField: null, sortOrder: null } +) { const organisme = await organismesDb().findOne({ _id: organismeId }); - const isDeca = !organisme?.is_transmission_target; - const db = isDeca ? effectifsDECADb() : effectifsDb(); + const { pageIndex, pageSize, search, filters, sortField, sortOrder } = options; + const db = await effectifsDb(); + + const parsedFilters = Object.entries(filters).reduce( + (acc, [key, value]) => { + if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) { + try { + const parsed = JSON.parse(value); + acc[key] = Array.isArray(parsed) ? parsed : [parsed]; + } catch { + acc[key] = [value]; + } + } else if (Array.isArray(value)) { + acc[key] = value; + } + return acc; + }, + {} as Record + ); + + const matchConditions = { + organisme_id: organismeId, + ...(sifa && { + annee_scolaire: { + $in: getAnneesScolaireListFromDate(sifa ? getSIFADate(new Date()) : new Date()), + }, + }), + ...Object.keys(parsedFilters).reduce((acc, key) => { + if (parsedFilters[key]?.length > 0) { + const fieldKey = + key === "formation_libelle_long" + ? "formation.libelle_long" + : key === "statut_courant" + ? "_computed.statut.en_cours" + : key; + acc[fieldKey] = { $in: parsedFilters[key] }; + } + return acc; + }, {}), + ...(typeof search === "string" && + search.trim() && { + $or: [ + { "apprenant.nom": { $regex: search.trim(), $options: "i" } }, + { "apprenant.prenom": { $regex: search.trim(), $options: "i" } }, + ], + }), + }; + + const fieldMap: Record = { + nom: "apprenant.nom", + prenom: "apprenant.prenom", + formation: "formation.libelle_long", + statut_courant: "_computed.statut.en_cours", + annee_scolaire: "annee_scolaire", + }; + + const sortConditions = + sortField && sortOrder + ? { [fieldMap[sortField] || sortField]: sortOrder === "desc" ? -1 : 1 } + : { "formation.annee_scolaire": -1 }; - const effectifs = await db - .find({ - organisme_id: organismeId, - ...(sifa - ? { - annee_scolaire: { - $in: getAnneesScolaireListFromDate(sifa ? getSIFADate(new Date()) : new Date()), + const pipeline = [ + { + $facet: { + allFilters: [ + { + $match: { + organisme_id: organismeId, + ...(sifa && { + annee_scolaire: { + $in: getAnneesScolaireListFromDate(sifa ? getSIFADate(new Date()) : new Date()), + }, + }), + }, + }, + { + $group: { + _id: null, + annee_scolaire: { $addToSet: "$annee_scolaire" }, + source: { $addToSet: "$source" }, + statut_courant: { $addToSet: "$_computed.statut.en_cours" }, + formation_libelle_long: { $addToSet: "$formation.libelle_long" }, + }, + }, + { + $project: { + _id: 0, + annee_scolaire: 1, + source: 1, + statut_courant: 1, + formation_libelle_long: 1, + }, + }, + ], + results: [ + { $match: matchConditions }, + { $sort: sortConditions }, + { $skip: pageIndex * pageSize }, + { $limit: pageSize }, + { + $project: { + id: { $toString: "$_id" }, + id_erp_apprenant: 1, + organisme_id: 1, + annee_scolaire: 1, + source: 1, + validation_errors: 1, + formation: 1, + nom: "$apprenant.nom", + prenom: "$apprenant.prenom", + date_de_naissance: "$apprenant.date_de_naissance", + historique_statut: "$apprenant.historique_statut", + statut: "$_computed.statut", + ...(sifa + ? { + requiredSifa: { + $filter: { + input: { + $setUnion: [ + requiredFieldsSifa, + { + $cond: [{ $not: "$apprenant.adresse.complete" }, requiredApprenantAdresseFieldsSifa, []], + }, + ], + }, + as: "fieldName", + cond: { $or: [{ $not: { $ifNull: ["$$fieldName", false] } }] }, + }, + }, + } + : {}), }, - } - : {}), - }) - .toArray(); + }, + ], + totalCount: [ + { $match: matchConditions }, + { + $count: "count", + }, + ], + }, + }, + { + $project: { + filters: { + annee_scolaire: { $arrayElemAt: ["$allFilters.annee_scolaire", 0] }, + source: { $arrayElemAt: ["$allFilters.source", 0] }, + statut_courant: { $arrayElemAt: ["$allFilters.statut_courant", 0] }, + formation_libelle_long: { $arrayElemAt: ["$allFilters.formation_libelle_long", 0] }, + }, + results: "$results", + total: { $arrayElemAt: ["$totalCount.count", 0] }, + }, + }, + ]; + + const [data] = await db.aggregate(pipeline).toArray(); return { - fromDECA: isDeca, - organismesEffectifs: effectifs - .filter((effectif) => !sifa || isEligibleSIFA(effectif._computed?.statut)) - .map((effectif) => ({ - id: effectif._id.toString(), - id_erp_apprenant: effectif.id_erp_apprenant, - organisme_id: organismeId, - annee_scolaire: effectif.annee_scolaire, - source: effectif.source, - validation_errors: effectif.validation_errors, - formation: effectif.formation, - nom: effectif.apprenant.nom, - prenom: effectif.apprenant.prenom, - date_de_naissance: effectif.apprenant.date_de_naissance, - historique_statut: effectif.apprenant.historique_statut, - statut: effectif._computed?.statut, - ...(sifa - ? { - requiredSifa: compact( - [ - ...(!effectif.apprenant.adresse?.complete - ? [...requiredFieldsSifa, ...requiredApprenantAdresseFieldsSifa] - : requiredFieldsSifa), - ].map((fieldName) => - !get(effectif, fieldName) || get(effectif, fieldName) === "" ? fieldName : undefined - ) - ), - } - : {}), - })), + fromDECA: !organisme?.is_transmission_target, + total: data?.total || 0, + filters: data?.filters || {}, + organismesEffectifs: data?.results || [], }; } diff --git a/server/src/http/server.ts b/server/src/http/server.ts index 5a4194069..0a8a52809 100644 --- a/server/src/http/server.ts +++ b/server/src/http/server.ts @@ -570,7 +570,18 @@ function setupRoutes(app: Application) { "/effectifs", requireOrganismePermission("manageEffectifs"), returnResult(async (req, res) => { - return await getOrganismeEffectifs(res.locals.organismeId, req.query.sifa === "true"); + const { pageIndex, pageSize, search, sortField, sortOrder, ...filters } = req.query; + + const options = { + pageIndex: parseInt(pageIndex, 10) || 0, + pageSize: parseInt(pageSize, 10) || 10, + search: search[0] || "", + filters, + sortField, + sortOrder, + }; + + return await getOrganismeEffectifs(res.locals.organismeId, req.query.sifa === "true", options); }) ) .put( diff --git a/ui/common/utils/stringUtils.ts b/ui/common/utils/stringUtils.ts index 1420307a8..abef76f9e 100644 --- a/ui/common/utils/stringUtils.ts +++ b/ui/common/utils/stringUtils.ts @@ -39,3 +39,8 @@ export const toPascalCase = (string) => .replace(new RegExp(/[^\w\s]/, "g"), "") .replace(new RegExp(/\s+(.)(\w*)/, "g"), ($1, $2, $3) => `${$2.toUpperCase() + $3}`) .replace(new RegExp(/\w/), (s) => s.toUpperCase()); + +export function capitalizeWords(str: string): string { + const formattedString = str.toLowerCase().replace(/_/g, " "); + return formattedString.charAt(0).toUpperCase() + formattedString.slice(1); +} diff --git a/ui/components/Table/TableWithApi.tsx b/ui/components/Table/TableWithApi.tsx new file mode 100644 index 000000000..21a9509d0 --- /dev/null +++ b/ui/components/Table/TableWithApi.tsx @@ -0,0 +1,273 @@ +import { ChevronLeftIcon, ChevronRightIcon } from "@chakra-ui/icons"; +import { Box, Button, Flex, HStack, Select, SystemProps, Table, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react"; +import { + ColumnDef, + PaginationState, + Row, + SortingState, + flexRender, + functionalUpdate, + getCoreRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { Fragment, useState, useMemo } from "react"; + +import { FirstPageIcon, LastPageIcon } from "@/modules/dashboard/icons"; +import { AddFill, SubtractLine } from "@/theme/components/icons"; + +interface TableWithApiProps extends SystemProps { + columns: ColumnDef[]; + data: T[]; + total?: number; + noDataMessage?: string; + expandAllRows?: boolean; + enableRowExpansion?: boolean; + sortingState?: SortingState; + paginationState?: PaginationState; + variant?: string; + showPagination?: boolean; + isLoading?: boolean; + onSortingChange?: (state: SortingState) => any; + onPaginationChange?: (state: PaginationState) => any; + renderSubComponent?: (row: Row) => React.ReactElement; + renderDivider?: () => React.ReactElement; +} + +function TableWithApi(props: TableWithApiProps) { + const [expandedRows, setExpandedRows] = useState>( + props.expandAllRows ? new Set(props.data.map((row) => row.id)) : new Set() + ); + + const totalPages = useMemo(() => { + if (!props.total) return 1; + return Math.ceil(props.total / (props.paginationState?.pageSize || 20)); + }, [props.total, props.paginationState?.pageSize]); + + const toggleRowExpansion = (rowId: string) => { + setExpandedRows((prev) => { + const newSet = new Set(prev); + if (newSet.has(rowId)) { + newSet.delete(rowId); + } else { + newSet.add(rowId); + } + return newSet; + }); + }; + + const table = useReactTable({ + data: props.data, + columns: props.columns, + pageCount: totalPages, + state: { + pagination: props.paginationState ?? { + pageIndex: 0, + pageSize: 20, + }, + sorting: props.sortingState, + }, + manualPagination: true, + onPaginationChange: (updater) => { + const newState = functionalUpdate(updater, table.getState().pagination); + props.onPaginationChange?.(newState); + }, + onSortingChange: (updater) => { + const newState = functionalUpdate(updater, table.getState().sorting); + props.onSortingChange?.(newState); + }, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); + + const { pageIndex } = table.getState().pagination; + + return ( + <> + + + {table.getHeaderGroups().map((headerGroup, index) => ( + + {headerGroup.headers.map((header, headerIndex) => ( + + ))} + {props.enableRowExpansion && } + + ))} + + + {!props.data || props.data.length === 0 ? ( + + + + ) : ( + table.getRowModel().rows.map((row) => { + const isExpanded = expandedRows.has(row.original.id); + + return ( + + props.enableRowExpansion && toggleRowExpansion(row.original.id)} + cursor={props.enableRowExpansion ? "pointer" : "default"} + _hover={props.enableRowExpansion ? { backgroundColor: "gray.100" } : undefined} + > + {row.getVisibleCells().map((cell, cellIndex) => ( + + ))} + {props.enableRowExpansion && ( + + )} + + + {isExpanded && props.renderSubComponent && ( + + + + )} + + {props.renderDivider && ( + + + + )} + + ); + }) + )} + +
div > .sort-icon": { + display: "inline-block", + color: "black", + }, + } + : undefined + } + paddingBottom={3} + bg="white" + > + {header.isPlaceholder ? null : ( + + {flexRender(header.column.columnDef.header, header.getContext())} + + {header.column.getIsSorted() === "desc" && } + {header.column.getIsSorted() === "asc" && } + {header.column.getIsSorted() === false && ( + + ▼ + + )} + + + )} +
+ {props.noDataMessage ?? "Aucun résultat"} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} + + + + +
+ {props.renderSubComponent(row)} +
+ {props.renderDivider()} +
+ + {props.showPagination !== false && ( + + + + + + {pageIndex - 1 > 0 && ( + + )} + {pageIndex > 0 && ( + + )} + + {pageIndex + 1 < totalPages && ( + + )} + {pageIndex + 2 < totalPages && ( + + )} + + + + + + + + + + )} + + ); +} + +export default TableWithApi; diff --git a/ui/components/skeletons/RowsSkeleton.tsx b/ui/components/skeletons/RowsSkeleton.tsx index aaf07941d..86fcaa1b3 100644 --- a/ui/components/skeletons/RowsSkeleton.tsx +++ b/ui/components/skeletons/RowsSkeleton.tsx @@ -1,35 +1,34 @@ import { Skeleton, Td, Tr } from "@chakra-ui/react"; -import React from "react"; +import { memo } from "react"; -const RowsSkeleton = ({ - nbRows = 5, - nbColumns = 5, - height = "1rem", -}: { +interface Props { nbRows?: number; nbColumns?: number; height?: string; -}) => { - const rows = Array.from({ length: nbRows }, (_, i) => i); - const columns = Array.from({ length: nbColumns }, (_, j) => j); +} - return ( - <> - {rows.map((i) => { - return ( - - {columns.map((j) => { - return ( - - - - ); - })} - - ); - })} - - ); -}; +const Cell = memo(({ height }: { height: string }) => ( + + + +)); +Cell.displayName = "Cell"; -export default RowsSkeleton; +const Row = memo(({ cols, height }: { cols: number; height: string }) => ( + + {Array.from({ length: cols }, (_, i) => ( + + ))} + +)); +Row.displayName = "Row"; + +const RowsSkeleton = ({ nbRows = 5, nbColumns = 5, height = "1rem" }: Props) => ( + <> + {Array.from({ length: nbRows }, (_, i) => ( + + ))} + +); + +export default memo(RowsSkeleton); diff --git a/ui/modules/admin/fusion-organismes/OrganismesDoublonsList.tsx b/ui/modules/admin/fusion-organismes/OrganismesDoublonsList.tsx index aa9ce683f..980309988 100644 --- a/ui/modules/admin/fusion-organismes/OrganismesDoublonsList.tsx +++ b/ui/modules/admin/fusion-organismes/OrganismesDoublonsList.tsx @@ -22,7 +22,7 @@ const OrganismesDoublonsList = ({ data }) => { data={data || []} loading={false} variant="third" - isRowExpanded={true} + expandAllRows={true} renderSubComponent={RenderSubComponent} paginationState={defaultPaginationState} columns={[ diff --git a/ui/modules/effectifs/EffectifsPage.tsx b/ui/modules/effectifs/EffectifsPage.tsx index 39665d738..49c92619d 100644 --- a/ui/modules/effectifs/EffectifsPage.tsx +++ b/ui/modules/effectifs/EffectifsPage.tsx @@ -1,36 +1,20 @@ import { AddIcon } from "@chakra-ui/icons"; -import { - Box, - Button, - Center, - Circle, - Container, - Heading, - HStack, - Spinner, - Switch, - Text, - VStack, -} from "@chakra-ui/react"; +import { Box, Container, Heading, HStack } from "@chakra-ui/react"; import { useQuery } from "@tanstack/react-query"; -import groupBy from "lodash.groupby"; +import { SortingState } from "@tanstack/react-table"; import { useRouter } from "next/router"; -import { useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { useSetRecoilState } from "recoil"; -import { DuplicateEffectifGroupPagination, EFFECTIFS_GROUP, getStatutApprenantNameFromCode } from "shared"; +import { DuplicateEffectifGroupPagination, EFFECTIFS_GROUP } from "shared"; -import { effectifsExportColumns } from "@/common/exports"; import { _get } from "@/common/httpClient"; import { Organisme } from "@/common/internal/Organisme"; -import { exportDataAsXlsx } from "@/common/utils/exportUtils"; -import DownloadButton from "@/components/buttons/DownloadButton"; import Link from "@/components/Links/Link"; import SupportLink from "@/components/Links/SupportLink"; import SimplePage from "@/components/Page/SimplePage"; import { effectifFromDecaAtom, effectifsStateAtom } from "../mon-espace/effectifs/engine/atoms"; -import EffectifsTableContainer from "../mon-espace/effectifs/engine/EffectifTableContainer"; -import { Input } from "../mon-espace/effectifs/engine/formEngine/components/Input/Input"; +import EffectifsTable from "../mon-espace/effectifs/engine/effectifsTable/EffectifsTable"; import BandeauTransmission from "../organismes/BandeauTransmission"; import BandeauDuplicatsEffectifs from "./BandeauDuplicatsEffectifs"; @@ -39,41 +23,185 @@ interface EffectifsPageProps { organisme: Organisme; modePublique: boolean; } -function EffectifsPage(props: EffectifsPageProps) { - // Booléen temporaire pour la désactivation temporaire du bouton de téléchargement de la liste - const TMP_DEACTIVATE_DOWNLOAD_BUTTON = true; +function EffectifsPage(props: EffectifsPageProps) { const router = useRouter(); const setCurrentEffectifsState = useSetRecoilState(effectifsStateAtom); const setEffectifFromDecaState = useSetRecoilState(effectifFromDecaAtom); - const [searchValue, setSearchValue] = useState(""); - const [showOnlyErrors, setShowOnlyErrors] = useState(false); - const [filtreAnneeScolaire, setFiltreAnneeScolaire] = useState("all"); - - const { - data: organismesEffectifs, - isFetching, - refetch, - } = useQuery(["organismes", props.organisme._id, "effectifs"], async () => { - const { fromDECA, organismesEffectifs } = await _get(`/api/v1/organismes/${props.organisme._id}/effectifs`); - // met à jour l'état de validation de chaque effectif (nécessaire pour le formulaire) - setCurrentEffectifsState( - organismesEffectifs.reduce((acc, { id, validation_errors }) => { - acc.set(id, { validation_errors, requiredSifa: [] }); - return acc; - }, new Map()) - ); - setEffectifFromDecaState(fromDECA); - return organismesEffectifs; - }); + + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const [search, setSearch] = useState(""); + const [filters, setFilters] = useState>({}); + const [sort, setSort] = useState([{ desc: true, id: "annee_scolaire" }]); + + useEffect(() => { + const parseQueryToFilters = (query: Record) => { + const filters: Record = {}; + Object.entries(query).forEach(([key, value]) => { + if ( + value && + key !== "pageIndex" && + key !== "pageSize" && + key !== "search" && + key !== "sortField" && + key !== "sortOrder" + ) { + try { + if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) { + const parsed = JSON.parse(decodeURIComponent(value)); + filters[key] = Array.isArray(parsed) ? parsed : [parsed]; + } else { + filters[key] = [decodeURIComponent(value)]; + } + } catch { + filters[key] = [decodeURIComponent(value)]; + } + } + }); + return filters; + }; + + const parsedFilters = parseQueryToFilters(router.query as Record); + setFilters(parsedFilters || {}); + }, [router.query]); + + const { data, isFetching } = useQuery( + ["organismes", props.organisme._id, "effectifs", pagination, search, filters, sort], + async () => { + const response = await _get(`/api/v1/organismes/${props.organisme._id}/effectifs`, { + params: { + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + search, + sortField: sort[0]?.id, + sortOrder: sort[0]?.desc ? "desc" : "asc", + ...filters, + }, + }); + + const { fromDECA, total, filters: returnedFilters, organismesEffectifs } = response; + + setCurrentEffectifsState( + organismesEffectifs.reduce((acc, { id, validation_errors }) => { + acc.set(id, { validation_errors, requiredSifa: [] }); + return acc; + }, new Map()) + ); + + setEffectifFromDecaState(fromDECA); + + return { total, filters: returnedFilters, organismesEffectifs }; + }, + { keepPreviousData: true } + ); const { data: duplicates } = useQuery(["organismes", props.organisme._id, "duplicates"], () => _get(`/api/v1/organismes/${props.organisme?._id}/duplicates`) ); - const effectifsByAnneeScolaire = useMemo(() => groupBy(organismesEffectifs, "annee_scolaire"), [organismesEffectifs]); + const handlePaginationChange = (newPagination) => { + setPagination(newPagination); + + router.push( + { + pathname: router.pathname, + query: { + ...router.query, + pageIndex: newPagination.pageIndex, + pageSize: newPagination.pageSize, + }, + }, + undefined, + { shallow: true } + ); + }; + + const handleSearchChange = (value: string) => { + setPagination({ ...pagination, pageIndex: 0 }); + setSearch(value); + + router.push( + { + pathname: router.pathname, + query: { ...router.query, search: value }, + }, + undefined, + { shallow: true } + ); + }; + + const handleFilterChange = (newFilters: Record) => { + setPagination({ ...pagination, pageIndex: 0 }); + const mergedFilters = { ...filters }; + + Object.entries(newFilters).forEach(([key, values]) => { + if (values.length > 0) { + mergedFilters[key] = values; + } else { + delete mergedFilters[key]; + } + }); + + const queryFilters = Object.entries(mergedFilters).reduce( + (acc, [key, values]) => { + acc[key] = JSON.stringify(values); + return acc; + }, + {} as Record + ); + + const updatedQuery = { ...router.query, ...queryFilters }; + Object.keys(router.query).forEach((key) => { + if (!queryFilters[key]) { + delete updatedQuery[key]; + } + }); + + setFilters(mergedFilters); + router.push( + { + pathname: router.pathname, + query: updatedQuery, + }, + undefined, + { shallow: true } + ); + }; + + const handleSortChange = (newSort: SortingState) => { + setPagination({ ...pagination, pageIndex: 0 }); + setSort(newSort); + + router.push( + { + pathname: router.pathname, + query: { + ...router.query, + sortField: newSort[0]?.id, + sortOrder: newSort[0]?.desc ? "desc" : "asc", + }, + }, + undefined, + { shallow: true } + ); + }; + + const resetFilters = () => { + setFilters({}); + setSearch(""); + + router.push( + { + pathname: router.pathname, + query: {}, + }, + undefined, + { shallow: true } + ); + }; const title = `${props.modePublique ? "Ses" : "Mes"} effectifs`; + return ( @@ -90,48 +218,10 @@ function EffectifsPage(props: EffectifsPageProps) { Ajouter via fichier Excel - {!TMP_DEACTIVATE_DOWNLOAD_BUTTON && organismesEffectifs?.length ? ( - { - exportDataAsXlsx( - `tdb-effectifs-${filtreAnneeScolaire}.xlsx`, - organismesEffectifs?.map((effectif) => { - return { - organisme_uai: props.organisme.uai, - organisme_siret: props.organisme.siret, - organisme_nom: props.organisme.raison_sociale, - organisme_nature: props.organisme.nature, - apprenant_nom: effectif.nom, - apprenant_prenom: effectif.prenom, - apprenant_date_de_naissance: effectif.date_de_naissance, - apprenant_statut: getStatutApprenantNameFromCode( - effectif.historique_statut?.sort((a, b) => { - return new Date(a.date_statut).getTime() - new Date(b.date_statut).getTime(); - })[0]?.valeur_statut - ), - formation_annee: effectif.formation?.annee, - formation_cfd: effectif.formation?.cfd, - formation_date_debut_formation: effectif.formation?.periode?.[0], - formation_date_fin_formation: effectif.formation?.periode?.[1], - formation_libelle_long: effectif.formation?.libelle_long, - formation_niveau: effectif.formation?.niveau, - formation_rncp: effectif.formation?.rncp, - }; - }) || [], - effectifsExportColumns - ); - }} - > - Télécharger la liste - - ) : null} - {organismesEffectifs && organismesEffectifs.length === 0 && ( + {data?.organismesEffectifs?.length === 0 && Object.keys(filters).length === 0 && ( )} @@ -139,105 +229,23 @@ function EffectifsPage(props: EffectifsPageProps) { )} - {isFetching && ( -
- -
- )} - - {organismesEffectifs && organismesEffectifs.length > 0 && ( - <> - - { - setSearchValue(value.trim()); - }, - }} - w="35%" - /> - - - - - Filtrer : - - - { - setShowOnlyErrors(e.target.checked); - }} - /> - Afficher uniquement les données en erreur - - - - Par année scolaire - setFiltreAnneeScolaire("all")} active={filtreAnneeScolaire === "all"}> - Toutes - - {Object.keys(effectifsByAnneeScolaire).map((anneeScolaire) => { - return ( - setFiltreAnneeScolaire(anneeScolaire)} - key={anneeScolaire} - active={anneeScolaire === filtreAnneeScolaire} - > - {anneeScolaire} - - ); - })} - - - - )} - - {Object.entries(effectifsByAnneeScolaire).map(([anneeScolaire, effectifs]) => { - if (filtreAnneeScolaire !== "all" && anneeScolaire !== filtreAnneeScolaire) { - return null; - } - const orgaEffectifs = showOnlyErrors - ? effectifs.filter((effectif) => effectif.validation_errors.length) - : effectifs; - const effectifsByCfd: { [cfd: string]: any[] } = groupBy(orgaEffectifs, "formation.cfd"); - return ( - - - {anneeScolaire} {!searchValue ? `- ${orgaEffectifs.length} apprenant(es) total` : ""} - - - {Object.entries(effectifsByCfd).map(([cfd, effectifs]) => { - const { formation } = effectifs[0]; - return ( - - ); - })} - - - ); - })} +
@@ -245,16 +253,3 @@ function EffectifsPage(props: EffectifsPageProps) { } export default EffectifsPage; - -const BadgeButton = ({ onClick, active = false, children, ...props }) => { - return ( - - ); -}; diff --git a/ui/modules/indicateurs/IndicateursForm.tsx b/ui/modules/indicateurs/IndicateursForm.tsx index c21f40be6..20f86db06 100644 --- a/ui/modules/indicateurs/IndicateursForm.tsx +++ b/ui/modules/indicateurs/IndicateursForm.tsx @@ -327,7 +327,7 @@ function IndicateursForm(props: IndicateursFormProps) { ({ ...effectif, id: effectif.organisme_id }))} loading={indicateursEffectifsLoading} noDataMessage="Aucun organisme ne semble correspondre aux filtres que vous avez sélectionnés" // paginationState={pagination} diff --git a/ui/modules/indicateurs/NewTable.tsx b/ui/modules/indicateurs/NewTable.tsx index ab732efcd..71edf5601 100644 --- a/ui/modules/indicateurs/NewTable.tsx +++ b/ui/modules/indicateurs/NewTable.tsx @@ -14,6 +14,7 @@ import { import { Fragment, useState } from "react"; import RowsSkeleton from "@/components/skeletons/RowsSkeleton"; +import { AddFill, SubtractLine } from "@/theme/components/icons"; import { ChevronLeftIcon, ChevronRightIcon, FirstPageIcon, LastPageIcon } from "../dashboard/icons"; @@ -22,18 +23,21 @@ interface NewTableProps extends SystemProps { data: T[]; noDataMessage?: string; loading?: boolean; - isRowExpanded?: boolean; + expandAllRows?: boolean; + enableRowExpansion?: boolean; sortingState?: SortingState; paginationState?: PaginationState; variant?: string; showPagination?: boolean; + isLoading?: boolean; onSortingChange?: (state: SortingState) => any; onPaginationChange?: (state: PaginationState) => any; renderSubComponent?: (row: Row) => React.ReactElement; renderDivider?: () => React.ReactElement; } -function NewTable(props: NewTableProps) { +function NewTable(props: NewTableProps) { + const isLoading = props.isLoading ?? false; const [pagination, setPagination] = useState( props.paginationState ?? { pageIndex: 0, @@ -41,6 +45,22 @@ function NewTable(props: NewTableProps) { } ); + const [expandedRows, setExpandedRows] = useState>( + props.expandAllRows ? new Set(props.data.map((row) => row.id)) : new Set() + ); + + const toggleRowExpansion = (rowId: string) => { + setExpandedRows((prev) => { + const newSet = new Set(prev); + if (newSet.has(rowId)) { + newSet.delete(rowId); + } else { + newSet.add(rowId); + } + return newSet; + }); + }; + const table = useReactTable({ data: props.data, columns: props.columns, @@ -67,89 +87,117 @@ function NewTable(props: NewTableProps) { {table.getHeaderGroups().map((headerGroup, index) => ( - {headerGroup.headers.map((header, headerIndex) => { - return ( - div > .sort-icon": { - display: "inline-block", - color: "black", - }, - } - : undefined - } - paddingBottom={3} - bg="white" - > - {header.isPlaceholder ? null : ( - - {flexRender(header.column.columnDef.header, header.getContext())} - - {header.column.getIsSorted() === "desc" && } - {header.column.getIsSorted() === "asc" && } - {header.column.getIsSorted() === false && ( - - ▼ - - )} - - - )} - - ); - })} + {headerGroup.headers.map((header, headerIndex) => ( + div > .sort-icon": { + display: "inline-block", + color: "black", + }, + } + : undefined + } + paddingBottom={3} + bg="white" + > + {header.isPlaceholder ? null : ( + + {flexRender(header.column.columnDef.header, header.getContext())} + + {header.column.getIsSorted() === "desc" && } + {header.column.getIsSorted() === "asc" && } + {header.column.getIsSorted() === false && ( + + ▼ + + )} + + + )} + + ))} + {props.enableRowExpansion && } ))} - {props.loading ? ( - - ) : table.getRowModel().rows.length === 0 ? ( + {isLoading ? ( + + ) : !props.data || props.data.length === 0 ? ( {props.noDataMessage ?? "Aucun résultat"} ) : ( - table.getRowModel().rows.map((row, index) => { + table.getRowModel().rows.map((row) => { + const isExpanded = expandedRows.has(row.original.id); + return ( - + props.enableRowExpansion && toggleRowExpansion(row.original.id)} + cursor={props.enableRowExpansion ? "pointer" : "default"} + _hover={props.enableRowExpansion ? { backgroundColor: "gray.100" } : undefined} > - {row.getVisibleCells().map((cell, index) => { - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ); - })} + {row.getVisibleCells().map((cell, cellIndex) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + {props.enableRowExpansion && ( + + + + + + )} - {props.isRowExpanded && ( - - {props?.renderSubComponent?.(row)} + {isExpanded && props.renderSubComponent && ( + + + {props.renderSubComponent(row)} + )} {props.renderDivider && ( - - {props?.renderDivider?.()} + + + {props.renderDivider()} + )} @@ -159,7 +207,7 @@ function NewTable(props: NewTableProps) { - {(props.showPagination ?? true) && ( + {(props.showPagination ?? true) && !isLoading && ( + + + ); +}; + +export default SIFAEffectifsFilterPanel; diff --git a/ui/modules/mon-espace/SIFA/SIFATable/SIFAEffectifsTable.tsx b/ui/modules/mon-espace/SIFA/SIFATable/SIFAEffectifsTable.tsx new file mode 100644 index 000000000..234b28eda --- /dev/null +++ b/ui/modules/mon-espace/SIFA/SIFATable/SIFAEffectifsTable.tsx @@ -0,0 +1,183 @@ +import { SearchIcon } from "@chakra-ui/icons"; +import { Box, Button, Divider, HStack, Input, InputGroup, InputRightElement, Switch, Text } from "@chakra-ui/react"; +import { UseQueryResult } from "@tanstack/react-query"; +import { ColumnDef, Row, SortingState } from "@tanstack/react-table"; +import { useRouter } from "next/router"; +import { useState, useEffect } from "react"; +import { getSIFADate } from "shared"; + +import { Organisme } from "@/common/internal/Organisme"; +import TableWithApi from "@/components/Table/TableWithApi"; + +import EffectifTableDetails from "../../effectifs/engine/EffectifsTableDetails"; + +import SIFAeffectifsTableColumnsDefs from "./SIFAEffectifsColumns"; +import SIFAEffectifsFilterPanel from "./SIFAEffectifsFilterPanel"; + +interface EffectifsTableProps { + organisme: Organisme; + organismesEffectifs: any[]; + filters: Record; + pagination: any; + search: string; + sort: SortingState; + onPaginationChange: (pagination: any) => void; + onSearchChange: (search: string) => void; + onFilterChange: (filters: Record) => void; + onSortChange: (sort: SortingState) => void; + total: number; + availableFilters: Record; + resetFilters: () => void; + isFetching: boolean; + canEdit?: boolean; + modeSifa?: boolean; + refetch: (options: { throwOnError: boolean; cancelRefetch: boolean }) => Promise; +} + +const EffectifsTable = ({ + organismesEffectifs, + filters, + pagination, + search, + sort: initialSort, + onPaginationChange, + onSearchChange, + onFilterChange, + onSortChange, + total, + availableFilters, + resetFilters, + isFetching, + canEdit, + modeSifa, + refetch, +}: EffectifsTableProps) => { + const router = useRouter(); + + const [sort, setSort] = useState(initialSort); + const [localSearch, setLocalSearch] = useState(search || ""); + const [showOnlyErrors, setShowOnlyErrors] = useState(false); + + useEffect(() => { + setSort(initialSort); + setLocalSearch(search || ""); + }, [initialSort, search]); + + const handleSearchInputChange = (event: React.ChangeEvent) => { + setLocalSearch(event.target.value); + }; + + const onSearchButtonClick = () => { + executeSearch(); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + executeSearch(); + } + }; + + const executeSearch = () => { + onSearchChange(localSearch); + + router.push( + { + pathname: router.pathname, + query: { ...router.query, search: localSearch }, + }, + undefined, + { shallow: true } + ); + }; + + const handlePaginationChange = (newPagination) => { + onPaginationChange(newPagination); + + router.push( + { + pathname: router.pathname, + query: { + ...router.query, + pageIndex: newPagination.pageIndex, + pageSize: newPagination.pageSize, + }, + }, + undefined, + { shallow: true } + ); + }; + + const handleSortChange = (newSort: SortingState) => { + onSortChange(newSort); + }; + + const filteredEffectifs = showOnlyErrors + ? organismesEffectifs.filter((effectif) => effectif.validation_errors?.length > 0) + : organismesEffectifs; + + return ( + + + + + + + + + + + + + + + { + setShowOnlyErrors(e.target.checked); + }} + /> + Afficher uniquement les données manquantes pour SIFA + + + + + + Vous avez {total} effectifs au total, en contrat au 31 décembre {getSIFADate(new Date()).getUTCFullYear()} + + + []} + enableRowExpansion={true} + sortingState={sort} + onSortingChange={handleSortChange} + onPaginationChange={handlePaginationChange} + isLoading={isFetching} + renderSubComponent={(row: Row) => ( + + )} + /> + + ); +}; + +export default EffectifsTable; diff --git a/ui/modules/mon-espace/effectifs/engine/Effectif.tsx b/ui/modules/mon-espace/effectifs/engine/Effectif.tsx index 30ba67f56..5fd8e4029 100644 --- a/ui/modules/mon-espace/effectifs/engine/Effectif.tsx +++ b/ui/modules/mon-espace/effectifs/engine/Effectif.tsx @@ -1,4 +1,4 @@ -import { Box, Skeleton } from "@chakra-ui/react"; +import { Skeleton } from "@chakra-ui/react"; import React from "react"; import { Statut } from "shared"; @@ -38,9 +38,7 @@ const Effectif = React.memo(function EffectifMemo({ <> {/* @ts-expect-error */} - - - + ); diff --git a/ui/modules/mon-espace/effectifs/engine/EffectifsTableDetails.tsx b/ui/modules/mon-espace/effectifs/engine/EffectifsTableDetails.tsx index 4eba1619b..20cd42696 100644 --- a/ui/modules/mon-espace/effectifs/engine/EffectifsTableDetails.tsx +++ b/ui/modules/mon-espace/effectifs/engine/EffectifsTableDetails.tsx @@ -1,4 +1,3 @@ -import { Box } from "@chakra-ui/react"; import { useQueryClient } from "@tanstack/react-query"; import React, { useEffect, useRef } from "react"; import { useSetRecoilState } from "recoil"; @@ -23,14 +22,12 @@ const EffectifTableDetails = ({ row, modeSifa = false, canEdit = false, refetch } return ( - - - + ); }; diff --git a/ui/modules/mon-espace/effectifs/engine/effectifForm/EffectifForm.tsx b/ui/modules/mon-espace/effectifs/engine/effectifForm/EffectifForm.tsx index aed25f3f7..306e396d6 100644 --- a/ui/modules/mon-espace/effectifs/engine/effectifForm/EffectifForm.tsx +++ b/ui/modules/mon-espace/effectifs/engine/effectifForm/EffectifForm.tsx @@ -218,7 +218,7 @@ export const EffectifForm = memo( const historyStatus = sortedParcours.slice(1); return ( - + {!fromDECA && ( )} - setAccordionIndex(expandedIndex)} - reduceMotion - > - - {({ isExpanded }) => ( - - - {parcours.length > 0 ? ( - <> + + setAccordionIndex(expandedIndex)} + reduceMotion + > + + {({ isExpanded }) => ( + + + {parcours.length > 0 ? ( + <> + + Statut actuel + + {getStatut(currentStatus.valeur)} + + + + Date de déclaration du statut + + {new Date(currentStatus.date).toLocaleDateString()} + + + + + + Anciens statuts + + {historyStatus.map((status, idx) => ( + + + {getStatut(status.valeur)} déclaré le {new Date(status.date).toLocaleDateString()} + + + ))} + + + ) : ( Statut actuel - {getStatut(currentStatus.valeur)} + Aucun statut - - Date de déclaration du statut - - {new Date(currentStatus.date).toLocaleDateString()} - - - - - - Anciens statuts - - {historyStatus.map((status, idx) => ( - - - {getStatut(status.valeur)} déclaré le {new Date(status.date).toLocaleDateString()} - - - ))} - - - ) : ( - - Statut actuel - - Aucun statut - - - )} - - - )} - - - {({ isExpanded }) => ( - - - - )} - - - {({ isExpanded }) => ( - - - - )} - - - {({ isExpanded }) => ( - - - - )} - - + )} + + + )} + + + {({ isExpanded }) => ( + + + + )} + + + {({ isExpanded }) => ( + + + + )} + + + {({ isExpanded }) => ( + + + + )} + + + ); } diff --git a/ui/modules/mon-espace/effectifs/engine/effectifForm/blocks/apprenant/EffectifApprenant.tsx b/ui/modules/mon-espace/effectifs/engine/effectifForm/blocks/apprenant/EffectifApprenant.tsx index 6e9e8d0de..cff139112 100644 --- a/ui/modules/mon-espace/effectifs/engine/effectifForm/blocks/apprenant/EffectifApprenant.tsx +++ b/ui/modules/mon-espace/effectifs/engine/effectifForm/blocks/apprenant/EffectifApprenant.tsx @@ -18,7 +18,7 @@ export const EffectifApprenant = memo(({ apprenant, modeSifa }: { apprenant: any if (!organisme) return null; return ( - + diff --git a/ui/modules/mon-espace/effectifs/engine/effectifForm/blocks/contrats/EffectifContrats.tsx b/ui/modules/mon-espace/effectifs/engine/effectifForm/blocks/contrats/EffectifContrats.tsx index e45eaf667..85dc63f80 100644 --- a/ui/modules/mon-espace/effectifs/engine/effectifForm/blocks/contrats/EffectifContrats.tsx +++ b/ui/modules/mon-espace/effectifs/engine/effectifForm/blocks/contrats/EffectifContrats.tsx @@ -9,7 +9,7 @@ export const ApprenantContrats = memo(({ contrats }: { contrats: any[] }) => { <> {contrats?.map((contrat, i) => { return ( - + diff --git a/ui/modules/mon-espace/effectifs/engine/effectifForm/blocks/formation/EffectifFormation.tsx b/ui/modules/mon-espace/effectifs/engine/effectifForm/blocks/formation/EffectifFormation.tsx index 7e338cb6f..94a9ed395 100644 --- a/ui/modules/mon-espace/effectifs/engine/effectifForm/blocks/formation/EffectifFormation.tsx +++ b/ui/modules/mon-espace/effectifs/engine/effectifForm/blocks/formation/EffectifFormation.tsx @@ -6,7 +6,7 @@ import { InputController } from "@/modules/mon-espace/effectifs/engine/formEngin // eslint-disable-next-line react/display-name, no-unused-vars export const EffectifFormation = memo(() => { return ( - + diff --git a/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsColumns.tsx b/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsColumns.tsx index 549329d46..b098b81a2 100644 --- a/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsColumns.tsx +++ b/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsColumns.tsx @@ -17,7 +17,7 @@ const effectifsTableColumnsDefs = [ ), cell: ({ row, getValue }) => , - size: 100, + size: 120, }, { accessorKey: "nom", @@ -30,7 +30,7 @@ const effectifsTableColumnsDefs = [ ), cell: ({ row, getValue }) => , - size: 150, + size: 160, }, { accessorKey: "prenom", @@ -45,21 +45,22 @@ const effectifsTableColumnsDefs = [ cell: ({ row, getValue }) => ( ), - size: 150, + size: 160, }, { accessorKey: "formation", header: () => "Formation", cell: ({ row }) => { return ( - - {row.original?.formation?.libelle_long || "Libellé manquant"} - + + {row.original?.formation?.libelle_long || "Libellé manquant"} + CFD : {row.original?.formation?.cfd} - RNCP : {row.original?.formation?.cfd} ); }, + size: 350, }, { accessorKey: "source", @@ -90,7 +91,7 @@ const effectifsTableColumnsDefs = [ value={getValue() === "FICHIER" ? capitalizeWords(getValue()) : getValue()} /> ), - size: 100, + size: 150, }, { accessorKey: "statut_courant", @@ -134,13 +135,13 @@ const effectifsTableColumnsDefs = [ return ( {getStatut(current.valeur)} - + depuis le {DateTime.fromISO(current.date).setLocale("fr-FR").toFormat("dd/MM/yyyy")} ); }, - size: 150, + size: 170, }, ]; diff --git a/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsFilterComponent.tsx b/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsFilterComponent.tsx index bda8b4d94..582e05082 100644 --- a/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsFilterComponent.tsx +++ b/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsFilterComponent.tsx @@ -12,6 +12,7 @@ interface EffectifsFilterComponentProps { onChange: (selectedValues: string[]) => void; isOpen: boolean; setIsOpen: (isOpen: boolean) => void; + sortOrder?: "asc" | "desc"; } const EffectifsFilterComponent: React.FC = ({ @@ -22,7 +23,15 @@ const EffectifsFilterComponent: React.FC = ({ onChange, isOpen, setIsOpen, + sortOrder = "asc", }) => { + const sortedOptions = [...options].sort((a, b) => { + if (sortOrder === "asc") { + return a.localeCompare(b); + } + return b.localeCompare(a); + }); + return (
= ({ setIsOpen(false)} width="auto" p="3w"> - {options.map((option, index) => ( + {sortedOptions.map((option, index) => ( {capitalizeWords(option)} diff --git a/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsFilterPanel.tsx b/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsFilterPanel.tsx index 162a91612..5b5312c13 100644 --- a/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsFilterPanel.tsx +++ b/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsFilterPanel.tsx @@ -39,6 +39,7 @@ const EffectifsFilterPanel: React.FC = ({ onChange={(values) => handleCheckboxChange("annee_scolaire", values)} isOpen={openFilter === "annee_scolaire"} setIsOpen={(isOpen) => setOpenFilter(isOpen ? "annee_scolaire" : null)} + sortOrder="desc" /> )} @@ -81,7 +82,6 @@ const EffectifsFilterPanel: React.FC = ({ /> )} - {/* Reset Button */} diff --git a/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsTable.tsx b/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsTable.tsx index 125bfaae4..a81c8da09 100644 --- a/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsTable.tsx +++ b/ui/modules/mon-espace/effectifs/engine/effectifsTable/EffectifsTable.tsx @@ -1,5 +1,6 @@ import { SearchIcon } from "@chakra-ui/icons"; import { Box, Button, Divider, HStack, Input, InputGroup, InputRightElement, Switch, Text } from "@chakra-ui/react"; +import { UseQueryResult } from "@tanstack/react-query"; import { Row, SortingState } from "@tanstack/react-table"; import { useRouter } from "next/router"; import { useState, useEffect } from "react"; @@ -10,9 +11,12 @@ import { exportDataAsXlsx } from "@/common/utils/exportUtils"; import DownloadButton from "@/components/buttons/DownloadButton"; import TableWithApi from "@/components/Table/TableWithApi"; +import EffectifTableDetails from "../EffectifsTableDetails"; + import effectifsTableColumnsDefs from "./EffectifsColumns"; import EffectifsFilterPanel from "./EffectifsFilterPanel"; +const DISPLAY_DOWNLOAD_BUTTON = false; interface EffectifsTableProps { organisme: Organisme; organismesEffectifs: any[]; @@ -28,6 +32,9 @@ interface EffectifsTableProps { availableFilters: Record; resetFilters: () => void; isFetching: boolean; + canEdit?: boolean; + modeSifa?: boolean; + refetch: (options: { throwOnError: boolean; cancelRefetch: boolean }) => Promise; } const EffectifsTable = ({ @@ -45,6 +52,9 @@ const EffectifsTable = ({ availableFilters, resetFilters, isFetching, + canEdit, + modeSifa, + refetch, }: EffectifsTableProps) => { const router = useRouter(); @@ -119,8 +129,8 @@ const EffectifsTable = ({ name="search_organisme" placeholder="Rechercher un apprenant" value={localSearch} - onChange={handleSearchInputChange} - onKeyDown={handleKeyDown} // Listen for "Enter" key + onChange={handleSearchInputChange} // This should work correctly now + onKeyDown={handleKeyDown} flex="1" mr="2" /> @@ -130,36 +140,38 @@ const EffectifsTable = ({ - { - exportDataAsXlsx( - `tdb-effectifs.xlsx`, - organismesEffectifs?.map((effectif) => ({ - organisme_uai: organisme.uai, - organisme_siret: organisme.siret, - organisme_nom: organisme.raison_sociale, - organisme_nature: organisme.nature, - apprenant_nom: effectif.nom, - apprenant_prenom: effectif.prenom, - apprenant_date_de_naissance: effectif.date_de_naissance, - apprenant_statut: effectif.statut_courant, - formation_annee: effectif.formation?.annee, - formation_cfd: effectif.formation?.cfd, - formation_libelle_long: effectif.formation?.libelle_long, - formation_niveau: effectif.formation?.niveau, - formation_rncp: effectif.formation?.rncp, - formation_date_debut_formation: effectif.formation?.date_debut_formation, - formation_date_fin_formation: effectif.formation?.date_fin_formation, - })) || [], - effectifsExportColumns - ); - }} - > - Télécharger la liste - + {DISPLAY_DOWNLOAD_BUTTON && ( + { + exportDataAsXlsx( + `tdb-effectifs.xlsx`, + organismesEffectifs?.map((effectif) => ({ + organisme_uai: organisme.uai, + organisme_siret: organisme.siret, + organisme_nom: organisme.raison_sociale, + organisme_nature: organisme.nature, + apprenant_nom: effectif.nom, + apprenant_prenom: effectif.prenom, + apprenant_date_de_naissance: effectif.date_de_naissance, + apprenant_statut: effectif.statut_courant, + formation_annee: effectif.formation?.annee, + formation_cfd: effectif.formation?.cfd, + formation_libelle_long: effectif.formation?.libelle_long, + formation_niveau: effectif.formation?.niveau, + formation_rncp: effectif.formation?.rncp, + formation_date_debut_formation: effectif.formation?.date_debut_formation, + formation_date_fin_formation: effectif.formation?.date_fin_formation, + })) || [], + effectifsExportColumns + ); + }} + > + Télécharger la liste + + )} @@ -197,11 +209,7 @@ const EffectifsTable = ({ onPaginationChange={handlePaginationChange} isLoading={isFetching} renderSubComponent={(row: Row) => ( - - - Détails pour {row.original.nom} {row.original.prenom} - - + )} /> diff --git a/ui/theme/components/table.js b/ui/theme/components/table.js index 2cc6c3dc0..52a2ab773 100644 --- a/ui/theme/components/table.js +++ b/ui/theme/components/table.js @@ -26,12 +26,17 @@ export const Table = { backgroundColor: "grey.100", }, _hover: { - backgroundColor: "grey.200", + backgroundColor: "#E3E3FD", + }, + "&.table-row-expanded": { + backgroundColor: "#E3E3FD", + _hover: { + backgroundColor: "#E3E3FD", + }, + }, + "&.expanded-row": { + backgroundColor: "white", }, - }, - td: { - paddingY: "2", - paddingX: "3", }, }, }, From 7d3b8f4eee00cdcece585c3ff9fe8e699058d8a6 Mon Sep 17 00:00:00 2001 From: Nicolas KREMER Date: Fri, 20 Dec 2024 12:44:53 +0100 Subject: [PATCH 3/7] fix: mise a jour row skeleton --- ui/components/skeletons/RowsSkeleton.tsx | 55 ++++++++++++------------ 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/ui/components/skeletons/RowsSkeleton.tsx b/ui/components/skeletons/RowsSkeleton.tsx index 86fcaa1b3..aaf07941d 100644 --- a/ui/components/skeletons/RowsSkeleton.tsx +++ b/ui/components/skeletons/RowsSkeleton.tsx @@ -1,34 +1,35 @@ import { Skeleton, Td, Tr } from "@chakra-ui/react"; -import { memo } from "react"; +import React from "react"; -interface Props { +const RowsSkeleton = ({ + nbRows = 5, + nbColumns = 5, + height = "1rem", +}: { nbRows?: number; nbColumns?: number; height?: string; -} +}) => { + const rows = Array.from({ length: nbRows }, (_, i) => i); + const columns = Array.from({ length: nbColumns }, (_, j) => j); -const Cell = memo(({ height }: { height: string }) => ( - - - -)); -Cell.displayName = "Cell"; + return ( + <> + {rows.map((i) => { + return ( + + {columns.map((j) => { + return ( + + + + ); + })} + + ); + })} + + ); +}; -const Row = memo(({ cols, height }: { cols: number; height: string }) => ( - - {Array.from({ length: cols }, (_, i) => ( - - ))} - -)); -Row.displayName = "Row"; - -const RowsSkeleton = ({ nbRows = 5, nbColumns = 5, height = "1rem" }: Props) => ( - <> - {Array.from({ length: nbRows }, (_, i) => ( - - ))} - -); - -export default memo(RowsSkeleton); +export default RowsSkeleton; From d937623e348fbd31948e5e85671e66f2a74bd915 Mon Sep 17 00:00:00 2001 From: Nicolas KREMER Date: Fri, 20 Dec 2024 12:46:39 +0100 Subject: [PATCH 4/7] fix: mise a jour new table --- ui/modules/indicateurs/IndicateursForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/modules/indicateurs/IndicateursForm.tsx b/ui/modules/indicateurs/IndicateursForm.tsx index 20f86db06..c21f40be6 100644 --- a/ui/modules/indicateurs/IndicateursForm.tsx +++ b/ui/modules/indicateurs/IndicateursForm.tsx @@ -327,7 +327,7 @@ function IndicateursForm(props: IndicateursFormProps) { ({ ...effectif, id: effectif.organisme_id }))} + data={indicateursEffectifs || []} loading={indicateursEffectifsLoading} noDataMessage="Aucun organisme ne semble correspondre aux filtres que vous avez sélectionnés" // paginationState={pagination} From 3fa467947910522c34af316512ae9384ce262f52 Mon Sep 17 00:00:00 2001 From: Nicolas KREMER Date: Fri, 20 Dec 2024 12:51:17 +0100 Subject: [PATCH 5/7] fix: suppression ancien tableau effectif --- .../engine/EffectifTableContainer.tsx | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 ui/modules/mon-espace/effectifs/engine/EffectifTableContainer.tsx diff --git a/ui/modules/mon-espace/effectifs/engine/EffectifTableContainer.tsx b/ui/modules/mon-espace/effectifs/engine/EffectifTableContainer.tsx deleted file mode 100644 index cfd137518..000000000 --- a/ui/modules/mon-espace/effectifs/engine/EffectifTableContainer.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Box, HStack, Text } from "@chakra-ui/react"; -import { UseQueryResult } from "@tanstack/react-query"; -import { useState } from "react"; - -import { DoubleChevrons } from "@/theme/components/icons/DoubleChevrons"; - -import EffectifsTable from "./EffectifsTable"; - -interface EffectifsTableContainerProps { - effectifs: any[]; - modeSifa?: boolean; - canEdit?: boolean; - searchValue?: string; - tableId: string; - formation: any; - refetch: (options: { throwOnError: boolean; cancelRefetch: boolean }) => Promise; -} -const EffectifsTableContainer = ({ - effectifs, - formation, - canEdit, - searchValue, - tableId, - modeSifa, - refetch, - ...props -}: EffectifsTableContainerProps) => { - const [count, setCount] = useState(effectifs.length); - return ( - - {count !== 0 && ( - - - - {formation.libelle_long} - - - [Code diplôme {formation.cfd}] - [Code RNCP {formation.rncp}] - - - )} - setCount(count)} - modeSifa={modeSifa} - refetch={refetch} - /> - - ); -}; - -export default EffectifsTableContainer; From 9afd2cb498f25a05d392bc17e261c18e8cd39914 Mon Sep 17 00:00:00 2001 From: Nicolas KREMER Date: Fri, 20 Dec 2024 12:58:52 +0100 Subject: [PATCH 6/7] fix: mise a jour NewTable type --- ui/modules/organismes/OrganismesTable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/modules/organismes/OrganismesTable.tsx b/ui/modules/organismes/OrganismesTable.tsx index 0cb104aa2..5b967994b 100644 --- a/ui/modules/organismes/OrganismesTable.tsx +++ b/ui/modules/organismes/OrganismesTable.tsx @@ -35,7 +35,6 @@ import InfoTransmissionDonnees from "./InfoTransmissionDonnees"; import OrganismesFilterPanel, { OrganismeFiltersListVisibilityProps } from "./OrganismesFilterPanel"; type OrganismeNormalized = Organisme & { - id: string; normalizedName: string; normalizedUai: string; normalizedCommune: string; From 0b34728bd9b76be2fc60e96c3e17a49d14047cf5 Mon Sep 17 00:00:00 2001 From: Nicolas KREMER Date: Mon, 23 Dec 2024 12:28:35 +0100 Subject: [PATCH 7/7] fix: mise a jour requete organisme --- .../specific.routes/organisme.routes.ts | 84 +++++++++++++++++-- ui/components/Table/TableWithApi.tsx | 6 +- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/server/src/http/routes/specific.routes/organisme.routes.ts b/server/src/http/routes/specific.routes/organisme.routes.ts index a7442d888..0655298fc 100644 --- a/server/src/http/routes/specific.routes/organisme.routes.ts +++ b/server/src/http/routes/specific.routes/organisme.routes.ts @@ -1,17 +1,17 @@ import { ObjectId } from "bson"; import { + STATUT_APPRENANT, getAnneesScolaireListFromDate, getSIFADate, requiredApprenantAdresseFieldsSifa, requiredFieldsSifa, } from "shared"; -// import { isEligibleSIFA } from "@/common/actions/sifa.actions/sifa.actions"; -import { effectifsDb, organismesDb } from "@/common/model/collections"; +import { effectifsDECADb, effectifsDb, organismesDb } from "@/common/model/collections"; export async function getOrganismeEffectifs( - organismeId, - sifa = false, + organismeId: ObjectId, + sifa: boolean = false, options: { pageIndex: number; pageSize: number; @@ -23,7 +23,8 @@ export async function getOrganismeEffectifs( ) { const organisme = await organismesDb().findOne({ _id: organismeId }); const { pageIndex, pageSize, search, filters, sortField, sortOrder } = options; - const db = await effectifsDb(); + const isDeca = !organisme?.is_transmission_target; + const db = isDeca ? effectifsDECADb() : effectifsDb(); const parsedFilters = Object.entries(filters).reduce( (acc, [key, value]) => { @@ -94,6 +95,25 @@ export async function getOrganismeEffectifs( annee_scolaire: { $in: getAnneesScolaireListFromDate(sifa ? getSIFADate(new Date()) : new Date()), }, + "_computed.statut.parcours": { + $elemMatch: { + valeur: STATUT_APPRENANT.APPRENTI, + date: { + $eq: { + $arrayElemAt: [ + { + $filter: { + input: "$_computed.statut.parcours", + as: "parcours", + cond: { $eq: ["$$parcours.valeur", STATUT_APPRENANT.APPRENTI] }, + }, + }, + -1, + ], + }, + }, + }, + }, }), }, }, @@ -181,13 +201,63 @@ export async function getOrganismeEffectifs( const [data] = await db.aggregate(pipeline).toArray(); return { - fromDECA: !organisme?.is_transmission_target, + fromDECA: isDeca, total: data?.total || 0, filters: data?.filters || {}, organismesEffectifs: data?.results || [], }; } +export async function getAllOrganismeEffectifsIds(organismeId: ObjectId, sifa = false) { + const organisme = await organismesDb().findOne({ _id: organismeId }); + const isDeca = !organisme?.is_transmission_target; + const db = isDeca ? effectifsDECADb() : effectifsDb(); + + const pipeline = [ + { + $match: { + organisme_id: organismeId, + ...(sifa && { + annee_scolaire: { + $in: getAnneesScolaireListFromDate(sifa ? getSIFADate(new Date()) : new Date()), + }, + "_computed.statut.parcours": { + $elemMatch: { + valeur: STATUT_APPRENANT.APPRENTI, + date: { + $eq: { + $arrayElemAt: [ + { + $filter: { + input: "$_computed.statut.parcours", + as: "parcours", + cond: { $eq: ["$$parcours.valeur", STATUT_APPRENANT.APPRENTI] }, + }, + }, + -1, + ], + }, + }, + }, + }, + }), + }, + }, + { + $project: { + id: { $toString: "$_id" }, + }, + }, + ]; + + const effectifs = await db.aggregate(pipeline).toArray(); + + return { + fromDECA: isDeca, + organismesEffectifs: effectifs, + }; +} + export async function updateOrganismeEffectifs( organismeId: ObjectId, sifa = false, @@ -196,7 +266,7 @@ export async function updateOrganismeEffectifs( } ) { if (!update["apprenant.type_cfa"]) return; - const { fromDECA, organismesEffectifs } = await getOrganismeEffectifs(organismeId, sifa); + const { fromDECA, organismesEffectifs } = await getAllOrganismeEffectifsIds(organismeId, sifa); !fromDECA && (await effectifsDb().updateMany( diff --git a/ui/components/Table/TableWithApi.tsx b/ui/components/Table/TableWithApi.tsx index 9c876d4c1..96afe51bd 100644 --- a/ui/components/Table/TableWithApi.tsx +++ b/ui/components/Table/TableWithApi.tsx @@ -49,10 +49,8 @@ function TableWithApi(props: TableWithApiProps { setExpandedRows((prev) => { - const newSet = new Set(prev); - if (newSet.has(rowId)) { - newSet.delete(rowId); - } else { + const newSet = new Set(); + if (!prev.has(rowId)) { newSet.add(rowId); } return newSet;