diff --git a/.env b/.env index 23f8d03..0d26d85 100644 --- a/.env +++ b/.env @@ -3,3 +3,5 @@ VITE_AUTH_TYPE=anonymous VITE_OIDC_CLIENT_ID= VITE_OIDC_ISSUER=https://localhost:8000 VITE_IDENTITY_PROVIDER= +VITE_ADMIN_LDAP_ROLE= +VITE_USER_LDAP_ROLE= diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index bb1c19c..2eb1a3f 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -13,7 +13,11 @@ export const useAccessToken = (): string => { }; export const useUser = () => { - return useOidc({ assertUserLoggedIn: true }).oidcTokens.decodedIdToken; + return { + decodedToken: useOidc({ assertUserLoggedIn: true }).oidcTokens.decodedIdToken, + isAdminLdap: useHasRole(import.meta.env.VITE_ADMIN_LDAP_ROLE), + isUserLdap: useHasRole(import.meta.env.VITE_USER_LDAP_ROLE), + }; }; export const useLogout = () => { diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx deleted file mode 100644 index 904b1b1..0000000 --- a/src/pages/Settings.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function Settings() { - return
Settings coming
; -} diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx new file mode 100644 index 0000000..9efa7dd --- /dev/null +++ b/src/pages/SettingsPage.tsx @@ -0,0 +1,72 @@ +import { Box, Divider, Tabs } from "@mui/material"; +import { PageTab } from "../ui/PageTab"; +import { useState, SyntheticEvent } from "react"; +import { SettingsHeader } from "../ui/Settings/SettingsHeader"; +import { SettingsHabilitationsCard } from "../ui/Settings/SettingsHabilitationsCard"; +import { Breadcrumbs } from "../ui/Breadcrumbs.tsx"; + +enum Tab { + Habilitations = "Habilitations", + Communications = "Communications", + NewSource = "NewSource", +} + +const TabNames = { + [Tab.Habilitations]: "Gestion des habilitations", + [Tab.Communications]: "Communications", + [Tab.NewSource]: "Nouvelle Source", +}; + +export function SettingsPage() { + const [currentTab, setCurrentTab] = useState(Tab.Habilitations); + const handleChange = (_: SyntheticEvent, newValue: Tab) => { + setCurrentTab(newValue); + }; + const breadcrumbs = [ + { href: "/", title: "Accueil" }, + { href: "/settings", title: "Réglages" }, + TabNames[currentTab], + ]; + return ( + <> + + + + {Object.keys(Tab).map(k => ( + + ))} + + + + + + + + + ); +} + +function SettingsTab({ tab }: { tab: Tab }) { + if (tab === Tab.Habilitations) { + return ; + } + + return; +} diff --git a/src/pages/SurveyPage.tsx b/src/pages/SurveyPage.tsx index fad8ccf..d36b123 100644 --- a/src/pages/SurveyPage.tsx +++ b/src/pages/SurveyPage.tsx @@ -93,13 +93,13 @@ export function SurveyPage() { - + ); } -function SurveyUnitTab({ +function SurveyTab({ survey, onSave, tab, diff --git a/src/routes.tsx b/src/routes.tsx index ec24e7c..0efe0f8 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -5,7 +5,7 @@ import { Layout } from "./ui/Layout"; import { PageError } from "./ui/PageError"; import { SurveyPage } from "./pages/SurveyPage"; import { ContactPage } from "./pages/ContactPage"; -import { Settings } from "./pages/Settings"; +import { SettingsPage } from "./pages/SettingsPage.tsx"; import { SearchContacts } from "./pages/Search/SearchContacts.tsx"; import { SearchSurveys } from "./pages/Search/SearchSurveys.tsx"; import { SearchSurveyUnits } from "./pages/Search/SearchSurveyUnits.tsx"; @@ -33,8 +33,8 @@ export const routes: RouteObject[] = [ }, { path: "surveys/:id", element: }, { path: "contacts/:id", element: }, + { path: "settings", element: }, { path: "survey-units/:id", element: }, - { path: "reglages", element: }, { path: "", element: }, ], }, diff --git a/src/theme.tsx b/src/theme.tsx index d2b0493..363d106 100644 --- a/src/theme.tsx +++ b/src/theme.tsx @@ -49,6 +49,7 @@ declare module "@mui/material/styles" { bodyLarge: CSSProperties; bodyMedium: CSSProperties; bodySmall: CSSProperties; + robotoLarge: CSSProperties; } interface TypographyVariantsOptions { @@ -67,6 +68,7 @@ declare module "@mui/material/styles" { bodyLarge?: CSSProperties; bodyMedium?: CSSProperties; bodySmall?: CSSProperties; + robotoLarge?: CSSProperties; } } @@ -87,6 +89,7 @@ declare module "@mui/material/Typography" { bodyLarge: true; bodyMedium: true; bodySmall: true; + robotoLarge: true; } } @@ -115,6 +118,11 @@ declare module "@mui/material/Paper" { disabled: true; } } +declare module "@mui/material/Chip" { + interface ChipPropsVariantOverrides { + role: true; + } +} declare module "@mui/material/Tab" { interface TabPropsClassesOverrides { @@ -206,6 +214,13 @@ const typography = { lineHeight: "16px", letterSpacing: 0.4, }, + robotoLarge: { + lineHeight: "24px", + fontSize: "16px", + letterSpacing: "0.15px", + fontWeight: 400, + fontFamily: "Roboto", + }, }; const palette = { background: { @@ -389,11 +404,27 @@ export const theme = createTheme({ }, MuiInputLabel: { styleOverrides: { - sizeSmall: { - ...typography.bodyMedium, + root: { + ...typography.bodyLarge, }, }, }, + MuiChip: { + variants: [ + { + props: { variant: "role" }, + style: { + fontWeight: 600, + lineHeight: "18px", + letterSpacing: "0.1px", + fontSize: "14px", + textTransform: "capitalize", + backgroundColor: "#EBEBEB", + height: "34px", + }, + }, + ], + }, MuiInputBase: { styleOverrides: { root: { diff --git a/src/types/api.ts b/src/types/api.ts index 4312a17..3a7b275 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -7,7 +7,17 @@ export type APISchemas = { /* Format: date-time */ timestamp?: string } - UserDto: { identifier: string; role?: string } + UserDto: { + identifier: string + role?: string + name?: string + firstName?: string + organization?: string + accreditedSources?: Array + /* Format: date-time */ + creationDate?: string + creationAuthor?: string + } SurveyDto: { id: string sourceId: string @@ -340,17 +350,17 @@ export type APISchemas = { } OnGoingDto: { ongoing?: boolean } PageableObject: { - /* Format: int64 */ - offset?: number - sort?: APISchemas["SortObject"] /* Format: int32 */ pageNumber?: number /* Format: int32 */ pageSize?: number - unpaged?: boolean + /* Format: int64 */ + offset?: number + sort?: APISchemas["SortObject"] paged?: boolean + unpaged?: boolean } - SortObject: { empty?: boolean; sorted?: boolean; unsorted?: boolean } + SortObject: { sorted?: boolean; empty?: boolean; unsorted?: boolean } UserPage: { content?: Array pageable?: APISchemas["PageableObject"] @@ -608,7 +618,7 @@ export type APISchemas = { empty?: boolean } PeriodDto: { value?: string; label?: string; period?: string } - PeriodicityDto: { value: string; label: string } + PeriodicityDto: { value?: string; label?: string } EligibleDto: { eligible?: string } OwnerPage: { content?: Array @@ -748,6 +758,7 @@ export type APISchemas = { totalPages?: number first?: boolean last?: boolean + pageable?: APISchemas["PageableObject"] /* Format: int32 */ size?: number content?: Array @@ -756,7 +767,6 @@ export type APISchemas = { sort?: APISchemas["SortObject"] /* Format: int32 */ numberOfElements?: number - pageable?: APISchemas["PageableObject"] empty?: boolean } HealthcheckDto: { status?: string } @@ -779,7 +789,7 @@ export type APISchemas = { empty?: boolean } ContactFirstLoginDto: { - identifier: string + identifier?: string externalId?: string civility?: "Female" | "Male" | "Undefined" lastName?: string @@ -1107,7 +1117,7 @@ export type APIEndpoints = { } } "/api/contacts/{id}": { - responses: { get: APISchemas["ContactFirstLoginDto"]; put: APISchemas["ContactDto"]; delete: null } + responses: { get: null; put: APISchemas["ContactDto"]; delete: null } requests: | { method?: "get"; urlParams: { id: string } } | { @@ -1240,6 +1250,10 @@ export type APIEndpoints = { responses: { get: null } requests: { method?: "get"; urlParams: { id: string } } } + "/api/users/v2": { + responses: { get: Array } + requests: { method?: "get" } + } "/api/temp/moog/campaigns/{idCampaign}/monitoring/progress": { responses: { get: APISchemas["JSONCollectionWrapperMoogProgressDto"] } requests: { method?: "get"; urlParams: { idCampaign: string } } diff --git a/src/ui/Header.tsx b/src/ui/Header.tsx index bb4c323..b3a1cb0 100644 --- a/src/ui/Header.tsx +++ b/src/ui/Header.tsx @@ -8,7 +8,7 @@ import { useUser, useLogout } from "../hooks/useAuth.ts"; import packageInfo from "../../package.json"; export function Header() { - const { preferred_username } = useUser(); + const { decodedToken, isAdminLdap, isUserLdap } = useUser(); const logout = useLogout(); return ( @@ -20,16 +20,28 @@ export function Header() { Platine - Collecte + Gestion v{packageInfo.version} - - {preferred_username} - + + + + + Bienvenue, {decodedToken.preferred_username.toUpperCase()} + + + {isAdminLdap + ? "Vous avez le profil Administrateur" + : isUserLdap + ? "Vous avez le profil Utilisateur" + : "Vous n'avez aucune habilitation annuaire"} + + + - + ); } diff --git a/src/ui/Settings/RoleChip.tsx b/src/ui/Settings/RoleChip.tsx new file mode 100644 index 0000000..fd94e71 --- /dev/null +++ b/src/ui/Settings/RoleChip.tsx @@ -0,0 +1,27 @@ +import { Chip } from "@mui/material"; + +type Props = { + role: string; +}; + +export const roleColorsWidth = [ + { role: "administrateur", color: "#442F99", width: "145px" }, + { role: "responsable", color: "#0C8C87", width: "127px" }, + { role: "gestionnaire", color: "#3C75A2", width: "129px" }, + { role: "assistance", color: "#5A1741", width: "113px" }, +]; + +export const RoleChip = ({ role }: Props) => { + const color = role ? roleColorsWidth.find(r => r.role === role.toLowerCase())?.color : "black"; + const width = role ? roleColorsWidth.find(r => r.role === role.toLowerCase())?.width : undefined; + return ( + + ); +}; diff --git a/src/ui/Settings/SettingsHabilitationsCard.tsx b/src/ui/Settings/SettingsHabilitationsCard.tsx new file mode 100644 index 0000000..aefb3da --- /dev/null +++ b/src/ui/Settings/SettingsHabilitationsCard.tsx @@ -0,0 +1,205 @@ +import { + Card, + Stack, + Divider, + Typography, + Paper, + Table, + TableCell, + TableContainer, + TableHead, + TableRow, + TableBody, + TextField, + CircularProgress, + Button, + TableFooter, + TablePagination, + InputAdornment, + CardContent, +} from "@mui/material"; +import { useFetchQuery } from "../../hooks/useFetchQuery"; +import { SettingsHabilitationsMenu } from "./SettingsHabilitationsMenu"; +import { RoleChip } from "./RoleChip"; +import { Row } from "../Row"; +import { ChangeEvent, useEffect, useState } from "react"; +import AddCircleOutlineOutlinedIcon from "@mui/icons-material/AddCircleOutlineOutlined"; +import { format } from "date-fns"; +import SearchIcon from "@mui/icons-material/Search"; +import { APISchemas } from "../../types/api"; + +interface Column { + id: string; + label: string; + minWidth?: string; + format?: (value: number) => string; +} + +const columns: readonly Column[] = [ + { id: "id", label: "Idep", minWidth: "95px" }, + { id: "name", label: "Nom", minWidth: "95px" }, + { id: "firstName", label: "Prénom", minWidth: "95px" }, + { id: "Organisation", label: "Organisation", minWidth: "150px" }, + { id: "role", label: "Rôle", minWidth: "120px" }, + { id: "pilotRights", label: "Droits Pilotage", minWidth: "50px" }, + { id: "ldapRights", label: "Droits Annuaire", minWidth: "50px" }, + { id: "accreditedSources", label: "Source", minWidth: "100px" }, + { id: "creation", label: "Date de création", minWidth: "120px" }, + { id: "actions", label: "Actions", minWidth: "80px" }, +]; + +export const SettingsHabilitationsCard = () => { + const { data: users /*, refetch*/ } = useFetchQuery("/api/users/v2"); + const [searchList, setSearchList] = useState>([]); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [pageNumber, setPageNumber] = useState(0); + + useEffect(() => { + if (users) setSearchList(users); + }, [users]); + + const handleChangePage = (_: React.MouseEvent | null, newPage: number) => { + setPageNumber(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPageNumber(0); + }; + + function filterValues(e: ChangeEvent): void { + const filteredList = users!.filter(u => + e.target.value.length > 0 + ? u.identifier.toLowerCase().includes(e.target.value.toLowerCase()) || + u.name?.toLowerCase().includes(e.target.value.toLowerCase()) || + u.role?.toLowerCase().includes(e.target.value.toLowerCase()) + : u, + ); + setSearchList(filteredList); + setPageNumber(0); + } + + const sortedUsers = searchList.sort((a, b) => (a.identifier > b.identifier ? 1 : -1)); + if (!users || !searchList) { + return ( + + + + ); + } + + return ( + + + + + {" "} + {"Gestion des habilitations des utilisateurs INSEE"} + + + + + + + + ), + }} + onChange={e => filterValues(e)} + /> + + + + + + + + + {columns.map(column => ( + + {column.label} + + ))} + + + + {sortedUsers + ?.slice(rowsPerPage * pageNumber, rowsPerPage * (pageNumber + 1)) + .map(user => ( + + {user.identifier} + {user.name} + {user.firstName} + {user.organization} + + {user.role ? : null} + + {"not provided"} + {"not provided"} + {user.accreditedSources?.join()} + + {user.creationDate ? `Le ${format(user.creationDate, "dd/MM/yyyy")}` : ""}{" "} + {`par + ${user.creationAuthor}`} + + + + + + ))} + + + + + `${page.from}-${page.to === -1 ? page.count : page.to} sur ${ + page.count + } entités affichées` + } + count={searchList.length} + rowsPerPage={rowsPerPage} + page={pageNumber} + onPageChange={handleChangePage} + onRowsPerPageChange={handleChangeRowsPerPage} + /> + + +
+
+
+
+
+ ); +}; diff --git a/src/ui/Settings/SettingsHabilitationsMenu.tsx b/src/ui/Settings/SettingsHabilitationsMenu.tsx new file mode 100644 index 0000000..0442a13 --- /dev/null +++ b/src/ui/Settings/SettingsHabilitationsMenu.tsx @@ -0,0 +1,59 @@ +import { IconButton, Typography } from "@mui/material"; +import { useState } from "react"; +import MoreVertIcon from "@mui/icons-material/MoreVert"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import { APISchemas } from "../../types/api"; + +type Props = { + user: APISchemas["UserDto"]; +}; + +const options = [ + "Suppression des droits annuaire", + "Suppression des droits Pilotage", + "Modification du rôle dans Pilotage", + "Ajout/suppression de sources dans Pilotage", +]; + +export const SettingsHabilitationsMenu = ({ user }: Props) => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + console.log(user); + + return ( + <> + + + + + {options.map(option => ( + + {option} + + ))} + + + ); +}; diff --git a/src/ui/Settings/SettingsHeader.tsx b/src/ui/Settings/SettingsHeader.tsx new file mode 100644 index 0000000..448daba --- /dev/null +++ b/src/ui/Settings/SettingsHeader.tsx @@ -0,0 +1,25 @@ +import { Stack, IconButton, Typography } from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import { Row } from "../Row"; +import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined"; +import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; + +export const SettingsHeader = () => { + const navigate = useNavigate(); + + return ( + + + navigate(-1)}> + + + + + + {"Réglages"} + + + + + ); +}; diff --git a/src/ui/Survey/SurveyCreateCampaignCard.tsx b/src/ui/Survey/SurveyCreateCampaignCard.tsx index b256a8b..940b4d6 100644 --- a/src/ui/Survey/SurveyCreateCampaignCard.tsx +++ b/src/ui/Survey/SurveyCreateCampaignCard.tsx @@ -30,7 +30,7 @@ export const SurveyCreateCampaignCard = ({ survey }: Props) => { const existingPeriods = campaigns ? campaigns.map(c => c.period?.toString()) : []; const periodicity = existingPeriods[0]?.substring(0, 1); const selectoptions = periods - ?.map(p => p.label!) + ?.map(p => p.value!) .filter(p => p.substring(0, 1) === periodicity && !existingPeriods?.includes(p)) .sort((a, b) => (a > b ? 1 : -1)); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 4c741ad..f230803 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -5,6 +5,8 @@ interface ImportMetaEnv { readonly VITE_OIDC_CLIENT_ID: string; readonly VITE_OIDC_ISSUER: string; readonly VITE_IDENTITY_PROVIDER: string; + readonly VITE_ADMIN_LDAP_ROLE: string; + readonly VITE_USER_LDAP_ROLE: string; } interface ImportMeta {