diff --git a/README.md b/README.md index 8d8b96cb..14d4b481 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ It is dedicated to be deployed as a module of [openimis-fe_js](https://github.co - `Searcher`: generic searcher page (with criteria form and result table) - `Form`: generic form. Manage dirty state, displays add/save button,... - `Table`: generic table. Headers (with -sort-actions), rows,... +- `CommonSnackbar`: generic snackbar. Display banner based on response ## Helpers diff --git a/package.json b/package.json index 596f7dac..2da7ab86 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build": "rollup -c", "start": "rollup -c -w", "format": "prettier src -w", - "prepare":"npm run build" + "prepare": "npm run build" }, "peerDependency": { "react-intl": "^5.8.1", @@ -50,7 +50,9 @@ "src" ], "dependencies": { + "cookie_js": "^1.4.2", "history": "^5.2.0", + "react-idle-timer": "^5.7.2", "react-router": "^5.2.1", "react-router-dom": "^5.2.1" } diff --git a/src/actions.js b/src/actions.js index 870848fd..e7f04da5 100644 --- a/src/actions.js +++ b/src/actions.js @@ -8,6 +8,7 @@ import { formatGQLString, formatMutation, formatServerError, + decodeId, } from "./helpers/api"; const ROLE_FULL_PROJECTION = () => [ @@ -28,8 +29,8 @@ const LANGUAGE_FULL_PROJECTION = () => ["name", "code", "sortOrder"]; const MODULEPERMISSION_FULL_PROJECTION = () => ["modulePermsList{moduleName, permissions{permsName, permsValue}}"]; function getApiUrl() { - let _baseApiUrl = process.env.REACT_APP_API_URL ?? '/api'; - if (_baseApiUrl.indexOf('/') !== 0) { + let _baseApiUrl = process.env.REACT_APP_API_URL ?? "/api"; + if (_baseApiUrl.indexOf("/") !== 0) { _baseApiUrl = `/${_baseApiUrl}`; } return _baseApiUrl; @@ -39,6 +40,7 @@ export const baseApiUrl = getApiUrl(); export function apiHeaders() { let headers = { + // "ngrok-skip-browser-warning": "true", "Content-Type": "application/json", }; return headers; @@ -147,7 +149,7 @@ export function prepareMutation(operation, input, params = {}) { return { operation, variables, clientMutationId: params.clientMutationId }; } -export function waitForMutation(clientMutationId) { +export function waitForMutation(clientMutationId, additionalRequest = "") { return async (dispatch) => { let attempts = 0; let res; @@ -166,6 +168,7 @@ export function waitForMutation(clientMutationId) { clientMutationId jsonContent error + ${additionalRequest} } } } @@ -186,7 +189,14 @@ export function waitForMutation(clientMutationId) { }; } -export function graphqlMutation(mutation, variables, type = "CORE_TRIGGER_MUTATION", params = {}, wait = true) { +export function graphqlMutation( + mutation, + variables, + type = "CORE_TRIGGER_MUTATION", + params = {}, + wait = true, + additionalRequest = "", +) { let clientMutationId; if (variables?.input) { clientMutationId = uuid.uuid(); @@ -197,11 +207,51 @@ export function graphqlMutation(mutation, variables, type = "CORE_TRIGGER_MUTATI if (clientMutationId) { dispatch(fetchMutation(clientMutationId)); if (wait) { - return dispatch(waitForMutation(clientMutationId)); + return dispatch(waitForMutation(clientMutationId, additionalRequest)); } else { return response?.payload?.data; } } + }; +} + +export function graphqlMutation2( + mutation, + variables, + type = "CORE_TRIGGER_MUTATION", + params = {}, + wait = true, + additionalRequest = "", +) { + let clientMutationId; + if (variables?.input) { + clientMutationId = uuid.uuid(); + variables.input.clientMutationId = clientMutationId; + } + return async (dispatch) => { + const response = await dispatch(graphqlWithVariables(mutation, variables, type, params)); + return response?.payload?.data; + }; +} + +export function graphqlMutationLegacy( + payload, + type = "CORE_TRIGGER_MUTATION", + params = {}, + wait = true, + additionalRequest = "", +) { + if (wait && !params.clientMutationId) { + console.error("graphqlMutationLegacy cannot wait with a specified clientMutationId"); + } + return async (dispatch) => { + const response = await dispatch(graphql(payload, type, params)); + dispatch(fetchMutation(params.clientMutationId)); + if (wait) { + return dispatch(waitForMutation(params.clientMutationId, additionalRequest)); + } else { + return response?.payload?.data; + } return response; }; } @@ -212,6 +262,7 @@ export function fetch(config) { [RSAA]: { ...config, headers: { + // "ngrok-skip-browser-warning": "true", "Content-Type": "application/json", ...config.headers, }, @@ -245,6 +296,12 @@ export function login(credentials) { await dispatch(refreshAuthToken()); } const action = await dispatch(loadUser()); + localStorage.setItem("userName", action?.payload?.username); + if (!localStorage.getItem("userLanguage")) { + localStorage.setItem("userLanguage", action?.payload?.i_user?.language); + } + localStorage.setItem("userId", action?.payload?.id); + localStorage.setItem("HfId", action?.payload?.i_user?.health_facility_id); return action.type !== "CORE_AUTH_ERR"; }; } @@ -289,10 +346,23 @@ export function logout() { } `; await dispatch(graphqlMutation(mutation, {})); + localStorage.removeItem("userLanguage"); + localStorage.removeItem("userId"); return dispatch({ type: "CORE_AUTH_LOGOUT" }); }; } +export function CheckAssignedProfile(userID) { + const mutation = `mutation CheckAssignedProfiles { + checkAssignedProfiles(userId:"${userID}") { + status + } + } + `; + return graphql(mutation, "CHECK_ASSIGNED_PROFILE", {}); +} +// } + export function fetchMutation(clientMutationId) { const payload = formatPageQuery( "mutationLogs", @@ -449,12 +519,63 @@ export function roleNameSetValid() { export function saveCurrentPaginationPage(page, afterCursor, beforeCursor, module) { return (dispatch) => { - dispatch({ type: "CORE_PAGINATION_PAGE", payload: { page, afterCursor, beforeCursor, module} }); + dispatch({ type: "CORE_PAGINATION_PAGE", payload: { page, afterCursor, beforeCursor, module } }); }; } export function clearCurrentPaginationPage() { return (dispatch) => { - dispatch({ type: "CORE_PAGINATION_PAGE_CLEAR" }) - } + dispatch({ type: "CORE_PAGINATION_PAGE_CLEAR" }); + }; +} + +export function fetchNotification(userID, first = 10) { + return graphql( + ` query CamuNotifications { + camuNotifications(first: ${first}, user_Id: "${userID}") { + totalCount + edgeCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + node { + id + title + module + message + isRead + createdAt + redirectUrl + } + } + } + } + `, + "CORE_NOTIFICATION_LIST", + ); +} + +function formatNotificationGQL(data) { + return ` + ${!!data.userId ? `userId: "${data.userId}"` : ""} + ${!!data.readAll ? `readAll: ${data.readAll}` : ""} + ${!!data.id ? `notificationId: "${decodeId(data.id)}"` : ""} + `; +} + +export function markNotificationAsRead(data, clientMutationLabel) { + let mutation = `mutation MarkNotificationAsRead { + markNotificationAsRead(${formatNotificationGQL(data)}) { + success + } +}`; + // let mutation = formatMutation("markNotificationAsRead ", formatNotificationGQL(data), clientMutationLabel); + var requestedDateTime = new Date(); + return graphql(mutation, ["CORE_ROLE_MUTATION_REQ", "CORE_CREATE_NOTIFICATION_RESP", "CORE_ROLE_MUTATION_ERR"], { + clientMutationLabel, + }); } diff --git a/src/components/App.css b/src/components/App.css index b41d297c..32a427ba 100644 --- a/src/components/App.css +++ b/src/components/App.css @@ -31,3 +31,9 @@ transform: rotate(360deg); } } + +.MuiMenuItem-root, +.MuiListItem-button, +.MuiButton-root { + transition: background-color 0.3s ease !important; +} diff --git a/src/components/App.js b/src/components/App.js index cc541ca4..35eed9bc 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -17,7 +17,11 @@ import LoginPage from "../pages/LoginPage"; import { useAuthentication } from "../helpers/hooks"; import ForgotPasswordPage from "../pages/ForgotPasswordPage"; import SetPasswordPage from "../pages/SetPasswordPage"; +import VerifyUserAndUpdatePasswordPage from "../pages/VerifyUserAndUpdatePasswordPage"; import { ErrorBoundary } from "@openimis/fe-core"; +import cookie from "cookie_js"; +import { useLocation } from "react-router-dom"; +// import { useQuery } from "./RequireAuth"; export const ROUTER_CONTRIBUTION_KEY = "core.Router"; export const UNAUTHENTICATED_ROUTER_CONTRIBUTION_KEY = "core.UnauthenticatedRouter"; @@ -33,6 +37,20 @@ const styles = () => ({ }, }); +export const getParamsFromUrl = () => { + const params = {}; + const queryString = window.location.search; // e.g. "?id=123&name=Tom" + + if (queryString) { + const urlParams = new URLSearchParams(queryString); + for (const [key, value] of urlParams.entries()) { + params[key] = value; + } + } + + return params; +}; + const App = (props) => { const { history, @@ -48,6 +66,7 @@ const App = (props) => { ...others } = props; + const query = getParamsFromUrl(); const auth = useAuthentication(); const routes = useMemo(() => { return modulesManager.getContribs(ROUTER_CONTRIBUTION_KEY); @@ -77,6 +96,22 @@ const App = (props) => { return { ...messages, ...msgs }; }, [user?.language, messages]); + useEffect(() => { + if (query["hideMenuNavigation"]) { + localStorage.setItem("hideMenuNavigation", "yes"); + } else { + localStorage.removeItem("hideMenuNavigation"); + } + + if (query["JWT"]) { + cookie.set("JWT", query["JWT"], { path: "/" }); + } + + if (query["JWT-refresh-token"]) { + cookie.set("JWT-refresh-token", query["JWT-refresh-token"], { path: "/" }); + } + }, [query]); + useEffect(() => { auth.initialize(); if (process.env.NODE_ENV == "development") { @@ -95,7 +130,7 @@ const App = (props) => { return ( <> - + @@ -109,6 +144,10 @@ const App = (props) => { } /> } /> } /> + } + /> {unauthenticatedRoutes.map((route) => ( { path={"/" + route.path} render={(props) => ( - + )} /> diff --git a/src/components/GedAlertBanner.js b/src/components/GedAlertBanner.js new file mode 100644 index 00000000..140c99d3 --- /dev/null +++ b/src/components/GedAlertBanner.js @@ -0,0 +1,25 @@ +import React from "react"; +import { useGedHealthCheck } from "../helpers/hooks/useGedHealthCheck"; + +export default function GedAlertBanner() { + const { isGedDown, isChecking } = useGedHealthCheck(); + + if (!isGedDown) return <>; + + return ( +
+ {`⚠️ Service GED (DMS) est actuellement indisponible. Veuillez contacter l'équipe technique.`} +
+ ); +} diff --git a/src/components/JournalDrawer.js b/src/components/JournalDrawer.js index 25787cdb..2f7c9586 100644 --- a/src/components/JournalDrawer.js +++ b/src/components/JournalDrawer.js @@ -92,6 +92,14 @@ const styles = (theme) => ({ width: "100%", margin: theme.spacing(1), }, + listContainer: { + height: "40vh", + overflowX: "scroll", + overflowY: "scroll", + scrollbarWidth: "none", + "&::-ms-overflow-style": "none", + "&::-webkit-scrollbar": {width: 0, height: 0} + }, }); class Messages extends Component { @@ -323,12 +331,12 @@ class JournalDrawer extends Component { - + {this.state.displayedMutations.map((m, idx) => ( {m.status == 0 && ( - + )} @@ -364,6 +372,7 @@ class JournalDrawer extends Component { )} + {!!m.clientMutationDetails && ( ({ button: { margin: theme.spacing(2), color: theme.palette.secondary.main, + transition: "background-color 0.3s ease", + "&:hover": { + backgroundColor: "rgba(255, 159, 28, 0.2)", + }, }, })); const LogoutButton = () => { const history = useHistory(); const dispatch = useDispatch(); + const useridlocal = localStorage.getItem("userId"); + const userid = useSelector((store) => store.admin.user?.id); + // console.log("userid",useSelector((store) => store.admin),'useridlocal',useridlocal); const onClick = async () => { - await dispatch(logout()); - history.push("/"); + const response = await dispatch(CheckAssignedProfile(useridlocal)); + if (!!response.payload.data.checkAssignedProfiles.status) { + await dispatch(logout()); + history.push("/"); + } }; const classes = useStyles(); diff --git a/src/components/RequireAuth.js b/src/components/RequireAuth.js index 3c099b01..8abd29d7 100644 --- a/src/components/RequireAuth.js +++ b/src/components/RequireAuth.js @@ -1,28 +1,38 @@ -import React, { useMemo } from "react"; -import withWidth from "@material-ui/core/withWidth"; -import { Redirect } from "../helpers/history"; -import { alpha, useTheme, makeStyles } from "@material-ui/core/styles"; -import { useModulesManager } from "../helpers/modules"; -import LogoutButton from "./LogoutButton"; -import Help from "../pages/Help"; -import clsx from "clsx"; +import React, { useEffect, useMemo, useState } from "react"; +import { useLocation } from "react-router-dom"; + import { AppBar, - Toolbar, - IconButton, - Typography, - Drawer, - Divider, - Tooltip, Button, - Hidden, ClickAwayListener, + Divider, + Drawer, + Hidden, + IconButton, + Toolbar, + Tooltip, + Typography, } from "@material-ui/core"; +import { alpha, makeStyles, useTheme } from "@material-ui/core/styles"; +import withWidth from "@material-ui/core/withWidth"; import MenuIcon from "@material-ui/icons/Menu"; +import NotificationsIcon from "@material-ui/icons/Notifications"; +import { useGraphqlQuery } from "@openimis/fe-core"; +import clsx from "clsx"; +import { useIdleTimer } from "react-idle-timer/dist/index.legacy.cjs.js"; // otherwise not building: https://github.com/SupremeTechnopriest/react-idle-timer/issues/350 +import { useDispatch, useSelector } from "react-redux"; +import { CheckAssignedProfile, logout } from "../actions"; +import { Redirect } from "../helpers/history"; +import { useAuthentication, useBoolean } from "../helpers/hooks"; +import { useModulesManager } from "../helpers/modules"; +import NotificationDialog from "./dialogs/NotificationDialog"; +import GedAlertBanner from "./GedAlertBanner"; import Contributions from "./generics/Contributions"; -import FormattedMessage from "./generics/FormattedMessage"; +import PageTitle from "./hooks/pageTitle"; import JournalDrawer from "./JournalDrawer"; -import { useBoolean, useAuthentication } from "../helpers/hooks"; +import LogoutButton from "./LogoutButton"; +// npm i cookie_js +import cookie from "cookie_js"; export const APP_BAR_CONTRIBUTION_KEY = "core.AppBar"; export const MAIN_MENU_CONTRIBUTION_KEY = "core.MainMenu"; @@ -58,6 +68,10 @@ const useStyles = makeStyles((theme) => ({ menuButton: { margin: theme.spacing(0, 1, 0, 1), padding: 0, + transition: "background-color 0.3s ease", + "&:hover": { + backgroundColor: "rgba(255, 159, 28, 0.2)", + }, }, autoHideMenuButton: { [theme.breakpoints.up("md")]: { @@ -100,6 +114,10 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.secondary.main, textTransform: "none", fontSize: theme.typography.title.fontSize, + transition: "background-color 0.3s ease", + "&:hover": { + backgroundColor: "rgba(255, 159, 28, 0.2)", + }, }, appVersionsBox: { padding: 0, @@ -150,68 +168,183 @@ const useStyles = makeStyles((theme) => ({ }, }, }, + iconContainer: { + position: "relative", + cursor: "pointer", + margin: "3px 9px 0 0", + padding: "8px", + borderRadius: "50%", + transition: "background-color 0.3s ease", + "&:hover": { + backgroundColor: "rgba(255, 159, 28, 0.2)", + }, + }, + iconBtn: { + position: "absolute", + top: "-.5rem", + right: ".8rem", + padding: theme.spacing(1.6), + width: "1.1rem", + // width: "15px", + height: "1.1rem", + background: "red", + borderRadius: "50%", + marginBottom: "20px", + textAlign: "center", + display: "flex", + justifyContent: "center", + alignItems: "center", + }, })); +export function useQuery() { + const { search } = useLocation(); + + return React.useMemo(() => new URLSearchParams(search), [search]); +} + const RequireAuth = (props) => { + // let { hideMenuNavigation } = useParams(); + let query = useQuery(); const { children, logo, redirectTo, ...others } = props; const [isOpen, setOpen] = useBoolean(); const [isDrawerOpen, setDrawerOpen] = useBoolean(); + const [anchorEl, setAnchorEl] = useState(null); const theme = useTheme(); const classes = useStyles(); const modulesManager = useModulesManager(); const auth = useAuthentication(); + const [searchString, setSearchString] = useState(); + const [hideMenu, setHideMenu] = useState(false); + const dispatch = useDispatch(); + const { isLoading, data, error } = useGraphqlQuery( + ` + query ApproverFamiliesCount { + approverFamiliesCount { + approverFamiliesCount + } + } + `, + { str: searchString }, + ); + + useEffect(() => { + if (query.get("hideMenuNavigation")) { + setHideMenu(true); + } + + if (query.get("jwt")) { + cookie.set("JWT", query.get("jwt")); + } + }, [query]); + useEffect(() => { + if (!!data) { + dispatch({ type: "INSUREE_COUNT_RESP", payload: data }); + } + }, [data]); + + const getNotification = useSelector((store) => store.core); + + const bellIcon = (event) => { + if (anchorEl) { + setAnchorEl(null); // Close the dialog if it's already open + } else { + setAnchorEl(event.currentTarget); // Open the dialog if it's closed + } + // if (data?.approverFamiliesCount?.approverFamiliesCount > 0) { + // historyPush(modulesManager, props.history, "insuree.route.pendingApproval"); + // } + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + const id = open ? "notification-popover" : undefined; const isAppBarMenu = useMemo(() => theme.menu.variant.toUpperCase() === "APPBAR", [theme.menu.variant]); + const idleTimeSet = 30 * 60 * 1000; + console.log("process.env.REACT_APP_IDLE_LOGOUT_TIME", Math.floor(process.env.REACT_APP_IDLE_LOGOUT_TIME)); + const idleTimeout = modulesManager.getConf("fe-core", "auth.idleTimeout", Math.floor(idleTimeSet)); // TODO: fix modulesManager - is always empty at this stage, so always using default value + const onIdle = async () => { + const userid = localStorage.getItem("userId"); + const response = await dispatch(CheckAssignedProfile(userid)); + if (!!response.payload.data.checkAssignedProfiles.status) { + await dispatch(logout()); + } + // history.push("/"); + }; + const { startIdleTimer } = useIdleTimer({ + onIdle: onIdle, + timeout: idleTimeout, + throttle: 500, + }); + useEffect(() => { + startIdleTimer; + }, [startIdleTimer]); if (!auth.isAuthenticated) { + sessionStorage.setItem("session_expired", "1"); return ; } return ( <> - - - + - - - - + + + {/* {modulesManager.getOpenIMISVersion()} */} + + - )} - } /> - - - - - {modulesManager.getOpenIMISVersion()} - - - - {isAppBarMenu && ( - - -
+ {isAppBarMenu && ( + + +
+ + + )} + +
+
+
+
{getNotification?.notificationListTotalCount}
+ +
+
- - )} - -
- - - - - + + + {/* */} + + + + )} {isOpen && ( )} - -
+ {!hideMenu && } + {!hideMenu &&
}
+ + {!hideMenu && } {children}
+ ); }; diff --git a/src/components/RoleHeadPanel.js b/src/components/RoleHeadPanel.js index 67b6b40d..2927db67 100644 --- a/src/components/RoleHeadPanel.js +++ b/src/components/RoleHeadPanel.js @@ -70,6 +70,7 @@ class RoleHeadPanel extends FormPanel { label={formatMessage(intl, "core", "roleManagement.isSystem")} control={ this.updateAttribute("isSystem", event.target.checked)} disabled @@ -82,6 +83,7 @@ class RoleHeadPanel extends FormPanel { label={formatMessage(intl, "core", "roleManagement.isBlocked")} control={ this.updateAttribute("isBlocked", event.target.checked)} disabled={!!isReadOnly} diff --git a/src/components/dialogs/AlertDialog.js b/src/components/dialogs/AlertDialog.js index 1def5383..fa49ea33 100644 --- a/src/components/dialogs/AlertDialog.js +++ b/src/components/dialogs/AlertDialog.js @@ -42,7 +42,8 @@ class AlertDialog extends Component { - {ensureArray(alert.message ?? formatMessage(intl, "core", "FatalError.message")).map( + {/* {ensureArray(alert.message ?? formatMessage(intl, "core", "FatalError.message")).map( */} + {ensureArray(formatMessage(intl, "core", `FatalError.${alert.message}`)) ?? formatMessage(intl, "core", "FatalError.message").map( (message, i) => ( {message} diff --git a/src/components/dialogs/NotificationDialog.js b/src/components/dialogs/NotificationDialog.js new file mode 100644 index 00000000..934937a4 --- /dev/null +++ b/src/components/dialogs/NotificationDialog.js @@ -0,0 +1,179 @@ +import React, { useState, useEffect, useRef } from "react"; +import { + IconButton, + Popover, + List, + ListItem, + ListItemText, + ListItemIcon, + Typography, + Divider, + Button, +} from "@material-ui/core"; +import CheckCircleIcon from "@material-ui/icons/CheckCircle"; +import { makeStyles } from "@material-ui/core/styles"; +import { useDispatch, useSelector } from "react-redux"; +import { fetchNotification, markNotificationAsRead } from "../../actions"; +import { historyPush, useHistory } from "../../helpers/history"; +import { useTranslations, formatMessage } from "../../helpers/i18n"; +import { useIntl } from "react-intl"; +import { parseData } from "../../helpers/api"; + +const useStyles = makeStyles((theme) => ({ + popover: { + position: "relative", + "&::before": { + content: '""', + position: "absolute", + top: -8, + left: "calc(50% - 8px)", // Center the arrow horizontally + width: 0, + height: 0, + borderLeft: "8px solid transparent", + borderRight: "8px solid transparent", + borderBottom: `8px solid ${theme.palette.background.paper}`, + zIndex: 1, + }, + }, + arrow: { + position: "absolute", + top: "-8px", + left: "calc(50% - 8px)", + width: 0, + height: 0, + borderLeft: "8px solid transparent", + borderRight: "8px solid transparent", + borderBottom: `8px solid ${theme.palette.background.paper}`, + zIndex: 1, + }, + clearAllButton: { + padding: 0, + minHeight: "auto", + minWidth: "auto", + color: theme.palette.primary.main, + }, +})); + +const NotificationDialog = ({ anchorEl, onClose, open, id, notificationData, modulesManager }) => { + const [notifications, setNotifications] = useState([]); + const [hasFetchedMore, setHasFetchedMore] = useState(false); + const intl = useIntl(); + const { formatDateFromISO } = useTranslations("core", modulesManager); + const classes = useStyles(); + const dispatch = useDispatch(); + const history = useHistory(); + const userid = localStorage.getItem("userId"); + const userLanguage = localStorage.getItem("userLanguage"); + const checkRes = useSelector((store) => store.core); + const listRef = useRef(null); + + useEffect(() => { + // if (open) { + dispatch(fetchNotification(userid)); + // } + }, [open]); + + useEffect(() => { + if (notificationData) { + setNotifications(notificationData); + } + }, [notificationData]); + + const handleClearAll = () => { + const res = dispatch(markNotificationAsRead({ readAll: true, userId: userid }, "label")); + if (!!res) { + dispatch(fetchNotification(userid)); + setNotifications([]); + } + }; + + const handleListItemClick = (notification) => { + if (!!notification.redirectUrl) { + history.push(notification.redirectUrl); + const res = dispatch(markNotificationAsRead({ ...notification, userId: userid }, "label")); + if (!!res) { + dispatch(fetchNotification(userid)); + } + onClose(); + } + }; + + const getNotificationMessage = (message) => { + try { + const parsedMessage = JSON.parse(message); + return parsedMessage[userLanguage] || parsedMessage["en"]; + } catch (error) { + console.error("Failed to parse notification message:", error); + return message; // Fallback to the original message if parsing fails + } + }; + + const handleScroll = () => { + const threshold = 1; + const isBottom = + listRef.current.scrollHeight - listRef.current.scrollTop <= listRef.current.clientHeight + threshold; + if (isBottom && !hasFetchedMore) { + dispatch( + fetchNotification( + userid, + checkRes.notificationListTotalCount < 100 ? checkRes.notificationListTotalCount : 100, + ), + ); + setHasFetchedMore(true); // Set to true to prevent further API calls + } + }; + + return ( +
+ + {/*
*/} +
+ {formatMessage(intl, "core", "title")} + +
+ + + {notifications.length > 0 ? ( + notifications.map((notification, index) => ( + + {/* */} + handleListItemClick(notification)}> + + + + + + {index < notifications.length - 1 && } + + )) + ) : ( + + {formatMessage(intl, "core", "emptyNotification")} + + )} + +
+
+ ); +}; + +export default NotificationDialog; diff --git a/src/components/generics/CommonSnakbar.js b/src/components/generics/CommonSnakbar.js new file mode 100644 index 00000000..45dc7eff --- /dev/null +++ b/src/components/generics/CommonSnakbar.js @@ -0,0 +1,132 @@ +import React, { useState, useCallback } from "react"; +import Snackbar from "@material-ui/core/Snackbar"; +import Alert from "@material-ui/lab/Alert"; +import IconButton from "@material-ui/core/IconButton"; +import FileCopyIcon from "@material-ui/icons/FileCopy"; +import CloseIcon from "@material-ui/icons/Close"; +import ErrorIcon from "@material-ui/icons/Error"; +import WarningIcon from "@material-ui/icons/Warning"; +import InfoIcon from "@material-ui/icons/Info"; +import CheckCircleIcon from "@material-ui/icons/CheckCircle"; +import { makeStyles } from "@material-ui/core/styles"; +import { formatMessage } from "@openimis/fe-core"; +const useStyles = makeStyles((theme) => ({ + snackbar: { + marginRight: "50px", + color: "white", + }, + alert: { + color: "white", + display: "flex", + alignItems: "center", + }, + copyContainer: { + display: "flex", + alignItems: "center", + }, + copyText: { + marginLeft: "4px", + }, + whiteIcon: { + color: "white", + }, +})); + +const CommonSnackbar = ({ intl,open, onClose, message, severity, copyText = "" }) => { + const [isCopied, setIsCopied] = useState(false); + const classes = useStyles(); + + const handleCopyClick = useCallback(() => { + if (navigator.clipboard) { + navigator.clipboard + .writeText(copyText) + .then(() => { + setIsCopied(true); + }) + .catch((err) => { + console.error("Failed to copy text: ", err); + }); + } + }, [copyText]); + + const handleClose = (event, reason) => { + if (reason === "clickaway") { + return; + } + onClose(event, reason); + setIsCopied(false); + }; + + + const getIcon = () => { + switch (severity) { + case "error": + return ; + case "warning": + return ; + case "info": + return ; + case "success": + return ; + default: + return null; + } + }; + + const getBackgroundColor = () => { + switch (severity) { + case "error": + return "#d32f2f"; + case "warning": + return "#ffa000"; + case "info": + return "#1976d2"; + case "success": + return "#4caf50"; + default: + return "inherit"; + } + }; + + return ( + + + {copyText && ( +
+
{copyText}
+ + + + {/* {isCopied ? "Copied!" : "Copy"} */} + {isCopied ? formatMessage(intl, "core", "common.Copied") : formatMessage(intl, "core", "common.Copy")} + +
+ )} + + + + + } + > + {message} +
+
+ ); +}; + +export default CommonSnackbar; diff --git a/src/components/generics/ConstantBasedPicker.js b/src/components/generics/ConstantBasedPicker.js index bf9273d7..f5de1ec2 100644 --- a/src/components/generics/ConstantBasedPicker.js +++ b/src/components/generics/ConstantBasedPicker.js @@ -22,10 +22,17 @@ class ConstantBasedPicker extends Component { } } - _formatValue = (v) => - v === null - ? formatMessage(this.props.intl, this.props.module, this.props.nullLabel ?? `${this.props.label}.null`) - : formatMessage(this.props.intl, this.props.module, `${this.props.label}.${v}`); + _formatValue = (v) => { + if (v === null) { + return formatMessage(this.props.intl, this.props.module, this.props.nullLabel ?? `${this.props.label}.null`); + } + + if (this.props.getValueFrom) { + return this.props.getValueFrom(v); + } + + return formatMessage(this.props.intl, this.props.module, `${this.props.label}.${v}`); + }; _onChange = (v) => { this.setState({ value: v }, (e) => { @@ -61,7 +68,7 @@ class ConstantBasedPicker extends Component { .map((v) => ({ value: v, label: this._formatValue(v), - })) + })), ); return ( ({ paper: theme.paper.paper, paperHeader: theme.paper.header, paperHeaderAction: theme.paper.action, fab: theme.fab, + fabAbove: { + position: "fixed", + bottom: 20, + right: 8, + zIndex: 2000, + marginBottom: "70px", + }, + fabAboveRework: { + position: "fixed", + bottom: 80, + right: 8, + zIndex: 2000, + marginBottom: "80px", + }, + fabMargin: { + marginBottom: "135px", + }, + fabPayMargin: { + marginBottom: "200px", + }, + customFab: { + background: "#fff", + border: "2px solid #FF841C", // Your desired border color + color: "#FF841C", // Your desired text/icon color + }, + customFabReject: { + background: "#fff", + border: "2px solid #FF0000", // Your desired border color + color: "#FF0000", // Your desired text/icon color + }, + customFabRework: { + background: "orange", + border: "2px solid orange", // Your desired border color + color: "#FFF", // Your desired text/icon color + }, + submitFabRework: { + background: "green", + border: "2px solid green", // Your desired border color + color: "#FFF", // Your desired text/icon color + }, + fabSubmitRework: { + position: "fixed", + bottom: 1, + right: 8, + zIndex: 2000, + }, + fabSubmitMargin: { + marginBottom: "80px", + }, }); class Form extends Component { state = { dirty: false, saving: false, + hideMenuNavigation: false, }; componentDidMount() { if (!!this.props.forcedDirty) { this.setState((state, props) => ({ dirty: true })); } + + // get query params from url using window.location.search + const query = new URLSearchParams(window.location.search); + this.setState({ hideMenuNavigation: query.get("hideMenuNavigation") }); } componentDidUpdate(prevProps, prevState, snapshot) { @@ -38,6 +99,9 @@ class Form extends Component { } else if (prevProps.update !== this.props.update) { this.setState({ saving: false }); } + if (prevProps.cancelSaveDisable !== this.props.cancelSaveDisable) { + this.setState({ saving: false }); + } } onEditedChanged = (data) => { @@ -69,8 +133,31 @@ class Form extends Component { headPanelContributionsKey, Panels, contributedPanelsKey = null, + emailButton, + print, + email, + hasReject, + allApproved, + approveorreject, + handleDialogOpen, + printButton, + approverData, + paymentPrint, + paymentApprove, + success, + edited, + rework, + actionRework, + reworkRequest, + exceptionApprove, + showPaidButton, + paid, + claimStatus, + showSubmitButton, + submitted, ...others } = this.props; + let userId = localStorage.getItem("userId"); return (
@@ -80,7 +167,7 @@ class Form extends Component { - {!!back && ( + {!!back && !this.state.hideMenuNavigation && ( @@ -99,6 +186,13 @@ class Form extends Component { {!!actions && ( + {claimStatus && ( + + + {formatMessage(this.props.intl, "claim", `claimStatus.${claimStatus}`)} + + + )} {actions.map((a, idx) => { if (!!a.onlyIfDirty && !this.state.dirty) return null; if (!!a.onlyIfNotDirty && !!this.state.dirty) return null; @@ -155,8 +249,175 @@ class Form extends Component { /> )} + {paymentApprove && !this.state.hideMenuNavigation ? ( + <> + {withTooltip( +
+ approveorreject({ ...this.props.edited, status: -1 })} + // disabled={!!this.state.saving || (!!canSave && !canSave())} + > + + +
, + addTooltip || formatMessage(this.props.intl, module, "rejectTooltip"), + )} + {withTooltip( +
+ approveorreject({ ...this.props.edited, status: 5 })} + // disabled={!!this.state.saving || (!!canSave && !canSave())} + > + + +
, + addTooltip || formatMessage(this.props.intl, module, "approveTooltip"), + )} + {/* {withTooltip( +
+ rework({ ...this.props.edited, status: 3 })} + // disabled={!!this.state.saving || (!!canSave && !canSave())} + > + + +
, + addTooltip || formatMessage(this.props.intl, module, "rejectTooltip"), + )} */} + + ) : null} + {paymentApprove && !this.state.hideMenuNavigation && actionRework ? ( + <> + {withTooltip( +
+ rework({ ...this.props.edited, status: 5 })} + // disabled={!!this.state.saving || (!!canSave && !canSave())} + > + + +
, + addTooltip || formatMessage(this.props.intl, module, "reworkTooltip"), + )} + + ) : null} + {showPaidButton && !this.state.hideMenuNavigation ? ( + <> + {withTooltip( +
+ paid({ ...this.props.edited, status: 5 })} + // disabled={!!this.state.saving || (!!canSave && !canSave())} + > + + +
, + // addTooltip || formatMessage(this.props.intl, module, "rejectTooltip"), + )} + + ) : null} + {exceptionApprove && !this.state.hideMenuNavigation ? ( + <> + {withTooltip( +
+ approveorreject({ ...this.props.edited, status: -1 })} + // disabled={!!this.state.saving || (!!canSave && !canSave())} + > + + +
, + addTooltip || formatMessage(this.props.intl, module, "addTooltip"), + )} + {withTooltip( +
+ approveorreject({ ...this.props.edited, status: 5 })} + // disabled={!!this.state.saving || (!!canSave && !canSave())} + > + + +
, + addTooltip || formatMessage(this.props.intl, module, "approveTooltip"), + )} + + ) : null} + {/* {title == "Insuree.title" && + this.props.edited?.biometricsStatus && + this.props.edited?.status == "WAITING_FOR_APPROVAL" ? ( */} + {title == "Insuree.title" && + this.props.edited?.biometricsStatus && + approverData == userId && + this.props.edited?.status == "WAITING_FOR_APPROVAL" ? ( + hasReject && this.props?.edited?.status !== "REJECTED" && this.props?.edited?.status !== "REWORK" ? ( + <> + {withTooltip( +
+ handleDialogOpen("rework", this.props.edited)} + disabled={!!this.state.saving || (!!canSave && !canSave())} + > + + +
, + addTooltip || formatMessage(this.props.intl, module, "reworkTooltip"), + )} + + {withTooltip( +
+ handleDialogOpen("reject", this.props.edited)} + disabled={!!this.state.saving || (!!canSave && !canSave())} + > + + +
, + addTooltip || formatMessage(this.props.intl, module, "rejectTooltip"), + )} + + ) : allApproved && this.props.edited.biometricsIsMaster && this.props?.edited?.status !== "APPROVED" ? ( + // && this.props?.edited?.biometricsIsMaster + <> + {withTooltip( +
+ handleDialogOpen("reject", this.props.edited)} + disabled={!!this.state.saving || (!!canSave && !canSave())} + > + + +
, + addTooltip || formatMessage(this.props.intl, module, "addTooltip"), + )} + {withTooltip( +
+ approveorreject({ ...this.props.edited, status: "APPROVED" })} + disabled={!!this.state.saving || (!!canSave && !canSave())} + > + + +
, + addTooltip || formatMessage(this.props.intl, module, "approveTooltip"), + )} + + ) : null + ) : null} + {!this.state.dirty && - !!add && !save && + !!add && + !save && withTooltip(
@@ -167,6 +428,7 @@ class Form extends Component { )} {(!!this.state.dirty || !!openDirty) && !!save && + !paymentApprove && withTooltip(
, - saveTooltip || formatMessage(this.props.intl, module, "saveTooltip"), + // saveTooltip || formatMessage(this.props.intl, module, "saveTooltip"), )} + {this.props.paymentPrint && !this.state.hideMenuNavigation + ? withTooltip( +
+
+ printButton(this.props.edited)} + > + + +
+
, + // saveTooltip || formatMessage(this.props.intl, module, "saveTooltip"), + ) + : ""} + {/* {(!!this.props.email && this.props.edited.email != "") ? */} + {this.props.print && !this.state.hideMenuNavigation && this.props.edited.email != "" + ? withTooltip( +
+
+ printButton(this.props.edited)} + > + + +
+
, + // saveTooltip || formatMessage(this.props.intl, module, "saveTooltip"), + ) + : ""} + {!!this.props.email && this.props.edited.email != "" + ? withTooltip( +
+ emailButton(this.props.edited)} + > + + +
, + // saveTooltip || formatMessage(this.props.intl, module, "saveTooltip"), + ) + : ""} {!this.state.dirty && !!fab && withTooltip( @@ -189,6 +500,22 @@ class Form extends Component {
, fabTooltip, )} + {showSubmitButton ? ( + <> + {withTooltip( +
+ submitted({ ...this.props.edited, status: 5 })} + // disabled={!!this.state.saving || (!!canSave && !canSave())} + > + + +
, + addTooltip || formatMessage(this.props.intl, module, "Submit"), + )} + + ) : null}
); } diff --git a/src/components/generics/MainMenuContribution.js b/src/components/generics/MainMenuContribution.js index 06e55dfb..c49e6ba4 100644 --- a/src/components/generics/MainMenuContribution.js +++ b/src/components/generics/MainMenuContribution.js @@ -1,28 +1,32 @@ -import React, { Component, Fragment } from "react"; -import PropTypes from "prop-types"; -import MuiAccordion from "@material-ui/core/Accordion"; -import MuiAccordionDetails from "@material-ui/core/AccordionDetails"; -import MuiAccordionSummary from "@material-ui/core/AccordionSummary"; -import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; -import Typography from "@material-ui/core/Typography"; -import { withTheme, withStyles } from "@material-ui/core/styles"; -import ListItem from "@material-ui/core/ListItem"; -import ListItemIcon from "@material-ui/core/ListItemIcon"; -import ListItemText from "@material-ui/core/ListItemText"; import { + Box, + Button, + ClickAwayListener, Divider, - List, + Grow, IconButton, - MenuList, + List, MenuItem, - Button, - Popper, - Grow, + MenuList, Paper, - ClickAwayListener, + Popper, } from "@material-ui/core"; -import withModulesManager from "../../helpers/modules"; +import MuiAccordion from "@material-ui/core/Accordion"; +import MuiAccordionDetails from "@material-ui/core/AccordionDetails"; +import MuiAccordionSummary from "@material-ui/core/AccordionSummary"; +import ListItem from "@material-ui/core/ListItem"; +import ListItemIcon from "@material-ui/core/ListItemIcon"; +import ListItemText from "@material-ui/core/ListItemText"; +import Typography from "@material-ui/core/Typography"; +import { withStyles, withTheme } from "@material-ui/core/styles"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import PropTypes from "prop-types"; +import React, { Component, Fragment } from "react"; +import { useIntl } from "react-intl"; +import { useLocation } from "react-router-dom"; import { _historyPush } from "../../helpers/history"; +import withModulesManager from "../../helpers/modules"; +import UsePageTitle from "../hooks/usePageTitle"; const styles = (theme) => ({ panel: { @@ -42,6 +46,11 @@ const styles = (theme) => ({ color: theme.palette.text.second, paddingTop: theme.menu.appBar.fontSize / 2, textTransform: "none", + transition: "all 0.3s ease", + "&:hover": { + backgroundColor: "rgba(255, 159, 28, 0.2)", + color: "#ff9f1c", + }, }, appBarMenuPaper: { borderTopLeftRadius: 0, @@ -75,9 +84,13 @@ const AccordionSummary = withStyles({ borderBottom: "1px solid rgba(0, 0, 0, .125)", marginBottom: -1, minHeight: 56, + transition: "background-color 0.3s ease", "&$expanded": { minHeight: 56, }, + "&:hover": { + backgroundColor: "rgba(255, 159, 28, 0.2)", + }, }, content: { "&$expanded": { @@ -113,14 +126,14 @@ class MainMenuContribution extends Component { handleMenuSelect = (e, route) => { // block normal href only for left click - if (e.type === 'click') { + if (e.type === "click") { e.stopPropagation(); e.preventDefault(); } this.toggleExpanded(e); this.redirect(route); }; - + redirect(route) { const { modulesManager, history } = this.props; _historyPush(modulesManager, history, route); @@ -129,10 +142,8 @@ class MainMenuContribution extends Component { appBarMenu = () => { return ( - + + - {this.props.entries.map((entry, idx) => ( -
- this.handleMenuSelect(e, entry.route)} component="a" href={`${process.env.PUBLIC_URL || ""}${entry.route}`} passHref> - {entry.icon} - - - - {entry.withDivider && ( - - )} -
- ))} + this.handleMenuSelect({}, route)} + isAppBar={true} + />
@@ -183,23 +186,12 @@ class MainMenuContribution extends Component { - {this.props.entries.map((entry, idx) => ( - - { - this.redirect(entry.route); - }} - > - {entry.icon} - - - {entry.withDivider && ( - - )} - - ))} + this.redirect(route)} + /> @@ -222,4 +214,195 @@ MainMenuContribution.propTypes = { history: PropTypes.object.isRequired, }; +const ButtonMenu = ({ state, toggleExpanded, props }) => { + const page = UsePageTitle(); + const intl = useIntl(); + + const translateKey = (key) => { + if (!key) return key; + try { + const translated = intl.formatMessage({ id: key }); + return translated !== key ? translated : key; + } catch { + return key; + } + }; + + const translatedParent = translateKey(page.parent); + const translatedHeader = translateKey(props.header); + + return ( + <> + + + ); +}; + +const isMenuItemActive = (entryRoute, currentPathname, allEntries) => { + if (!entryRoute || !currentPathname) { + return false; + } + + if (currentPathname === entryRoute) { + return true; + } + + const hasMoreSpecificMatch = allEntries.some((otherEntry) => { + const otherRoute = otherEntry.route; + if (!otherRoute || otherRoute === entryRoute) { + return false; + } + + if (otherRoute.length > entryRoute.length && currentPathname.startsWith(otherRoute)) { + const nextChar = currentPathname[otherRoute.length]; + if (nextChar === "/" || nextChar === undefined) { + return true; + } + } + return false; + }); + + if (hasMoreSpecificMatch) { + return false; + } + + if (currentPathname.startsWith(entryRoute) && entryRoute !== "/") { + const nextChar = currentPathname[entryRoute.length]; + return nextChar === "/" || nextChar === undefined; + } + + return false; +}; + +const MenuItemsList = ({ entries, header, classes, redirect, isAppBar = false }) => { + const location = useLocation(); + const page = UsePageTitle(); + const currentPath = location.pathname; + + return ( + <> + {entries.map((entry, idx) => { + const isActive = isMenuItemActive(entry.route, currentPath, entries); + + if (isAppBar) { + return ( +
+ { + e.stopPropagation(); + e.preventDefault(); + redirect(entry.route); + }} + component="a" + href={`${process.env.PUBLIC_URL || ""}${entry.route}`} + passHref + selected={isActive} + style={{ + backgroundColor: isActive ? "rgba(255, 159, 28, 0.1)" : "transparent", + transition: "background-color 0.3s ease", + }} + onMouseEnter={(e) => { + if (!isActive) { + e.currentTarget.style.backgroundColor = "rgba(255, 159, 28, 0.15)"; + const icon = e.currentTarget.querySelector(".MuiListItemIcon-root"); + const text = e.currentTarget.querySelector(".MuiListItemText-primary"); + if (icon) icon.style.color = "#ff9f1c"; + if (text) text.style.color = "#ff9f1c"; + } + }} + onMouseLeave={(e) => { + if (!isActive) { + e.currentTarget.style.backgroundColor = "transparent"; + const icon = e.currentTarget.querySelector(".MuiListItemIcon-root"); + const text = e.currentTarget.querySelector(".MuiListItemText-primary"); + if (icon) icon.style.color = "inherit"; + if (text) text.style.color = "inherit"; + } + }} + > + + {entry.icon} + + + + {entry.withDivider && } +
+ ); + } + + return ( + + { + redirect(entry.route); + }} + selected={isActive} + style={{ + backgroundColor: isActive ? "rgba(255, 159, 28, 0.1)" : "transparent", + borderLeft: isActive ? "3px solid #ff9f1c" : "3px solid transparent", + transition: "background-color 0.3s ease", + }} + onMouseEnter={(e) => { + if (!isActive) { + e.currentTarget.style.backgroundColor = "rgba(255, 159, 28, 0.15)"; + const icon = e.currentTarget.querySelector(".MuiListItemIcon-root"); + const text = e.currentTarget.querySelector(".MuiListItemText-primary"); + if (icon) icon.style.color = "#ff9f1c"; + if (text) text.style.color = "#ff9f1c"; + } + }} + onMouseLeave={(e) => { + if (!isActive) { + e.currentTarget.style.backgroundColor = "transparent"; + const icon = e.currentTarget.querySelector(".MuiListItemIcon-root"); + const text = e.currentTarget.querySelector(".MuiListItemText-primary"); + if (icon) icon.style.color = "inherit"; + if (text) text.style.color = "inherit"; + } + }} + > + + {entry.icon} + + + + {entry.withDivider && } + + ); + })} + + ); +}; + export default withModulesManager(withTheme(withStyles(styles)(MainMenuContribution))); diff --git a/src/components/generics/Picker.js b/src/components/generics/Picker.js index dbfdc1c1..d51de1ce 100644 --- a/src/components/generics/Picker.js +++ b/src/components/generics/Picker.js @@ -22,7 +22,7 @@ import FakeInput from "../inputs/FakeInput"; const styles = (theme) => ({ label: { - color: theme.palette.primary.main, + color: theme.palette.text.primary, }, dialogTitle: theme.dialog.title, dialogContent: theme.dialog.content, diff --git a/src/components/generics/Searcher.js b/src/components/generics/Searcher.js index 8aa3b26f..c88f9f91 100644 --- a/src/components/generics/Searcher.js +++ b/src/components/generics/Searcher.js @@ -32,13 +32,15 @@ import Table from "./Table"; const styles = (theme) => ({ root: { width: "100%", + margin: 24 }, paper: theme.paper.body, paperHeader: theme.paper.header, - paperHeaderTitle: theme.paper.title, + paperHeaderTitle: theme.paper.headerTitles, paperHeaderMessage: theme.paper.message, paperHeaderAction: { paddingInline: 5, + color: "#00913E" }, paperDivider: theme.paper.divider, tableHeaderAction: theme.table.headerAction, @@ -83,12 +85,12 @@ class SelectionMenu extends Component { {entries.map((i, idx) => ( - + ))} - {this.props.exportable && ( ))} {this.props.exportable && ( - )} @@ -148,8 +150,9 @@ class SelectionMenu extends Component { actions = [], processing, actionsContributionKey = null, + isMenu = true } = this.props; - + console.log(actions, isMenu,"actionss") let contributed_entries = modulesManager.getContribs(actionsContributionKey); if (!actions.length && !contributed_entries) return null; if (processing) { @@ -169,9 +172,11 @@ class SelectionMenu extends Component { entries.push({ text: formatMessage(intl, "claim", a.label), action: a.action }); } }); - if (entries.length > 2 || (this.props.exportable && entries.length>=1)) { + if (entries.length > 5 || (this.props.exportable && entries.length >= 1)) { return this.renderMenu(entries, actionsContributionKey); } else { + console.log(entries,'entries'); + return this.renderButtons(entries, actionsContributionKey); } } @@ -249,7 +254,15 @@ class Searcher extends Component { filters[filter.id] = { value: filter.value, filter: filter.filter }; } }); - this.setState({ filters }, (e) => this.applyFilters()); + // console.log("this.props.updateIsVerifyInsuree", this.props.updateIsVerifyInsuree) + // this.setState({ filters }, (e) => this.applyFilters()); + this.setState({ filters }, () => { + if (this.props.updateIsVerifyInsuree) { + this.props.updateIsVerifyInsuree(false); // Update isVerifyInsuree in the parent component + this.applyFilters(); + } + this.applyFilters(); + }); }; _cacheAndApply = () => { @@ -352,6 +365,7 @@ class Searcher extends Component { (e) => a(s) ); }; + headerActions = (filters) => { if (!!this.props.headerActions) return this.props.headerActions(filters); @@ -359,13 +373,13 @@ class Searcher extends Component { return this.props.sorts(filters).map((s) => !!s ? [ - () => - this.setState( - (state, props) => ({ orderBy: sort(state.orderBy, s[0], s[1]) }), - (e) => this.props.fetch(this.filtersToQueryParams()) - ), - () => formatSorter(this.state.orderBy, s[0], s[1]), - ] + () => + this.setState( + (state, props) => ({ orderBy: sort(state.orderBy, s[0], s[1]) }), + (e) => this.props.fetch(this.filtersToQueryParams()) + ), + () => formatSorter(this.state.orderBy, s[0], s[1]), + ] : [null, () => null] ); } @@ -403,6 +417,7 @@ class Searcher extends Component { processing = false, withSelection = null, actionsContributionKey = null, + isMenu = true, withPagination = true, exportable = false, exportFetch = null, @@ -425,12 +440,13 @@ class Searcher extends Component { onChangeFilters={this.onChangeFilters} FilterExt={FilterExt} filterPaneContributionsKey={filterPaneContributionsKey} + reset={this.resetFilters} /> } /> )} {!!contributionKey && } - + {errorItems ? ( @@ -449,7 +465,7 @@ class Searcher extends Component { - {fetchedItems && ( + {fetchedItems && ( {!!localPreHeaders && localPreHeaders.length > 0 && ( - + {localPreHeaders.map((h, idx) => { if (headerSpans.length > idx && !headerSpans[idx]) return null; @@ -221,7 +221,7 @@ class Table extends Component { )} {!!localHeaders && localHeaders.length > 0 && ( - + {localHeaders.map((h, idx) => { if (headerSpans.length > idx && !headerSpans[idx]) return null; @@ -323,7 +323,7 @@ class Table extends Component { {(fetching || error) && ( - {" "} + {/* We do not want to display the spinner with the empty table */} )} diff --git a/src/components/hooks/pageTitle.js b/src/components/hooks/pageTitle.js new file mode 100644 index 00000000..a9555301 --- /dev/null +++ b/src/components/hooks/pageTitle.js @@ -0,0 +1,75 @@ +import { Box, Grid, Typography } from "@material-ui/core"; +import { makeStyles } from "@material-ui/styles"; +import React from "react"; +import { useSelector } from "react-redux"; +import { useLocation } from "react-router-dom"; +import UsePageTitle from "./usePageTitle"; + +const useStyles = makeStyles((theme) => ({ + container: theme.page, + parentTitle: { + fontSize: "0.875rem", + color: theme.palette.text.secondary, + fontWeight: 500, + textTransform: "uppercase", + letterSpacing: "0.5px", + marginBottom: theme.spacing(0.5), + }, + mainTitle: { + fontSize: "1.75rem", + fontWeight: 600, + color: theme.palette.text.primary, + marginBottom: theme.spacing(0.5), + }, + subtitle: { + fontSize: "0.9375rem", + color: theme.palette.text.secondary, + fontWeight: 400, + marginTop: theme.spacing(0.5), + }, +})); + +function PageTitle() { + const classes = useStyles(); + const page = UsePageTitle(); + const location = useLocation(); + const user = useSelector((state) => state.core.user); + + if (!page.parent && !page.title) { + return <>; + } + + const isHomePage = location.pathname === "/home" || location.pathname === "/front/home"; + const username = user?.i_user?.username || user?.username || ""; + + let displaySubtitle = page.subtitle; + if (isHomePage && username) { + displaySubtitle = `Bienvenue sur le compte ${username} !`; + } + + return ( + + + + {page.parent && ( + + {page.parent} + + )} + {page.title && ( + + {page.title} + + )} + {displaySubtitle && ( + + {displaySubtitle} + + )} + + + + ); +} + +export default PageTitle; diff --git a/src/components/hooks/routes.js b/src/components/hooks/routes.js new file mode 100644 index 00000000..1922856a --- /dev/null +++ b/src/components/hooks/routes.js @@ -0,0 +1,439 @@ +const affiliation = [ + { + parent: "insuree.mainMenu", + path: "/insuree/families", + title: "insuree.menu.familiesOrGroups", + subtitle: "core.routes.affiliation.families.subtitle", + }, + { + parent: "insuree.mainMenu", + path: "/insuree/insurees", + title: "insuree.menu.insurees", + subtitle: "core.routes.affiliation.insurees.subtitle", + }, + { + parent: "insuree.mainMenu", + path: "/policy/policies", + title: "policy.menu.policies", + subtitle: "core.routes.affiliation.policies.subtitle", + }, + { + parent: "insuree.mainMenu", + title: "core.routes.affiliation.assignment.title", + path: "/insuree/insurees/PendingApprovalAssignemnt", + subtitle: "core.routes.affiliation.assignment.subtitle", + }, +]; + +const administration = [ + { + parent: "admin.mainMenu", + path: "/admin/users", + title: "admin.menu.users", + subtitle: "core.routes.administration.users.subtitle", + }, + { + parent: "admin.mainMenu", + path: "/admin/bank", + title: "core.routes.administration.bank.title", + subtitle: "core.routes.administration.bank.subtitle", + }, + { + parent: "admin.mainMenu", + path: "/admin/fosaConfiguration", + title: "core.routes.administration.fosaCategories.title", + subtitle: "core.routes.administration.fosaCategories.subtitle", + }, + { + parent: "admin.mainMenu", + path: "/admin/operation", + title: "core.routes.administration.operations.title", + subtitle: "core.routes.administration.operations.subtitle", + }, + { + parent: "admin.mainMenu", + path: "/admin/serviceActs", + title: "core.routes.administration.medicalServices.title", + subtitle: "core.routes.administration.medicalServices.subtitle", + }, + { + parent: "admin.mainMenu", + path: "/location/locations", + title: "admin.menu.locations", + subtitle: "core.routes.administration.locations.subtitle", + }, + { + parent: "admin.mainMenu", + path: "/location/centers", + title: "core.routes.administration.centers.title", + subtitle: "core.routes.administration.centers.subtitle", + }, + { + parent: "admin.mainMenu", + path: "/roles", + title: "core.routes.administration.roles.title", + subtitle: "core.routes.administration.roles.subtitle", + }, + { + parent: "admin.mainMenu", + path: "/policyHolderUsers", + title: "core.routes.administration.policyholderAdmins.title", + subtitle: "core.routes.administration.policyholderAdmins.subtitle", + }, + { + parent: "admin.mainMenu", + path: "/admin/dashboard", + title: "core.routes.administration.dashboard.title", + subtitle: "core.routes.administration.dashboard.subtitle", + }, + { + parent: "admin.mainMenu", + path: "/admin/fosa/users", + title: "core.routes.administration.fosaUsers.title", + subtitle: "core.routes.administration.fosaUsers.subtitle", + }, + { + parent: "admin.mainMenu", + path: "/admin/policyholder/users", + title: "core.routes.administration.policyholderUsers.title", + subtitle: "core.routes.administration.policyholderUsers.subtitle", + }, + { + parent: "admin.mainMenu", + path: "/admin/fosa/user-functionalities", + title: "core.routes.administration.fosaUserFunctionalities.title", + subtitle: "core.routes.administration.fosaUserFunctionalities.subtitle", + }, + { + parent: "admin.mainMenu", + path: "/admin/fosa/user-specialties", + title: "core.routes.administration.fosaUserSpecialties.title", + subtitle: "core.routes.administration.fosaUserSpecialties.subtitle", + }, +]; + +const demandePayement = [ + { + parent: "claim.mainMenu", + path: "/claim/reviews", + title: "claim.menu.reviews", + subtitle: "core.routes.payment.reviews.subtitle", + }, + { + parent: "claim.mainMenu", + path: "/claim_batch", + title: "claim_batch.menu.claim_batch", + subtitle: "core.routes.payment.batch.subtitle", + }, + { + parent: "claim.mainMenu", + path: "/claim/healthFacilities", + title: "claim.menu.healthFacilityClaims", + subtitle: "core.routes.payment.requests.subtitle", + }, + { + parent: "claim.mainMenu", + path: "/claim/invoiceApproval", + title: "core.routes.payment.invoices.title", + subtitle: "core.routes.payment.invoices.subtitle", + }, +]; + +const category = [ + { + parent: "admin.productMenu", + title: "admin.menu.products", + path: "/admin/products", + subtitle: "core.routes.category.products.subtitle", + }, + { + parent: "admin.productMenu", + title: "core.routes.category.contributions.title", + path: "/contributionPlans", + subtitle: "core.routes.category.contributions.subtitle", + }, + { + parent: "admin.productMenu", + title: "core.routes.category.contributionBundles.title", + path: "/contributionPlanBundles", + subtitle: "core.routes.category.contributionBundles.subtitle", + }, + { + parent: "admin.productMenu", + path: "/front/admin/products", + title: "core.routes.category.adminProducts.title", + subtitle: "core.routes.category.adminProducts.subtitle", + }, + { + parent: "admin.productMenu", + path: "/admin/declarations-audit", + title: "core.routes.category.declarationsAudit.title", + subtitle: "core.routes.category.declarationsAudit.subtitle", + }, +]; + +const fosa = [ + { + parent: "admin.fosaMenu", + title: "admin.menu.healthFacilities", + path: "/location/healthFacilities", + subtitle: "core.routes.fosa.healthFacilities.subtitle", + }, + { + parent: "admin.fosaMenu", + title: "admin.menu.medicalServicesPrices", + path: "/medical/pricelists/services", + subtitle: "core.routes.fosa.medicalServicesPrices.subtitle", + }, + { + parent: "admin.fosaMenu", + title: "admin.menu.medicalItemsPrices", + path: "/medical/pricelists/items", + subtitle: "core.routes.fosa.medicalItemsPrices.subtitle", + }, + { + parent: "admin.fosaMenu", + title: "admin.menu.medicalServices", + path: "/medical/medicalServices", + subtitle: "core.routes.fosa.medicalServices.subtitle", + }, + { + parent: "admin.fosaMenu", + title: "admin.menu.medicalItems", + path: "/medical/medicalItems", + subtitle: "core.routes.fosa.medicalItems.subtitle", + }, + { + parent: "Fosa", + title: "Gestion des Actes de Soins", + path: "/healthServiceManagement", + }, + { + parent: "Fosa", + title: "Verification", + path: "/insuree/insurees/verifyinsuree", + subtitle: "core.routes.fosa.verification.subtitle", + }, + { + parent: "admin.fosaMenu", + title: "core.routes.fosa.userFunctionalities.title", + path: "/fosa/user-functionalities", + subtitle: "core.routes.fosa.userFunctionalities.subtitle", + }, + { + parent: "admin.fosaMenu", + title: "core.routes.fosa.userSpecialties.title", + path: "/fosa/user-specialties", + subtitle: "core.routes.fosa.userSpecialties.subtitle", + }, + { + parent: "admin.fosaMenu", + title: "admin.menu.mainDiagnoses", + path: "/medical/medicalPathologies", + subtitle: "core.routes.fosa.pathologies.subtitle", + }, + { + parent: "admin.fosaMenu", + title: "core.routes.fosa.pathologiesBundle.title", + path: "/medical/medicalPathologiesBundle", + subtitle: "core.routes.fosa.pathologiesBundle.subtitle", + }, + { + parent: "admin.fosaMenu", + title: "admin.menu.conventionnements", + path: "/location/conventionnements", + subtitle: "core.routes.fosa.conventionnements.subtitle", + }, + { + parent: "admin.fosaMenu", + path: "/claim/preauthorization", + title: "core.routes.fosa.preauthorization.title", + subtitle: "core.routes.fosa.preauthorization.subtitle", + }, + { + parent: "admin.fosaMenu", + path: "/claim/preauthorization/approval", + title: "core.routes.fosa.preauthorizationApproval.title", + subtitle: "core.routes.fosa.preauthorizationApproval.subtitle", + }, +]; + +const location = [ + { + parent: "core.routes.location.mainMenu", + title: "core.routes.location.title", + path: "/location", + subtitle: "core.routes.location.subtitle", + }, +]; + +const outils = [ + { + parent: "tools.mainMenu", + path: "/tools/registers", + title: "tools.menu.registers", + subtitle: "core.routes.tools.registers.subtitle", + }, + { + parent: "tools.mainMenu", + path: "/tools/extracts", + title: "tools.menu.extracts", + subtitle: "core.routes.tools.extracts.subtitle", + }, + { + parent: "tools.mainMenu", + path: "/tools/reports", + title: "core.routes.tools.reports.title", + subtitle: "core.routes.tools.reports.subtitle", + }, + { + parent: "tools.mainMenu", + path: "/tools/manualSync", + title: "core.routes.tools.manualSync.title", + subtitle: "core.routes.tools.manualSync.subtitle", + }, +]; + +const profile = [ + { + parent: "profile.mainMenu", + path: "/profile", + title: "core.routes.profile.main.title", + subtitle: "core.routes.profile.main.subtitle", + }, + { + parent: "profile.mainMenu", + path: "/profile/myProfile", + title: "profile.menu.myProfile", + subtitle: "core.routes.profile.myProfile.subtitle", + }, + { + parent: "profile.mainMenu", + path: "/profile/changePassword", + title: "profile.menu.changePassword", + subtitle: "core.routes.profile.changePassword.subtitle", + }, +]; + +const souscripteursBase = [ + { + parent: "core.routes.policyholder.mainMenu", + path: "/souscripteurs", + title: "core.routes.policyholder.title", + subtitle: "core.routes.policyholder.subtitle", + }, +]; + +const souscripteurs = [ + { + parent: "policyHolder.mainmenu", + path: "/contribution/contributions", + title: "policy.menu.contributions", + subtitle: "core.routes.policyholder.contributions.subtitle", + }, + { + parent: "policyHolder.mainmenu", + path: "/policyHolders", + title: "core.routes.policyholder.registration.title", + subtitle: "core.routes.policyholder.registration.subtitle", + }, + { + parent: "policyHolder.mainmenu", + path: "/payment/payments", + title: "payment.menu.payments", + subtitle: "core.routes.policyholder.recoveries.subtitle", + }, + { + parent: "policyHolder.mainmenu", + path: "/paymentApproval", + title: "policyHolder.menu.paymentForApproval", + subtitle: "core.routes.policyholder.paymentApproval.subtitle", + }, + { + parent: "policyHolder.mainmenu", + path: "/contracts", + title: "core.routes.policyholder.declaration.title", + subtitle: "core.routes.policyholder.declaration.subtitle", + }, + { + parent: "policyHolder.mainmenu", + path: "/declaration", + title: "core.routes.policyholder.declarationReport.title", + subtitle: "core.routes.policyholder.declarationReport.subtitle", + }, + { + parent: "policyHolder.mainmenu", + path: "/policyholderRequest", + title: "core.routes.policyholder.newRequests.title", + subtitle: "core.routes.policyholder.newRequests.subtitle", + }, +]; + +const juridique = [ + { + parent: "core.routes.legal.mainMenu", + path: "/invoices", + title: "core.routes.legal.invoices.title", + subtitle: "core.routes.legal.invoices.subtitle", + }, + { + parent: "core.routes.legal.mainMenu", + path: "/bills", + title: "core.routes.legal.bills.title", + subtitle: "core.routes.legal.bills.subtitle", + }, + { + parent: "core.routes.legal.mainMenu", + path: "/paymentPlans", + title: "core.routes.legal.paymentPlans.title", + subtitle: "core.routes.legal.paymentPlans.subtitle", + }, + { + parent: "core.routes.legal.mainMenu", + path: "/payment/paymentpenalty", + title: "core.routes.legal.penalty.title", + subtitle: "core.routes.legal.penalty.subtitle", + }, +]; + +const exceptions = [ + { + parent: "core.routes.exceptions.mainMenu", + path: "/exception", + title: "core.routes.exceptions.insuree.title", + subtitle: "core.routes.exceptions.insuree.subtitle", + }, + { + parent: "core.routes.exceptions.mainMenu", + path: "/exception/policyholder", + title: "core.routes.exceptions.policyholder.title", + subtitle: "core.routes.exceptions.policyholder.subtitle", + }, + { + parent: "core.routes.exceptions.mainMenu", + path: "/exception/pendingapproval", + title: "core.routes.exceptions.pendingApproval.title", + subtitle: "core.routes.exceptions.pendingApproval.subtitle", + }, +]; + +export const routePages = [ + { + path: "/home", + title: "core.routes.home.title", + parent: "core.routes.home.parent", + subtitle: "core.routes.home.subtitle", + }, + ...affiliation, + ...demandePayement, + ...administration, + ...category, + ...fosa, + ...location, + ...outils, + ...profile, + ...souscripteursBase, + ...souscripteurs, + ...juridique, + ...exceptions, +]; diff --git a/src/components/hooks/usePageTitle.js b/src/components/hooks/usePageTitle.js new file mode 100644 index 00000000..15bae634 --- /dev/null +++ b/src/components/hooks/usePageTitle.js @@ -0,0 +1,104 @@ +import React from "react"; +import { useIntl } from "react-intl"; +import { useLocation } from "react-router-dom"; +import { routePages } from "../hooks/routes"; + +const compareRoutesByLength = (firstRoute, secondRoute) => { + const firstRoutePath = firstRoute.path || ""; + const secondRoutePath = secondRoute.path || ""; + return secondRoutePath.length - firstRoutePath.length; +}; + +const isExactMatch = (routePath, currentPathname) => { + return routePath === currentPathname; +}; + +const isParameterizedMatch = (routePath, currentPathname) => { + const pathPattern = routePath.replace(/:[^/]+/g, "[^/]+"); + const regex = new RegExp(`^${pathPattern}(/.*)?$`); + return regex.test(currentPathname); +}; + +const isPrefixMatch = (routePath, currentPathname, allRoutes) => { + if (!currentPathname.startsWith(routePath) || routePath === "/") { + return false; + } + + const hasMoreSpecificMatch = allRoutes.some((otherRoute) => { + const otherRoutePath = otherRoute.path; + if (!otherRoutePath || otherRoutePath === routePath) { + return false; + } + + if (otherRoutePath.length > routePath.length && currentPathname.startsWith(otherRoutePath)) { + const nextChar = currentPathname[otherRoutePath.length]; + if (nextChar === "/" || nextChar === undefined) { + return true; + } + } + return false; + }); + + if (hasMoreSpecificMatch) { + return false; + } + + const nextChar = currentPathname[routePath.length]; + return nextChar === "/" || nextChar === undefined; +}; + +const doesRouteMatch = (route, currentPathname, allRoutes) => { + const routePath = route.path; + if (!routePath) { + return false; + } + + return ( + isExactMatch(routePath, currentPathname) || + isParameterizedMatch(routePath, currentPathname) || + isPrefixMatch(routePath, currentPathname, allRoutes) + ); +}; + +const findMatchingRoute = (routes, currentPathname) => { + const sortedRoutes = [...routes].sort(compareRoutesByLength); + return sortedRoutes.find((route) => doesRouteMatch(route, currentPathname, sortedRoutes)) || null; +}; + +function UsePageTitle() { + const location = useLocation(); + const { pathname } = location; + const intl = useIntl(); + const [page, setPage] = React.useState({ path: pathname }); + + React.useEffect(() => { + const matchedRoute = findMatchingRoute(routePages, pathname); + + if (matchedRoute) { + const translateKey = (key) => { + if (!key) return key; + try { + const translated = intl.formatMessage({ id: key }); + return translated !== key ? translated : key; + } catch { + return key; + } + }; + + const translatedRoute = { + ...matchedRoute, + path: pathname, + parent: translateKey(matchedRoute.parent), + title: translateKey(matchedRoute.title), + subtitle: translateKey(matchedRoute.subtitle), + }; + setPage((previousPage) => translatedRoute); + } else { + setPage({ path: pathname }); + } + }, [pathname, intl]); + + return page; +} + +export default UsePageTitle; diff --git a/src/components/inputs/AutoSuggestion.js b/src/components/inputs/AutoSuggestion.js index 8fb30beb..c9b15769 100644 --- a/src/components/inputs/AutoSuggestion.js +++ b/src/components/inputs/AutoSuggestion.js @@ -16,7 +16,9 @@ const styles = (theme) => ({ }, header: theme.table.title, label: { - color: theme.palette.primary.main, + // color: theme.palette.primary.main, + color: theme.palette.text.primary, + }, textField: { width: "100%", @@ -250,7 +252,7 @@ class AutoSuggestion extends Component { }; renderSelect = () => { - const { module, withNull, nullLabel, label, required = false, getSuggestionValue } = this.props; + const { module, withNull, nullLabel, label, required = false, getSuggestionValue, readOnly } = this.props; const { suggestions, selected } = this.state; var options = suggestions.map((r) => ({ value: r, label: getSuggestionValue(r) })); if (withNull) { @@ -269,7 +271,7 @@ class AutoSuggestion extends Component { }; renderAutoselect = () => { - const { classes, label, disabled = false, required = false, placeholder, getSuggestionValue } = this.props; + const { classes, label, disabled = false, required = false, placeholder, getSuggestionValue, readOnly } = this.props; const { suggestions, value } = this.state; const inputProps = { className: classes.suggestionInputField, @@ -280,6 +282,7 @@ class AutoSuggestion extends Component { disabled, onChange: this._onAutoselectChange, required, + }; return ( ); }; @@ -307,7 +315,17 @@ class AutoSuggestion extends Component { const { classes, label, readOnly = false, selectThreshold = null } = this.props; const { value, suggestions } = this.state; if (!!readOnly) { - return ; + return ; } if ( !value && diff --git a/src/components/inputs/Autocomplete.js b/src/components/inputs/Autocomplete.js index 49d6e776..540a0e58 100644 --- a/src/components/inputs/Autocomplete.js +++ b/src/components/inputs/Autocomplete.js @@ -8,7 +8,9 @@ import { useModulesManager } from "../../helpers/modules"; const styles = (theme) => ({ label: { - color: theme.palette.primary.main, + // color: theme.palette.primary.main, + color: theme.palette.text.primary, + }, }); diff --git a/src/components/inputs/NumberInput.js b/src/components/inputs/NumberInput.js index f687e907..4ac8f9a4 100644 --- a/src/components/inputs/NumberInput.js +++ b/src/components/inputs/NumberInput.js @@ -1,7 +1,7 @@ import React, { Component } from "react"; import TextInput from "./TextInput"; import { injectIntl } from "react-intl"; -import {formatMessage, formatMessageWithValues} from "../../helpers/i18n"; +import { formatMessage, formatMessageWithValues } from "../../helpers/i18n"; class NumberInput extends Component { constructor(props) { @@ -10,27 +10,68 @@ class NumberInput extends Component { isEdited: false, }; } + + formatNumberMask = (input) => { + try { + // Remove all non-digit characters + const digits = input.toString().replace(/\D/g, ""); + + // Group digits from the end in sets of 3 + const reversed = digits.split("").reverse(); + const grouped = []; + + for (let i = 0; i < reversed.length; i += 3) { + grouped.push( + reversed + .slice(i, i + 3) + .reverse() + .join(""), + ); + } + + // Reverse back and join with spaces + return grouped.reverse().join(" "); + } catch (error) { + return input; + } + }; formatInput = (v, displayZero, displayNa, decimal) => { - if (!v && displayNa && !this.state.isEdited) return formatMessage(this.props.intl, this.props.module, "core.NumberInput.notApplicable"); - if (v === 0 && displayZero) return '0'; - if (v == 0 && !displayZero) return ''; + if (!v && displayNa && !this.state.isEdited) + return formatMessage(this.props.intl, this.props.module, "core.NumberInput.notApplicable"); + if (v === 0 && displayZero) return "0"; + if (v == 0 && !displayZero) return ""; if (decimal && !isNaN(Number(v))) { - if (typeof v === 'string' && v.includes('.') && v.split('.')[1].length > 2) return parseFloat(v).toFixed(2); + if (typeof v === "string" && v.includes(".") && v.split(".")[1].length > 2) return parseFloat(v).toFixed(2); else return v; } if (!v || isNaN(v)) return ""; + if (this.props.useMask) { + return this.formatNumberMask(v); + } return parseFloat(v); }; handleNaBlur = () => { - if ((isNaN(this.props.value) || this.props.value === '') && this.state.isEdited) { + if ((isNaN(this.props.value) || this.props.value === "") && this.state.isEdited) { this.props.onChange(undefined); } this.setState({ isEdited: false }); }; render() { - const { intl, module = "core", min = null, max = null, value, error, displayZero = false, displayNa = false, decimal = false, ...others } = this.props; - let inputProps = { ...this.props.inputProps, type: "tel" }; // We use "tel" instead of "number" to hide up/down arrows + const { + intl, + module = "core", + min = null, + max = null, + value, + error, + displayZero = false, + displayNa = false, + decimal = false, + isNumber, + ...others + } = this.props; + let inputProps = { ...this.props.inputProps, type: "number" }; let err = error; if (min !== null) { @@ -57,9 +98,10 @@ class NumberInput extends Component { value={value} error={err} inputProps={inputProps} - formatInput={(v) =>this.formatInput(v, displayZero, displayNa, decimal)} - onFocus={() => this.setState({isEdited: true})} + formatInput={(v) => this.formatInput(v, displayZero, displayNa, decimal)} + onFocus={() => this.setState({ isEdited: true })} onBlur={() => this.handleNaBlur()} + isNumber={isNumber} /> ); } diff --git a/src/components/inputs/SelectInput.js b/src/components/inputs/SelectInput.js index 1784d9a1..e13eca64 100644 --- a/src/components/inputs/SelectInput.js +++ b/src/components/inputs/SelectInput.js @@ -8,7 +8,7 @@ import _ from "lodash-uuid"; const styles = (theme) => ({ label: { - color: theme.palette.primary.main, + color: theme.palette.text.primary, }, }); diff --git a/src/components/inputs/TextInput.js b/src/components/inputs/TextInput.js index fc62a55f..6cc2fb75 100644 --- a/src/components/inputs/TextInput.js +++ b/src/components/inputs/TextInput.js @@ -1,12 +1,26 @@ import React, { Component } from "react"; import { withTheme, withStyles } from "@material-ui/core/styles"; import { injectIntl } from "react-intl"; -import { TextField } from "@material-ui/core"; +import { TextField, InputAdornment } from "@material-ui/core"; import { formatMessage } from "../../helpers/i18n"; - +import { AttachMoney } from "@material-ui/icons"; const styles = (theme) => ({ label: { - color: theme.palette.primary.main, + color: theme.palette.text.primary, + }, + // NOTE: This is used to hide the increment/decrement arrows from the number input + numberInput: { + "& input[type=number]": { + "-moz-appearance": "textfield", + }, + "& input[type=number]::-webkit-outer-spin-button": { + "-webkit-appearance": "none", + margin: 0, + }, + "& input[type=number]::-webkit-inner-spin-button": { + "-webkit-appearance": "none", + margin: 0, + }, }, }); @@ -35,10 +49,13 @@ class TextInput extends Component { } } _onChange = (e) => { - let {value} = e.target; + let { value } = e.target; if (this.props.formatInput) { value = this.props.formatInput(value); } + if (this.props.CapitalThreeLetterLimit) { + value = value.slice(0, 3).toUpperCase(); // Restrict to 3 letters and capitalize them + } if (value !== this.state.value) { this.setState({ value }, () => this.props.onChange && this.props.onChange(this.state.value)); } @@ -57,19 +74,48 @@ class TextInput extends Component { formatInput = null, helperText, type, + capitalize, + isNumber, ...others } = this.props; + const inputClass = readOnly ? classes.disabledInput : ""; + let transformedValue = this.state.value; + + // Capitalize the value if the capitalize prop is true + // if (capitalize && transformedValue) { + // transformedValue = transformedValue.toUpperCase(); + // } return ( + {isNumber && XAF} + + ), + inputProps: { + style: { + textTransform: !!capitalize && "capitalize", + }, + }, }} - InputProps={{ inputProps, startAdornment, endAdornment }} onChange={this._onChange} + // value={transformedValue} value={this.state.value} error={Boolean(error)} helperText={error ?? helperText} diff --git a/src/helpers/ErrorBoundary.js b/src/helpers/ErrorBoundary.js index eb7d0f44..6700fdf1 100644 --- a/src/helpers/ErrorBoundary.js +++ b/src/helpers/ErrorBoundary.js @@ -1,4 +1,6 @@ import React from "react"; +import { formatMessage } from "./i18n"; +import { injectIntl } from "react-intl"; class ErrorBoundary extends React.Component { constructor(props) { @@ -17,10 +19,11 @@ class ErrorBoundary extends React.Component { } render() { if (this.state.hasError) { - return

An error was not properly caught. Refer to console log.

; + return

{formatMessage(this.props.intl, "core", "common.errorBoundary")}

; + // return

An error was not properly caught. Refer to console log.

; } return this.props.children; } } -export default ErrorBoundary; +export default injectIntl(ErrorBoundary); diff --git a/src/helpers/hooks.js b/src/helpers/hooks.js index 5f9f3a50..de320672 100644 --- a/src/helpers/hooks.js +++ b/src/helpers/hooks.js @@ -1,7 +1,15 @@ import { useModulesManager } from "@openimis/fe-core"; import { useState, useEffect, useRef, useCallback } from "react"; import { useSelector, useDispatch } from "react-redux"; -import { refreshAuthToken, login, logout, initialize, graphqlWithVariables, graphqlMutation } from "../actions"; +import { + refreshAuthToken, + login, + logout, + initialize, + graphqlWithVariables, + graphqlMutation, + graphqlMutation2, +} from "../actions"; export const useDebounceCb = (cb, duration = 0) => { const [payload, setPayload] = useState(); @@ -95,7 +103,7 @@ export const useGraphqlMutation = (operation, config) => { const dispatch = useDispatch(); const [state, setState] = useState({ isLoading: false, error: null }); - function mutate(input) { + function mutate(input, usePlainObject = false) { if (state.isLoading) { console.warn("A mutation is already in progress"); return; @@ -103,9 +111,11 @@ export const useGraphqlMutation = (operation, config) => { setState({ isLoading: true, error: null }); return new Promise(async (resolve, reject) => { try { - const variables = { - input, - }; + const variables = usePlainObject + ? input + : { + input, + }; const result = await dispatch( graphqlMutation(operation, variables, config.type, { operation, input }, config.wait), ); @@ -144,6 +154,48 @@ export const useGraphqlMutation = (operation, config) => { }; }; +export const useGraphqlMutation2 = (operation, config) => { + config = { ...DEFAULT_GRAPHQL_MUTATION_CONFIG, ...config }; + const dispatch = useDispatch(); + const [state, setState] = useState({ isLoading: false, error: null }); + + const mutate = async (input, usePlainObject = false) => { + if (state.isLoading) { + console.warn("A mutation is already in progress"); + return; + } + + setState({ isLoading: true, error: null }); + + try { + const variables = usePlainObject ? input : { input }; + + const result = await dispatch(graphqlMutation2(operation, variables, config.type, { operation, input }, false)); + + const error = result?.error?.map((err) => err.detail).join("; "); + + if (error) { + throw new Error(error); + } + + setState({ isLoading: false, error: null }); + + return result; + } catch (err) { + setState({ isLoading: false, error: err }); + + // throw err; + return err; + } + }; + + return { + isLoading: state.isLoading, + error: state.error, + mutate, + }; +}; + export const useAuthentication = () => { const dispatch = useDispatch(); const user = useSelector((state) => state.core.user); diff --git a/src/helpers/hooks/useGedHealthCheck.js b/src/helpers/hooks/useGedHealthCheck.js new file mode 100644 index 00000000..7b5c6642 --- /dev/null +++ b/src/helpers/hooks/useGedHealthCheck.js @@ -0,0 +1,59 @@ +import { baseApiUrl, apiHeaders, Contributions } from "@openimis/fe-core"; +import { useCallback, useEffect, useRef, useState } from "react"; + +const API_URL = `${baseApiUrl}/insuree/ged-health-check/`; +export const useGedHealthCheck = () => { + const [healthStatus, setHealthStatus] = useState({ + isGedDown: false, + isChecking: false, + }); + + useEffect(() => { + const fetchHealthStatus = async () => { + try { + setHealthStatus((prev) => ({ ...prev, isChecking: true })); + + const response = await fetch(API_URL, { + headers: apiHeaders, + method: "GET", + credentials: "same-origin", + }); + + if (response.status != 200) { + throw new Error(`Failed to fetch GED health status: ${response.statusText}`); + } + + const payload = await response.json(); + + setHealthStatus((prev) => ({ + ...prev, + isChecking: false, + isGedDown: payload.status !== 200, + })); + + sessionStorage.setItem("gedHealthStatus", payload.status === 200 ? "UP" : "DOWN"); + } catch (err) { + console.error("Error fetching GED health status:", err); + setHealthStatus((prev) => ({ + ...prev, + isChecking: false, + isGedDown: true, + })); + } + }; + + const storedStatus = sessionStorage.getItem("gedHealthStatus") || false; + + if (!storedStatus) { + fetchHealthStatus(); + } else { + setHealthStatus((prev) => ({ + ...prev, + isChecking: false, + isGedDown: storedStatus !== "UP", + })); + } + }, []); + + return { ...healthStatus }; +}; diff --git a/src/helpers/i18n.js b/src/helpers/i18n.js index 01b77765..1bbc2d54 100644 --- a/src/helpers/i18n.js +++ b/src/helpers/i18n.js @@ -39,7 +39,7 @@ export function toISODate(d) { } export function withTooltip(c, t) { - return !!t ? {c} : c; + return !!t ? {c} : c; } export function useTranslations(moduleName, modulesManager) { diff --git a/src/index.js b/src/index.js index 84495292..3f0515bc 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ import App from "./components/App"; import React from "react"; import messages_en from "./translations/en.json"; +import messages_fr from "./translations/fr.json"; import KeepLegacyAlive from "./components/KeepLegacyAlive"; import AutoSuggestion from "./components/inputs/AutoSuggestion"; import Autocomplete from "./components/inputs/Autocomplete"; @@ -50,13 +51,16 @@ import { apiHeaders, graphql, graphqlMutation, + graphqlMutationLegacy, graphqlWithVariables, + waitForMutation, journalize, coreAlert, coreConfirm, fetchMutation, prepareMutation, clearCurrentPaginationPage, + login, } from "./actions"; import { formatMessage, @@ -85,11 +89,9 @@ import { sort, formatSorter, formatGQLString, - formatNodeQuery + formatNodeQuery, } from "./helpers/api"; -import { - downloadExport -} from "./helpers/downloadExport" +import { downloadExport } from "./helpers/downloadExport"; import { useDebounceCb, usePrevious, @@ -114,11 +116,16 @@ import { formatJsonField } from "./helpers/jsonExt"; import { RIGHT_ROLE_SEARCH } from "./constants"; import { authMiddleware } from "./middlewares"; import RefreshAuthToken from "./components/RefreshAuthToken"; +import CommonSnackbar from "./components/generics/CommonSnakbar"; +import AdTimePicker from "./pickers/AdTimePicker"; const ROUTE_ROLES = "roles"; const ROUTE_ROLE = "roles/role"; const DEFAULT_CONFIG = { - "translations": [{ key: "en", messages: messages_en }], + "translations": [ + { key: "en", messages: messages_en }, + { key: "fr", messages: messages_fr }, + ], "reducers": [{ key: "core", reducer: reducer }], "middlewares": [authMiddleware], "refs": [ @@ -128,20 +135,21 @@ const DEFAULT_CONFIG = { { key: "core.MonthPicker", ref: MonthPicker }, { key: "core.LanguagePicker", ref: LanguagePicker }, { key: "core.route.role", ref: ROUTE_ROLE }, + { key: "core.AdTimePicker", ref: AdTimePicker }, ], "core.Boot": [KeepLegacyAlive, RefreshAuthToken], "core.Router": [ { path: ROUTE_ROLES, component: Roles }, { path: ROUTE_ROLE + "/:role_uuid?", component: Role }, ], - "admin.MainMenu": [ - { - text: , - icon: , - route: "/" + ROUTE_ROLES, - filter: (rights) => rights.includes(RIGHT_ROLE_SEARCH), - }, - ], + // "admin.MainMenu": [ + // { + // text: , + // icon: , + // route: "/" + ROUTE_ROLES, + // filter: (rights) => rights.includes(RIGHT_ROLE_SEARCH), + // }, + // ], }; export const CoreModule = (cfg) => { @@ -169,6 +177,8 @@ export { graphql, graphqlWithVariables, graphqlMutation, + graphqlMutationLegacy, + waitForMutation, journalize, fetchMutation, prepareMutation, @@ -257,4 +267,6 @@ export { ConfirmDialog, useAuthentication, useBoolean, + CommonSnackbar, + login, }; diff --git a/src/pages/ForgotPasswordPage.js b/src/pages/ForgotPasswordPage.js index f07be3ae..fb6b669c 100644 --- a/src/pages/ForgotPasswordPage.js +++ b/src/pages/ForgotPasswordPage.js @@ -6,19 +6,23 @@ import TextInput from "../components/inputs/TextInput"; import { useTranslations } from "../helpers/i18n"; import { useModulesManager } from "../helpers/modules"; import Helmet from "../helpers/Helmet"; -import { useGraphqlMutation } from "../helpers/hooks"; +import { useGraphqlMutation, useGraphqlMutation2 } from "../helpers/hooks"; +import ArrowBackIcon from "@material-ui/icons/ArrowBack"; +import { useHistory } from "react-router-dom"; const useStyles = makeStyles((theme) => ({ container: { position: "absolute", - top: "30%", + top: 0, + bottom: 0, left: 0, right: 0, margin: "auto", display: "flex", justifyContent: "center", + alignItems: "center", }, - paper: theme.paper.paper, + paper: theme.paper.loginPaper, logo: { maxHeight: 100, }, @@ -28,15 +32,16 @@ const ForgotPasswordPage = (props) => { const classes = useStyles(); const modulesManager = useModulesManager(); const { formatMessage } = useTranslations("core.ForgotPasswordPage", modulesManager); - const [username, setUsername] = useState(); + const [email, setEmail] = useState(); const [isDone, setDone] = useState(false); - const { isLoading, mutate } = useGraphqlMutation( + const [error, setError] = useState(null); + const history = useHistory(); + const { isLoading, mutate } = useGraphqlMutation2( ` - mutation resetPassword($input: ResetPasswordMutationInput!) { - resetPassword(input: $input) { - clientMutationId + mutation newPasswordRequest($email: String!, $fromWhatEnv: String!) { + newPasswordRequest(email: $email, fromWhatEnv: $fromWhatEnv) { success - error + message } } `, @@ -47,55 +52,143 @@ const ForgotPasswordPage = (props) => { const onSubmit = async (e) => { e.preventDefault(); - await mutate({ username }); - await setDone(true); + const res = await mutate({ email, fromWhatEnv: "imis" }, true); + console.log("res", res); + if (res?.newPasswordRequest?.success) { + await setDone(true); + } else { + setError(res?.newPasswordRequest?.message || "An error occurred, be sure to use a valid email"); + } + }; + + const handleBackToLogin = () => { + // Use the history object to navigate to the LoginPage route + history.push("/login"); }; return ( <> -
- - -
- - {!isDone && ( - - -

{formatMessage("recoverTitle")}

-
- - {formatMessage("explanationMessage")} - - - {formatMessage("contactAdministrator")} - - - setUsername(username)} - /> - - - + {formatMessage("recoverTitle")} +
+ + + setEmail(email)} + /> + + + {error && ( + + {error} + + )} + + + + + +
-
- )} + )} + + {isDone && ( + //

{formatMessage("done")}

- {isDone &&

{formatMessage("done")}

} - - - + + + + +
+ {formatMessage("done.Verification")} +
+
+ {formatMessage("done.Administrator")} +
+ + + +
+ )} + + + +
); diff --git a/src/pages/LoginPage.js b/src/pages/LoginPage.js index b5ab8d7d..2747af37 100644 --- a/src/pages/LoginPage.js +++ b/src/pages/LoginPage.js @@ -1,13 +1,27 @@ import React, { useMemo, useState, useEffect } from "react"; import { useHistory } from "../helpers/history"; import { makeStyles } from "@material-ui/styles"; -import { Button, Box, Grid, Paper, LinearProgress } from "@material-ui/core"; +import { + Button, + Box, + Grid, + Paper, + LinearProgress, + Checkbox, + FormControlLabel, + IconButton, + InputAdornment, + TextField, +} from "@material-ui/core"; +import Alert from "@material-ui/lab/Alert"; import TextInput from "../components/inputs/TextInput"; import { useTranslations } from "../helpers/i18n"; import { useModulesManager } from "../helpers/modules"; import Helmet from "../helpers/Helmet"; import { useAuthentication } from "../helpers/hooks"; import Contributions from "./../components/generics/Contributions"; +import Visibility from "@material-ui/icons/Visibility"; +import VisibilityOff from "@material-ui/icons/VisibilityOff"; const useStyles = makeStyles((theme) => ({ container: { @@ -21,16 +35,19 @@ const useStyles = makeStyles((theme) => ({ justifyContent: "center", alignItems: "center", }, - paper: theme.paper.paper, + paper: theme.paper.loginPaper, logo: { maxHeight: 100, width: 100, }, + inputAdornment: { + backgroundColor: "#0000", // Adjust based on your theme + }, })); const LOGIN_PAGE_CONTRIBUTION_KEY = "core.LoginPage"; -const LoginPage = ({ logo }) => { +const LoginPage = ({ logo, backgroundImage }) => { const classes = useStyles(); const history = useHistory(); const modulesManager = useModulesManager(); @@ -39,18 +56,70 @@ const LoginPage = ({ logo }) => { const [hasError, setError] = useState(false); const auth = useAuthentication(); const [isAuthenticating, setAuthenticating] = useState(false); + const initialState = localStorage.getItem("rememberMe") === "true"; + const [rememberMe, setRememberMe] = useState(initialState); + const [showPassword, setShowPassword] = useState(false); + const handleClickShowPassword = () => setShowPassword(!showPassword); + const handleMouseDownPassword = () => setShowPassword(!showPassword); + const handleRememberMeChange = (event) => { + const isChecked = event.target.checked; + setRememberMe(isChecked); + + console.log("Remember Me is checked:", isChecked); + + if (!isChecked) { + // If "Remember Me" is unchecked, clear the saved credentials. + localStorage.removeItem("rememberedUsername"); + localStorage.removeItem("rememberedPassword"); + } else { + // If "Remember Me" is checked, save the username and password. + localStorage.setItem("rememberedUsername", credentials.username); + localStorage.setItem("rememberedPassword", credentials.password); + + console.log("Saved username and password:", credentials.username, credentials.password); + } + + localStorage.setItem("rememberMe", isChecked.toString()); + }; useEffect(() => { if (auth.isAuthenticated) { history.push("/"); + } else { + // If "Remember Me" is checked, retrieve and set the saved credentials. + if (rememberMe) { + const savedUsername = localStorage.getItem("rememberedUsername"); + const savedPassword = localStorage.getItem("rememberedPassword"); + if (savedUsername && savedPassword) { + setCredentials({ username: savedUsername, password: savedPassword }); + } + } } }, []); + console.log("==> cred", credentials) + const onSubmit = async (e) => { e.preventDefault(); setError(false); setAuthenticating(true); + + if (!rememberMe) { + console.log("Don't remember") + // If "Remember Me" is unchecked, clear the saved credentials. + localStorage.removeItem("rememberedUsername"); + localStorage.removeItem("rememberedPassword"); + } else { + console.log("Remember") + // If "Remember Me" is checked, save the username and password. + localStorage.setItem("rememberedUsername", credentials.username); + localStorage.setItem("rememberedPassword", credentials.password); + + console.log("Saved username and password:", credentials.username, credentials.password); + } + if (await auth.login(credentials)) { + sessionStorage.removeItem("session_expired"); history.push("/"); } else { setError(true); @@ -62,7 +131,6 @@ const LoginPage = ({ logo }) => { e.preventDefault(); history.push("/forgot_password"); }; - return ( <> {isAuthenticating && ( @@ -70,62 +138,156 @@ const LoginPage = ({ logo }) => { )} -
- - -
- - - - - - {formatMessage("appName")} - - - - setCredentials({ ...credentials, username })} - /> - - - setCredentials({ ...credentials, password })} - /> - - {hasError && ( - - {formatMessage("authError")} +
+
+
+ + + + + + + + + {/* + {formatMessage("core.displayAppName")} + */} + + {/* + "Insurance Management System" + */} +
+ CAMU IMS +
+ +
+ {sessionStorage.getItem("session_expired") && ( + Votre session a expiré. Veuillez vous reconnecter. + )} +
+ + + setCredentials({ ...credentials, username })} + autoComplete={rememberMe ? "username" : "off"} + /> + + + setCredentials({ ...credentials, password: event.target.value })} + autoComplete={rememberMe ? "current-password" : "off"} + InputProps={{ + endAdornment: ( + + + {showPassword ? : } + + + ), + }} + /> + + {hasError && ( + + {formatMessage("authError")} + + )} + + + + } + /> + +
+ {formatMessage("forgotPassword")} +
+ +
+ + +
- )} - - - - - - - - -
- -
+ + + +
+
); diff --git a/src/pages/Roles.js b/src/pages/Roles.js index a8eca93b..370f048a 100644 --- a/src/pages/Roles.js +++ b/src/pages/Roles.js @@ -122,6 +122,7 @@ class RawRoleFilter extends Component { label={formatMessage(intl, "core", "roleManagement.showHistory")} control={ this._onChangeFilter("showHistory", event.target.checked)} /> @@ -189,12 +190,12 @@ class Roles extends Component { language === null ? role.name : language === LANGUAGE_EN - ? role.name - : role.altLanguage === null - ? role.name - : role.altLanguage, - (role) => (role.isSystem !== null ? : ""), - (role) => (role.isBlocked !== null ? : ""), + ? role.name + : role.altLanguage === null + ? role.name + : role.altLanguage, + (role) => (role.isSystem !== null ? : ""), + (role) => (role.isBlocked !== null ? : ""), (role) => (!!role.validityFrom ? formatDateFromISO(modulesManager, intl, role.validityFrom) : ""), (role) => (!!role.validityTo ? formatDateFromISO(modulesManager, intl, role.validityTo) : ""), ]; diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js index abf47b14..f4606d73 100644 --- a/src/pages/SetPasswordPage.js +++ b/src/pages/SetPasswordPage.js @@ -12,7 +12,7 @@ import { useGraphqlMutation } from "../helpers/hooks"; const useStyles = makeStyles((theme) => ({ container: { position: "absolute", - top: "30%", + top: "20%", left: 0, right: 0, margin: "auto", @@ -23,9 +23,12 @@ const useStyles = makeStyles((theme) => ({ logo: { maxHeight: 100, }, + setBox: { + boxShadow: '0px 0px 0px #eee' + } })); -const SetPasswordPage = () => { +const SetPasswordPage = (props) => { const classes = useStyles(); const history = useHistory(); const modulesManager = useModulesManager(); @@ -78,12 +81,32 @@ const SetPasswordPage = () => { return ( <> +
+
- +
- + + + + +
{formatMessage("pageTitle")}
{
+
); }; diff --git a/src/pages/VerifyUserAndUpdatePasswordPage.js b/src/pages/VerifyUserAndUpdatePasswordPage.js new file mode 100644 index 00000000..93a802ca --- /dev/null +++ b/src/pages/VerifyUserAndUpdatePasswordPage.js @@ -0,0 +1,176 @@ +import React, { useState, useMemo, useEffect } from "react"; + +import { makeStyles } from "@material-ui/styles"; +import { Button, Box, Grid, Paper } from "@material-ui/core"; +import TextInput from "../components/inputs/TextInput"; +import { useTranslations } from "../helpers/i18n"; +import { useModulesManager } from "../helpers/modules"; +import { useHistory } from "../helpers/history"; +import Helmet from "../helpers/Helmet"; +import { useGraphqlMutation2 } from "../helpers/hooks"; + +const useStyles = makeStyles((theme) => ({ + container: { + position: "absolute", + top: "20%", + left: 0, + right: 0, + margin: "auto", + display: "flex", + justifyContent: "center", + }, + paper: theme.paper.paper, + logo: { + maxHeight: 100, + }, + setBox: { + boxShadow: "0px 0px 0px #eee", + }, +})); + +const VerifyUserAndUpdatePasswordPage = (props) => { + const classes = useStyles(); + const history = useHistory(); + const modulesManager = useModulesManager(); + const { formatMessage } = useTranslations("core.SetPasswordPage", modulesManager); + const [credentials, setCredentials] = useState({}); + const [error, setError] = useState(); + + const { mutate } = useGraphqlMutation2( + ` + mutation verifyUserAndUpdatePassword($password: String!, $token: String!, $userId: String!) { + verifyUserAndUpdatePassword(password: $password, token: $token, userId: $userId) { + success + message + } + } + `, + { wait: false }, + ); + + useEffect(() => { + const search = new URLSearchParams(location.search); + setCredentials({ + token: search.get("token"), + username: search.get("username"), + userId: search.get("user_id"), + }); + }, []); + + const onSubmit = async (e) => { + e.preventDefault(); + if (isValid) { + const result = await mutate( + { + password: credentials.password, + username: credentials.username, + token: credentials.token, + userId: credentials.userId, + }, + true, + ); + if (result?.verifyUserAndUpdatePassword.success) { + history.push("/"); + } else { + setError(result?.verifyUserAndUpdatePassword.error || formatMessage("error")); + } + } + }; + + const isValid = useMemo( + () => + credentials.confirmPassword && + credentials.password && + credentials.password === credentials.confirmPassword && + credentials.username && + credentials.token, + [credentials], + ); + + return ( + <> +
+
+
+ + +
+ + + + + +
+ {formatMessage("pageTitle")} +
+ + + + + setCredentials({ ...credentials, password })} + /> + + + setCredentials({ ...credentials, confirmPassword })} + /> + + {error && ( + + {error} + + )} + + + + +
+
+
+
+
+
+ + ); +}; + +export default VerifyUserAndUpdatePasswordPage; diff --git a/src/pickers/AdDatePicker.js b/src/pickers/AdDatePicker.js index 45e685f8..d8d1bfb8 100644 --- a/src/pickers/AdDatePicker.js +++ b/src/pickers/AdDatePicker.js @@ -2,13 +2,16 @@ import React, { Component } from "react"; import moment from "moment"; import { withTheme, withStyles } from "@material-ui/core/styles"; import { injectIntl } from "react-intl"; -import { FormControl } from "@material-ui/core"; +import { FormControl, TextField } from "@material-ui/core"; import { DatePicker as MUIDatePicker } from "@material-ui/pickers"; import { formatMessage, toISODate } from "../helpers/i18n"; +import MomentUtils from "@date-io/moment"; +import { MuiPickersUtilsProvider } from "@material-ui/pickers"; +import "moment/locale/fr"; const styles = (theme) => ({ label: { - color: theme.palette.primary.main, + color: theme.palette.text.primary, }, }); @@ -24,14 +27,14 @@ class AdDatePicker extends Component { this.setState((state, props) => ({ value: props.value || null })); } - componentDidUpdate(prevState, prevProps, snapshot) { - if (prevState.value !== this.props.value) { - this.setState((state, props) => ({ value: fromISODate(props.value) })); + componentDidUpdate(prevProps) { + if (prevProps.value !== this.props.value) { + this.setState({ value: fromISODate(this.props.value) }); } } dateChange = (d) => { - this.setState({ value: d }, (i) => this.props.onChange(toISODate(d))); + this.setState({ value: d }, () => this.props.onChange(toISODate(d))); }; render() { @@ -44,31 +47,59 @@ class AdDatePicker extends Component { readOnly = false, required = false, fullWidth = true, - format = "YYYY-MM-DD", + format = "DD-MM-YYYY", reset, + monthtrue, + daytrue, ...otherProps } = this.props; + let userlang = localStorage.getItem("userLanguage"); + let locale = userlang === "fr" ? "fr" : "en"; + moment.locale(locale); + + const displayFormat = daytrue ? 'DD' : format; return ( - + + ( + + )} + /> + ); } } -export default injectIntl(withTheme(withStyles(styles)(AdDatePicker))); +export default injectIntl(withTheme(withStyles(styles)(AdDatePicker))); \ No newline at end of file diff --git a/src/pickers/AdTimePicker.js b/src/pickers/AdTimePicker.js new file mode 100644 index 00000000..79246fab --- /dev/null +++ b/src/pickers/AdTimePicker.js @@ -0,0 +1,129 @@ +import React, { Component } from "react"; +import moment from "moment"; +import { withTheme, withStyles } from "@material-ui/core/styles"; +import { injectIntl } from "react-intl"; +import { FormControl, TextField } from "@material-ui/core"; +import { TimePicker as MUITimePicker } from "@material-ui/pickers"; +import { formatMessage } from "../helpers/i18n"; +import MomentUtils from "@date-io/moment"; +import { MuiPickersUtilsProvider } from "@material-ui/pickers"; +import "moment/locale/fr"; + +const styles = (theme) => ({ + label: { + color: theme.palette.text.primary, + }, +}); + +function fromISODate(timeStr) { + if (!timeStr) return null; + + // Log what is being passed to moment + console.log("Converting time string:", timeStr); + + // Ensure the time string is in "HH:mm:ss" format + const time = moment(timeStr, "HH:mm:ss"); + + // Check if the date is valid + if (!time.isValid()) { + console.error("Invalid time format:", timeStr); + return null; + } + + return time.toDate(); // Convert the moment object to a JavaScript Date object + } + + +class AdTimePicker extends Component { + state = { value: null }; + + componentDidMount() { + if (this.props.value) { + console.log("Initial value:", this.props.value); + this.setState({ value: fromISODate(this.props.value) }); + } + } + + componentDidUpdate(prevProps) { + if (prevProps.value !== this.props.value) { + console.log("Previous value:", prevProps.value); + console.log("Current value received:", this.props.value); + + if (this.props.value) { + const parsedDate = fromISODate(this.props.value); + if (parsedDate) { + this.setState({ value: parsedDate }); + } else { + console.error("Failed to parse time:", this.props.value); + } + } + } + } + + + dateChange = (d) => { + if (!d) { + this.setState({ value: null }, () => this.props.onChange(null)); + return; + } + + // Format the date as "HH:mm:ss" and update state + const formattedTime = moment(d).format('HH:mm:ss'); + this.setState({ value: d }, () => this.props.onChange(formattedTime)); + }; + + + render() { + const { + intl, + classes, + readOnly = false, + required = false, + fullWidth = true, + label, + ...otherProps + } = this.props; + + let userlang = localStorage.getItem("userLanguage"); + let locale = userlang === "fr" ? "fr" : "en"; + moment.locale(locale); + console.log("value",this.state.value) + return ( + + + ( + + )} + /> + + + ); + } + } + + +export default injectIntl(withTheme(withStyles(styles)(AdTimePicker))); diff --git a/src/reducer.js b/src/reducer.js index 0652844b..9ce6ef17 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -43,6 +43,12 @@ function reducer( afterCursor: null, beforeCursor: null, module: null, + assigned: null, + fetchingNotificationList: false, + fetchedNotificationList: false, + notificationList: null, + notificationListTotalCount: null, + errorNotificationList: null, }, action, ) { @@ -309,10 +315,23 @@ function reducer( }, }, }; + case "CORE_NOTIFICATION_LIST_RESP": + return { + ...state, + fetchingNotificationList: false, + fetchedNotificationList: true, + notificationList: parseData(action.payload.data.camuNotifications), + notificationListTotalCount: !!action.payload.data.camuNotifications + ? action.payload.data.camuNotifications.totalCount + : null, + errorNotificationList: formatGraphQLError(action.payload), + }; case "CORE_ROLE_MUTATION_REQ": return dispatchMutationReq(state, action); case "CORE_ROLE_MUTATION_ERR": return dispatchMutationErr(state, action); + case "CORE_CREATE_NOTIFICATION_RESP": + return dispatchMutationResp(state, "markNotificationAsRead", action); case "CORE_CREATE_ROLE_RESP": return dispatchMutationResp(state, "createRole", action); case "CORE_UPDATE_ROLE_RESP": @@ -358,6 +377,21 @@ function reducer( role: null, roleRights: [], }; + case "CHECK_ASSIGNED_PROFILE_REQ": + return { + ...state, + assigned: null, + }; + case "CHECK_ASSIGNED_PROFILE_RES": + return { + ...state, + assigned: action.payload, + }; + case "CHECK_ASSIGNED_PROFILE_ERR": + return { + ...state, + assigned: null, + }; case "CORE_PAGINATION_PAGE": return { ...state, diff --git a/src/translations/en.json b/src/translations/en.json index 756a2b38..25dca1f4 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -12,6 +12,7 @@ "core.roleManagement.null": "Any", "core.roleManagement.true": "True", "core.roleManagement.false": "False", + "core.displayAppName": "CAMU", "core.roleManagement.createButton.tooltip": "Create new Role", "core.roleManagement.role.page.title": "Role Details {label}", "core.roleManagement.requiredFieldsEmptyError": "* These fields are required", @@ -36,29 +37,153 @@ "core.Autocomplete.openText": "Open", "core.Autocomplete.closeText": "Close", "core.Autocomplete.placeholder": "Search...", - "core.LoginPage.username.label": "Username", - "core.LoginPage.password.label": "Password", - "core.LoginPage.loginBtn": "Log In", - "core.LoginPage.authError": "The password or the username you've entered is incorrect.", + "core.LoginPage.username.label": "Nom d'utilisateur", + "core.LoginPage.password.label": "Mot de passe", + "core.LoginPage.rememberMe": "Se souvenir de moi", + "core.LoginPage.loginBtn": "Connexion", + "core.LoginPage.authError": "Le mot de passe ou le nom d'utilisateur que vous avez saisi est incorrect", "core.LoginPage.pageTitle": "Log In", - "core.LoginPage.forgotPassword": "Forgot Password ?", - "core.ForgotPasswordPage.pageTitle": "Forgot Password ?", - "core.ForgotPasswordPage.submitBtn": "Submit", - "core.ForgotPasswordPage.username.label": "Username", - "core.ForgotPasswordPage.recoverTitle": "Recover your account", - "core.ForgotPasswordPage.explanationMessage": "Enter your username to be able to recover your account. En e-mail with the instructions will be sent to your e-mail address.", - "core.ForgotPasswordPage.contactAdministrator": "If you do not receive an e-mail, please check with your administrator.", + "core.LoginPage.forgotPassword": "Mot de passe oublié ?", + "core.ForgotPasswordPage.pageTitle": "Mot de passe oublié ?", + "core.ForgotPasswordPage.submitBtn": "Soumettre", + "core.ForgotPasswordPage.username.label": "Nom d'utilisateur", "core.ForgotPasswordPage.done": "Done ! Check your inbox and click on the verification link to reset your password.", - "core.SetPasswordPage.pageTitle": "Set a new Password", - "core.SetPasswordPage.username.label": "Username", - "core.SetPasswordPage.password.label": "Password", - "core.SetPasswordPage.confirmPassword.label": "Confirm Password", + "core.ForgotPasswordPage.done.Verification": "Un e-mail contenant le lien de vérification a été envoyé à votre adresse e-mail", + "core.ForgotPasswordPage.done.Administrator": "Si vous ne recevez pas d'e-mail, veuillez contacter votre administrateur", + "core.ForgotPasswordPage.backButton": "Retour à la connexion", + "core.SetPasswordPage.pageTitle": "Définir un nouveau mot de passe", + "core.SetPasswordPage.username.label": "Nom d'utilisateur", + "core.SetPasswordPage.password.label": "Mot de passe", + "core.SetPasswordPage.confirmPassword.label": "Confirmer le mot de passe", "core.SetPasswordPage.error": "Unknown error", - "core.SetPasswordPage.submitBtn": "Submit", + "core.SetPasswordPage.submitBtn": "Soumettre", "core.table.resultsLoading": "Loading...", "core.exportSearchResult": "Export search result", "core.exportSearchResult.tooltip": "Export result", "LanguagePicker.label": "Language", "core.Language.null": "", - "core.NumberInput.notApplicable": "N/A" + "core.NumberInput.notApplicable": "N/A", + "core.ForgotPasswordPage.recoverTitle": "Récupérer votre compte", + "core.ForgotPasswordPage.explanationMessage": "Saisissez votre nom d'utilisateur pour pouvoir récupérer votre compte. Un e-mail contenant les instructions sera envoyé à votre adresse e-mail.", + "core.ForgotPasswordPage.contactAdministrator": "Si vous ne recevez pas d'e-mail, veuillez contacter votre administrateur.", + "core.bank.label": "Bank", + "core.datePicker.ok": "OK", + "core.datePicker.cancel": "CANCEL", + "core.datePicker.clear": "CLEAR", + "core.common.errorBoundary": "An error was not properly caught. Refer to console log.", + "core.common.Copied": "Copied!", + "core.common.Copy": "Copy", + "core.title": "Notifications", + "core.clearAll": "Clear All", + "core.emptyNotification": "No Notification", + "core.routes.home.parent": "CAMU IMS", + "core.routes.home.title": "Home", + "core.routes.home.subtitle": "", + "core.routes.affiliation.families.subtitle": "Manage and view insured families", + "core.routes.affiliation.insurees.subtitle": "Manage and view individual insureds", + "core.routes.affiliation.policies.subtitle": "Manage insurance policies", + "core.routes.affiliation.assignment.title": "User assignment", + "core.routes.affiliation.assignment.subtitle": "Assign users to pending approvals", + "core.routes.administration.users.subtitle": "Manage system users", + "core.routes.administration.bank.title": "Bank", + "core.routes.administration.bank.subtitle": "Manage banking information", + "core.routes.administration.fosaCategories.title": "FOSA Categories", + "core.routes.administration.fosaCategories.subtitle": "Configure health facility categories", + "core.routes.administration.operations.title": "Operation Management", + "core.routes.administration.operations.subtitle": "Manage system operations", + "core.routes.administration.medicalServices.title": "Medical services category management", + "core.routes.administration.medicalServices.subtitle": "Manage medical service categories", + "core.routes.administration.locations.subtitle": "Manage geographic locations", + "core.routes.administration.centers.title": "Centers", + "core.routes.administration.centers.subtitle": "Manage geographic centers", + "core.routes.administration.roles.title": "Role Management", + "core.routes.administration.roles.subtitle": "Manage roles and permissions", + "core.routes.administration.policyholderAdmins.title": "Policyholder Administrators", + "core.routes.administration.policyholderAdmins.subtitle": "Manage policyholder administrators", + "core.routes.administration.dashboard.title": "Dashboard", + "core.routes.administration.dashboard.subtitle": "Administrative dashboard", + "core.routes.administration.fosaUsers.title": "FOSA Users", + "core.routes.administration.fosaUsers.subtitle": "Manage health facility users", + "core.routes.administration.policyholderUsers.title": "Policyholder Users", + "core.routes.administration.policyholderUsers.subtitle": "Manage policyholder users", + "core.routes.administration.fosaUserFunctionalities.title": "FOSA User Functionalities", + "core.routes.administration.fosaUserFunctionalities.subtitle": "Manage FOSA user functionalities", + "core.routes.administration.fosaUserSpecialties.title": "FOSA User Specialties", + "core.routes.administration.fosaUserSpecialties.subtitle": "Manage FOSA user specialties", + "core.routes.payment.reviews.subtitle": "Control and approve invoices for payment", + "core.routes.payment.batch.subtitle": "Evaluate payment requests by batch", + "core.routes.payment.requests.subtitle": "Manage health facility payment requests", + "core.routes.payment.invoices.title": "Invoices", + "core.routes.payment.invoices.subtitle": "Approve and manage invoices", + "core.routes.category.products.subtitle": "Manage insured categories", + "core.routes.category.contributions.title": "Contributions", + "core.routes.category.contributions.subtitle": "Manage contribution plans", + "core.routes.category.contributionBundles.title": "Contribution rates/bundles", + "core.routes.category.contributionBundles.subtitle": "Manage contribution rates and bundles", + "core.routes.category.adminProducts.title": "Products", + "core.routes.category.adminProducts.subtitle": "Manage administrative products", + "core.routes.category.declarationsAudit.title": "Declarations Audit", + "core.routes.category.declarationsAudit.subtitle": "View declarations audit", + "core.routes.fosa.healthFacilities.subtitle": "Manage health facilities", + "core.routes.fosa.medicalServicesPrices.subtitle": "Manage care packages for medical acts", + "core.routes.fosa.medicalItemsPrices.subtitle": "Manage care packages for medical products", + "core.routes.fosa.medicalServices.subtitle": "Manage available medical acts", + "core.routes.fosa.medicalItems.subtitle": "Manage available medical products", + "core.routes.fosa.verification.title": "Verification", + "core.routes.fosa.verification.subtitle": "Verify insured eligibility", + "core.routes.fosa.userFunctionalities.title": "User Functionalities", + "core.routes.fosa.userFunctionalities.subtitle": "Manage FOSA user functionalities", + "core.routes.fosa.userSpecialties.title": "User Specialties", + "core.routes.fosa.userSpecialties.subtitle": "Manage FOSA user specialties", + "core.routes.fosa.pathologies.subtitle": "Manage medical pathologies", + "core.routes.fosa.pathologiesBundle.title": "Pathology Bundles", + "core.routes.fosa.pathologiesBundle.subtitle": "Manage medical pathology bundles", + "core.routes.fosa.conventionnements.subtitle": "Manage health facility agreements", + "core.routes.fosa.preauthorization.title": "Pre-authorization", + "core.routes.fosa.preauthorization.subtitle": "Manage pre-authorization requests", + "core.routes.fosa.preauthorizationApproval.title": "Pre-authorization Approval", + "core.routes.fosa.preauthorizationApproval.subtitle": "Approve pre-authorization requests", + "core.routes.location.mainMenu": "Location", + "core.routes.location.title": "Location", + "core.routes.location.subtitle": "Manage locations", + "core.routes.tools.registers.subtitle": "View and manage registers", + "core.routes.tools.extracts.subtitle": "Generate and view extracts", + "core.routes.tools.reports.title": "Reports", + "core.routes.tools.reports.subtitle": "Generate and view reports", + "core.routes.tools.manualSync.title": "Manual Synchronization", + "core.routes.tools.manualSync.subtitle": "Perform manual synchronizations", + "core.routes.profile.main.title": "Profile", + "core.routes.profile.main.subtitle": "Manage your user profile", + "core.routes.profile.myProfile.subtitle": "View and modify your personal information", + "core.routes.profile.changePassword.subtitle": "Change your password", + "core.routes.policyholder.mainMenu": "Policyholders", + "core.routes.policyholder.title": "Policyholders", + "core.routes.policyholder.subtitle": "Manage policyholders", + "core.routes.policyholder.contributions.subtitle": "Manage policyholder contributions", + "core.routes.policyholder.registration.title": "Registration", + "core.routes.policyholder.registration.subtitle": "Manage policyholder registration", + "core.routes.policyholder.recoveries.subtitle": "Manage payment recoveries", + "core.routes.policyholder.paymentApproval.subtitle": "Approve policyholder payments", + "core.routes.policyholder.declaration.title": "Declaration", + "core.routes.policyholder.declaration.subtitle": "Manage contract declarations", + "core.routes.policyholder.declarationReport.title": "Declaration Report", + "core.routes.policyholder.declarationReport.subtitle": "View declaration reports", + "core.routes.policyholder.newRequests.title": "New Policyholder Requests", + "core.routes.policyholder.newRequests.subtitle": "Manage new policyholder requests", + "core.routes.legal.mainMenu": "Legal and Financial", + "core.routes.legal.invoices.title": "Invoices", + "core.routes.legal.invoices.subtitle": "Manage system invoices", + "core.routes.legal.bills.title": "Bills", + "core.routes.legal.bills.subtitle": "View and manage bills", + "core.routes.legal.paymentPlans.title": "Payment Plans", + "core.routes.legal.paymentPlans.subtitle": "Manage payment plans", + "core.routes.legal.penalty.title": "Penalty and Sanction", + "core.routes.legal.penalty.subtitle": "Manage payment penalties and sanctions", + "core.routes.exceptions.mainMenu": "Exceptions", + "core.routes.exceptions.insuree.title": "Exception for Insureds", + "core.routes.exceptions.insuree.subtitle": "Manage exceptions for insureds", + "core.routes.exceptions.policyholder.title": "Exception for Policyholders", + "core.routes.exceptions.policyholder.subtitle": "Manage exceptions for policyholders", + "core.routes.exceptions.pendingApproval.title": "Pending Approval", + "core.routes.exceptions.pendingApproval.subtitle": "View exceptions pending approval" } diff --git a/src/translations/fr.json b/src/translations/fr.json new file mode 100644 index 00000000..b710767e --- /dev/null +++ b/src/translations/fr.json @@ -0,0 +1,127 @@ +{ + "core.ForgotPasswordPage.backButton": "Retour à la connexion", + "core.SetPasswordPage.pageTitle": "Définir un nouveau mot de passe", + "core.SetPasswordPage.username.label": "Nom d'utilisateur", + "core.SetPasswordPage.password.label": "Mot de passe", + "core.SetPasswordPage.confirmPassword.label": "Confirmer le mot de passe", + "core.SetPasswordPage.error": "Unknown error", + "core.SetPasswordPage.submitBtn": "Soumettre", + "core.ForgotPasswordPage.pageTitle": "Mot de passe oublié ?", + "core.ForgotPasswordPage.submitBtn": "Soumettre", + "core.ForgotPasswordPage.username.label": "Nom d'utilisateur", + "core.ForgotPasswordPage.done": "Done ! Check your inbox and click on the verification link to reset your password.", + "core.ForgotPasswordPage.done.Verification": "Un e-mail contenant le lien de vérification a été envoyé à votre adresse e-mail", + "core.ForgotPasswordPage.done.Administrator": "Si vous ne recevez pas d'e-mail, veuillez contacter votre administrateur", + "core.ForgotPasswordPage.recoverTitle": "Récupérer votre compte", + "core.ForgotPasswordPage.explanationMessage": "Saisissez votre nom d'utilisateur pour pouvoir récupérer votre compte. Un e-mail contenant les instructions sera envoyé à votre adresse e-mail.", + "core.routes.home.parent": "CAMU IMS", + "core.routes.home.title": "Accueil", + "core.routes.home.subtitle": "", + "core.routes.affiliation.families.subtitle": "Gérer et consulter les familles d'assurés", + "core.routes.affiliation.insurees.subtitle": "Gérer et consulter les assurés individuels", + "core.routes.affiliation.policies.subtitle": "Gérer les polices d'assurance", + "core.routes.affiliation.assignment.title": "Affectation d'un utilisateur", + "core.routes.affiliation.assignment.subtitle": "Affecter des utilisateurs aux approbations en attente", + "core.routes.administration.users.subtitle": "Gérer les utilisateurs du système", + "core.routes.administration.bank.title": "Bank", + "core.routes.administration.bank.subtitle": "Gérer les informations bancaires", + "core.routes.administration.fosaCategories.title": "Catégories des Fosa", + "core.routes.administration.fosaCategories.subtitle": "Configurer les catégories des formations sanitaires", + "core.routes.administration.operations.title": "Opération Management", + "core.routes.administration.operations.subtitle": "Gérer les opérations du système", + "core.routes.administration.medicalServices.title": "Medical services category management", + "core.routes.administration.medicalServices.subtitle": "Gérer les catégories de services médicaux", + "core.routes.administration.locations.subtitle": "Gérer les localisations géographiques", + "core.routes.administration.centers.title": "Centres", + "core.routes.administration.centers.subtitle": "Gérer les centres géographiques", + "core.routes.administration.roles.title": "Gestion des roles", + "core.routes.administration.roles.subtitle": "Gérer les rôles et permissions", + "core.routes.administration.policyholderAdmins.title": "Administrateurs souscripteurs", + "core.routes.administration.policyholderAdmins.subtitle": "Gérer les administrateurs des souscripteurs", + "core.routes.administration.dashboard.title": "Dashboard", + "core.routes.administration.dashboard.subtitle": "Tableau de bord administratif", + "core.routes.administration.fosaUsers.title": "Utilisateurs Fosa", + "core.routes.administration.fosaUsers.subtitle": "Gérer les utilisateurs des formations sanitaires", + "core.routes.administration.policyholderUsers.title": "Utilisateurs souscripteurs", + "core.routes.administration.policyholderUsers.subtitle": "Gérer les utilisateurs des souscripteurs", + "core.routes.administration.fosaUserFunctionalities.title": "Fonctionnalités utilisateur FOSA", + "core.routes.administration.fosaUserFunctionalities.subtitle": "Gérer les fonctionnalités des utilisateurs FOSA", + "core.routes.administration.fosaUserSpecialties.title": "Spécialités utilisateur FOSA", + "core.routes.administration.fosaUserSpecialties.subtitle": "Gérer les spécialités des utilisateurs FOSA", + "core.routes.payment.reviews.subtitle": "Contrôler et approuver les factures pour paiement", + "core.routes.payment.batch.subtitle": "Évaluer les demandes de paiement par lots", + "core.routes.payment.requests.subtitle": "Gérer les demandes de paiement des formations sanitaires", + "core.routes.payment.invoices.title": "Invoices", + "core.routes.payment.invoices.subtitle": "Approuver et gérer les factures", + "core.routes.category.products.subtitle": "Gérer les catégories d'assurés", + "core.routes.category.contributions.title": "Cotisations", + "core.routes.category.contributions.subtitle": "Gérer les plans de cotisation", + "core.routes.category.contributionBundles.title": "Taux/forfait cotisations", + "core.routes.category.contributionBundles.subtitle": "Gérer les taux et forfaits de cotisation", + "core.routes.category.adminProducts.title": "Produits", + "core.routes.category.adminProducts.subtitle": "Gérer les produits administratifs", + "core.routes.category.declarationsAudit.title": "Audit des déclarations", + "core.routes.category.declarationsAudit.subtitle": "Consulter l'audit des déclarations", + "core.routes.fosa.healthFacilities.subtitle": "Gérer les formations sanitaires", + "core.routes.fosa.medicalServicesPrices.subtitle": "Gérer les paquets de soins pour les actes médicaux", + "core.routes.fosa.medicalItemsPrices.subtitle": "Gérer les paquets de soins pour les produits médicaux", + "core.routes.fosa.medicalServices.subtitle": "Gérer les actes médicaux disponibles", + "core.routes.fosa.medicalItems.subtitle": "Gérer les produits médicaux disponibles", + "core.routes.fosa.verification.title": "Verification", + "core.routes.fosa.verification.subtitle": "Vérifier l'éligibilité des assurés", + "core.routes.fosa.userFunctionalities.title": "Fonctionnalités utilisateur", + "core.routes.fosa.userFunctionalities.subtitle": "Gérer les fonctionnalités des utilisateurs FOSA", + "core.routes.fosa.userSpecialties.title": "Spécialités utilisateur", + "core.routes.fosa.userSpecialties.subtitle": "Gérer les spécialités des utilisateurs FOSA", + "core.routes.fosa.pathologies.subtitle": "Gérer les pathologies médicales", + "core.routes.fosa.pathologiesBundle.title": "Paquets de pathologies", + "core.routes.fosa.pathologiesBundle.subtitle": "Gérer les paquets de pathologies médicales", + "core.routes.fosa.conventionnements.subtitle": "Gérer les conventionnements des formations sanitaires", + "core.routes.fosa.preauthorization.title": "Pré-autorisation", + "core.routes.fosa.preauthorization.subtitle": "Gérer les demandes de pré-autorisation", + "core.routes.fosa.preauthorizationApproval.title": "Approbation pré-autorisation", + "core.routes.fosa.preauthorizationApproval.subtitle": "Approuver les demandes de pré-autorisation", + "core.routes.location.mainMenu": "Localisation", + "core.routes.location.title": "Localisation", + "core.routes.location.subtitle": "Gérer les localisations", + "core.routes.tools.registers.subtitle": "Consulter et gérer les registres", + "core.routes.tools.extracts.subtitle": "Générer et consulter les extraits", + "core.routes.tools.reports.title": "Rapports", + "core.routes.tools.reports.subtitle": "Générer et consulter les rapports", + "core.routes.tools.manualSync.title": "Manuels Synchronisation", + "core.routes.tools.manualSync.subtitle": "Effectuer des synchronisations manuelles", + "core.routes.profile.main.title": "Profil", + "core.routes.profile.main.subtitle": "Gérer votre profil utilisateur", + "core.routes.profile.myProfile.subtitle": "Consulter et modifier vos informations personnelles", + "core.routes.profile.changePassword.subtitle": "Modifier votre mot de passe", + "core.routes.policyholder.mainMenu": "Souscripteurs", + "core.routes.policyholder.title": "Souscripteurs", + "core.routes.policyholder.subtitle": "Gérer les souscripteurs", + "core.routes.policyholder.contributions.subtitle": "Gérer les contributions des souscripteurs", + "core.routes.policyholder.registration.title": "Immatriculation", + "core.routes.policyholder.registration.subtitle": "Gérer l'immatriculation des souscripteurs", + "core.routes.policyholder.recoveries.subtitle": "Gérer les recouvrements de paiement", + "core.routes.policyholder.paymentApproval.subtitle": "Approuver les paiements des souscripteurs", + "core.routes.policyholder.declaration.title": "Déclaration", + "core.routes.policyholder.declaration.subtitle": "Gérer les déclarations de contrat", + "core.routes.policyholder.declarationReport.title": "Rapport de déclaration", + "core.routes.policyholder.declarationReport.subtitle": "Consulter les rapports de déclaration", + "core.routes.policyholder.newRequests.title": "Nouveau souscripteur demandes", + "core.routes.policyholder.newRequests.subtitle": "Gérer les demandes de nouveaux souscripteurs", + "core.routes.legal.mainMenu": "Juridique et financier", + "core.routes.legal.invoices.title": "Factures", + "core.routes.legal.invoices.subtitle": "Gérer les factures du système", + "core.routes.legal.bills.title": "Factures", + "core.routes.legal.bills.subtitle": "Consulter et gérer les factures", + "core.routes.legal.paymentPlans.title": "Plans de paiement", + "core.routes.legal.paymentPlans.subtitle": "Gérer les plans de paiement", + "core.routes.legal.penalty.title": "Pénalité de sanction", + "core.routes.legal.penalty.subtitle": "Gérer les pénalités et sanctions de paiement", + "core.routes.exceptions.mainMenu": "Exceptions", + "core.routes.exceptions.insuree.title": "Exception pour les assurés", + "core.routes.exceptions.insuree.subtitle": "Gérer les exceptions pour les assurés", + "core.routes.exceptions.policyholder.title": "Exception pour les souscripteurs", + "core.routes.exceptions.policyholder.subtitle": "Gérer les exceptions pour les souscripteurs", + "core.routes.exceptions.pendingApproval.title": "En attente d'approbation", + "core.routes.exceptions.pendingApproval.subtitle": "Consulter les exceptions en attente d'approbation" +}