diff --git a/client/src/components/alert/alert.component.jsx b/client/src/components/alert/alert.component.jsx
index 439822f68..f57dcb5fd 100644
--- a/client/src/components/alert/alert.component.jsx
+++ b/client/src/components/alert/alert.component.jsx
@@ -1,5 +1,5 @@
import { Alert } from "antd";
-export default function AlertComponent(props) {
- return ;
+export default function AlertComponent({ title, message, ...props }) {
+ return ;
}
diff --git a/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx b/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx
index 3254c9f74..69c5a1831 100644
--- a/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx
+++ b/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx
@@ -4,17 +4,200 @@ import AlertComponent from "../alert/alert.component";
import "./form-fields-changed.styles.scss";
import Prompt from "../../utils/prompt";
-export default function FormsFieldChanged({ form, skipPrompt }) {
+export default function FormsFieldChanged({ form, skipPrompt, onErrorNavigate, onReset, onDirtyChange }) {
const { t } = useTranslation();
+ const normalizeNamePath = (namePath) => (Array.isArray(namePath) ? namePath.filter((part) => part !== undefined) : [namePath]);
+
+ const getFieldIdCandidates = (namePath) => {
+ const normalizedNamePath = normalizeNamePath(namePath).map((part) => String(part));
+ const underscoreId = normalizedNamePath.join("_");
+ const dashId = normalizedNamePath.join("-");
+ const dotName = normalizedNamePath.join(".");
+
+ return [underscoreId, dashId, dotName].filter(Boolean);
+ };
+
+ const clearFormMeta = () => {
+ const fieldMeta = form.getFieldsError().map(({ name }) => ({
+ name,
+ touched: false,
+ validating: false,
+ errors: [],
+ warnings: []
+ }));
+
+ if (fieldMeta.length > 0) {
+ form.setFields(fieldMeta);
+ }
+
+ onDirtyChange?.(false);
+ };
+
const handleReset = () => {
- form.resetFields();
+ if (onReset) {
+ onReset();
+ } else {
+ form.resetFields();
+ }
+
+ window.requestAnimationFrame(() => {
+ clearFormMeta();
+ });
+ };
+
+ const getFieldDomNode = (namePath) => {
+ const fieldInstance = form.getFieldInstance?.(namePath);
+ const fieldIdCandidates = getFieldIdCandidates(namePath);
+ const domCandidates = [
+ fieldInstance?.nativeElement,
+ fieldInstance?.input,
+ fieldInstance?.resizableTextArea?.textArea,
+ fieldInstance
+ ];
+
+ fieldIdCandidates.forEach((fieldId) => {
+ const escapedFieldId = CSS.escape(fieldId);
+ const directNode = document.getElementById(fieldId) || document.querySelector(`#${escapedFieldId}`);
+ const labelNode = document.querySelector(`label[for="${escapedFieldId}"]`);
+ const namedNode = document.querySelector(`[name="${escapedFieldId}"]`);
+ const formItemNode =
+ directNode?.closest?.(".ant-form-item") ||
+ labelNode?.closest?.(".ant-form-item") ||
+ namedNode?.closest?.(".ant-form-item");
+
+ domCandidates.push(directNode);
+ domCandidates.push(namedNode);
+ domCandidates.push(formItemNode);
+ domCandidates.push(formItemNode?.querySelector?.("input, textarea, select, .ant-select-selector"));
+ });
+
+ return domCandidates.find((candidate) => candidate instanceof HTMLElement) ?? null;
+ };
+
+ const waitForAnimationFrames = (frameCount = 1) =>
+ new Promise((resolve) => {
+ let remainingFrames = frameCount;
+ const nextFrame = () => {
+ if (remainingFrames <= 0) {
+ resolve();
+ return;
+ }
+ remainingFrames -= 1;
+ window.requestAnimationFrame(nextFrame);
+ };
+ window.requestAnimationFrame(nextFrame);
+ });
+
+ const getFieldOwningTabMeta = (namePath) => {
+ const fieldDomNode = getFieldDomNode(namePath);
+ const owningTabPane = fieldDomNode?.closest?.(".ant-tabs-tabpane");
+ const paneId = owningTabPane?.getAttribute?.("id") || null;
+ const owningTabButton = paneId
+ ? document.querySelector(`[role="tab"][aria-controls="${paneId.replace(/"/g, '\\"')}"]`)
+ : null;
+ const tabLabel = owningTabButton?.textContent?.trim() || null;
+
+ return {
+ owningTabPane,
+ owningTabButton,
+ tabLabel
+ };
+ };
+
+ const openFieldOwningTab = async (namePath) => {
+ const { owningTabPane, owningTabButton } = getFieldOwningTabMeta(namePath);
+ if (!owningTabPane || owningTabPane.classList.contains("ant-tabs-tabpane-active")) return false;
+
+ if (!(owningTabButton instanceof HTMLElement)) return false;
+
+ owningTabButton.click();
+
+ for (let index = 0; index < 24; index += 1) {
+ await waitForAnimationFrames();
+ if (owningTabPane.classList.contains("ant-tabs-tabpane-active")) return true;
+ }
+
+ return owningTabPane.classList.contains("ant-tabs-tabpane-active");
+ };
+
+ const scrollToErrorField = (namePath) => {
+ const normalizedNamePath = normalizeNamePath(namePath);
+ if (!normalizedNamePath.length) return;
+
+ try {
+ form.scrollToField(normalizedNamePath, {
+ behavior: "smooth",
+ block: "center",
+ focus: true
+ });
+ window.requestAnimationFrame(() => {
+ const fallbackNode = getFieldDomNode(normalizedNamePath);
+ fallbackNode?.focus?.();
+ });
+ return;
+ } catch {
+ const fallbackTarget = document.getElementById(normalizedNamePath[0]?.toString?.() ?? "");
+ fallbackTarget?.scrollIntoView({
+ behavior: "smooth",
+ block: "center"
+ });
+ }
+ };
+
+ const handleErrorClick = async (namePath) => {
+ const normalizedNamePath = normalizeNamePath(namePath);
+ if (!normalizedNamePath.length) return;
+
+ const switchedTab = await openFieldOwningTab(normalizedNamePath);
+ if (!switchedTab) {
+ const navigationDelayMs = onErrorNavigate?.(normalizedNamePath) ?? 0;
+ if (navigationDelayMs > 0) {
+ window.setTimeout(() => {
+ window.requestAnimationFrame(() => {
+ scrollToErrorField(normalizedNamePath);
+ });
+ }, navigationDelayMs);
+ return;
+ }
+ }
+
+ await waitForAnimationFrames(switchedTab ? 2 : 1);
+ scrollToErrorField(normalizedNamePath);
};
//if (!form.isFieldsTouched()) return <>>;
return (
{() => {
- const errors = form.getFieldsError().filter((e) => e.errors.length > 0);
+ const errors = form
+ .getFieldsError()
+ .filter((fieldError) => fieldError.errors.length > 0)
+ .flatMap((fieldError) => {
+ const tabMeta = getFieldOwningTabMeta(fieldError.name);
+
+ return fieldError.errors.map((errorMessage, errorIndex) => ({
+ key: `${(fieldError.name || []).join(".")}-${errorIndex}-${errorMessage}`,
+ message: errorMessage,
+ namePath: fieldError.name,
+ tabLabel: tabMeta.tabLabel
+ }));
+ });
+
+ const groupedErrors = errors.reduce((groups, error) => {
+ const groupKey = error.tabLabel || "__ungrouped__";
+ if (!groups[groupKey]) {
+ groups[groupKey] = {
+ key: groupKey,
+ label: error.tabLabel,
+ errors: []
+ };
+ }
+ groups[groupKey].errors.push(error);
+ return groups;
+ }, {});
+ const errorGroups = Object.values(groupedErrors);
+ const hasTabbedErrorGroups = errorGroups.some((group) => Boolean(group.label));
+
if (form.isFieldsTouched())
return (
@@ -39,10 +222,35 @@ export default function FormsFieldChanged({ form, skipPrompt }) {
{errors.length > 0 && (
- {errors.map((e, idx) => e.errors.map((e2, idx2) => - {e2}
))}
+
+ {errorGroups.map((group) => (
+
+ {hasTabbedErrorGroups && group.label ? (
+
{group.label}
+ ) : null}
+
+ {group.errors.map((error) => (
+ -
+ {Array.isArray(error.namePath) && error.namePath.length > 0 ? (
+
+ ) : (
+ error.message
+ )}
+
+ ))}
+
+
+ ))}
}
showIcon
diff --git a/client/src/components/form-fields-changed-alert/form-fields-changed.styles.scss b/client/src/components/form-fields-changed-alert/form-fields-changed.styles.scss
index 155407907..cb3e3940f 100644
--- a/client/src/components/form-fields-changed-alert/form-fields-changed.styles.scss
+++ b/client/src/components/form-fields-changed-alert/form-fields-changed.styles.scss
@@ -4,4 +4,47 @@
min-height: unset !important;
}
}
+
+ &__error-list {
+ margin: 0;
+ padding-left: 18px;
+ }
+
+ &__error-groups {
+ display: grid;
+ gap: 10px;
+ }
+
+ &__error-group {
+ display: grid;
+ gap: 4px;
+ }
+
+ &__error-group-title {
+ font-weight: 600;
+ }
+
+ &__error-link {
+ display: inline;
+ padding: 0;
+ border: 0;
+ background: none;
+ color: inherit;
+ font: inherit;
+ line-height: inherit;
+ text-align: left;
+ cursor: pointer;
+ text-decoration: underline;
+ text-underline-offset: 2px;
+
+ &:hover {
+ color: color-mix(in srgb, var(--ant-color-error) 82%, var(--ant-color-text));
+ }
+
+ &:focus-visible {
+ outline: 2px solid color-mix(in srgb, var(--ant-color-error) 32%, transparent);
+ outline-offset: 2px;
+ border-radius: 4px;
+ }
+ }
}
diff --git a/client/src/components/form-items-formatted/url-form-item.component.jsx b/client/src/components/form-items-formatted/url-form-item.component.jsx
new file mode 100644
index 000000000..c038442fc
--- /dev/null
+++ b/client/src/components/form-items-formatted/url-form-item.component.jsx
@@ -0,0 +1,34 @@
+import { LinkOutlined } from "@ant-design/icons";
+import { Button, Input, Space } from "antd";
+import { forwardRef, useMemo } from "react";
+
+const HAS_URL_PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
+const LOCALHOST_OR_IP_REGEX = /^(localhost|127(?:\.\d{1,3}){3}|\d{1,3}(?:\.\d{1,3}){3})(:\d+)?(\/.*)?$/i;
+
+const getUrlActionHref = (value) => {
+ const trimmedValue = String(value ?? "").trim();
+ if (!trimmedValue) return null;
+
+ if (HAS_URL_PROTOCOL_REGEX.test(trimmedValue)) return trimmedValue;
+ if (trimmedValue.startsWith("//")) return `https:${trimmedValue}`;
+ if (LOCALHOST_OR_IP_REGEX.test(trimmedValue)) return `http://${trimmedValue}`;
+
+ return `https://${trimmedValue}`;
+};
+
+const FormItemUrl = forwardRef(function FormItemUrl({ value, defaultValue, ...props }, ref) {
+ const urlActionHref = useMemo(() => getUrlActionHref(value ?? defaultValue), [defaultValue, value]);
+
+ return (
+
+
+ {urlActionHref ? (
+ } href={urlActionHref} target="_blank" rel="noopener noreferrer" />
+ ) : (
+ } disabled />
+ )}
+
+ );
+});
+
+export default FormItemUrl;
diff --git a/client/src/components/layout-form-row/config-list-actions.utils.jsx b/client/src/components/layout-form-row/config-list-actions.utils.jsx
new file mode 100644
index 000000000..73bec841e
--- /dev/null
+++ b/client/src/components/layout-form-row/config-list-actions.utils.jsx
@@ -0,0 +1,17 @@
+import { Button } from "antd";
+import ConfigListEmptyState from "./config-list-empty-state.component.jsx";
+
+export const buildConfigListActionButton = ({ key, label, onClick, id }) => (
+
+);
+
+export const renderConfigListOrEmpty = ({ fields, actionLabel, renderItems }) =>
+ fields.length === 0 ? : renderItems();
+
+export const buildSectionActionButton = (key, label, onClick, id) =>
+ buildConfigListActionButton({ key, label, onClick, id });
+
+export const renderListOrEmpty = (fields, actionLabel, renderItems) =>
+ renderConfigListOrEmpty({ fields, actionLabel, renderItems });
diff --git a/client/src/components/layout-form-row/inline-validated-form-row.component.jsx b/client/src/components/layout-form-row/inline-validated-form-row.component.jsx
new file mode 100644
index 000000000..70910856b
--- /dev/null
+++ b/client/src/components/layout-form-row/inline-validated-form-row.component.jsx
@@ -0,0 +1,47 @@
+import { Form } from "antd";
+import LayoutFormRow from "./layout-form-row.component";
+
+export default function InlineValidatedFormRow({ actions, errorNames = [], extraErrors = [], form, ...layoutFormRowProps }) {
+ const normalizedErrorNames = Array.isArray(errorNames) ? errorNames : [errorNames];
+ const normalizedExtraErrors = Array.isArray(extraErrors) ? extraErrors.filter(Boolean) : [extraErrors].filter(Boolean);
+
+ return (
+
+ {() => {
+ const fieldErrors = normalizedErrorNames.flatMap((name) => form?.getFieldError?.(name) || []);
+ const errors = [...new Set([...fieldErrors, ...normalizedExtraErrors])];
+ const resolvedClassName = [
+ layoutFormRowProps.className,
+ errors.length > 0 ? "imex-form-row--error" : null
+ ]
+ .filter(Boolean)
+ .join(" ");
+
+ const normalizedActions = Array.isArray(actions) ? actions.filter(Boolean) : [actions].filter(Boolean);
+ const resolvedActions =
+ errors.length > 0
+ ? [
+ 0 ? 8 : 0,
+ width: "100%",
+ textAlign: "left"
+ }}
+ >
+
+ {normalizedActions.length > 0 ?
{normalizedActions}
: null}
+
+ ]
+ : normalizedActions.length > 0
+ ? normalizedActions
+ : undefined;
+
+ return ;
+ }}
+
+ );
+}
diff --git a/client/src/components/layout-form-row/layout-form-row.styles.scss b/client/src/components/layout-form-row/layout-form-row.styles.scss
index 3e8674362..2c95c7973 100644
--- a/client/src/components/layout-form-row/layout-form-row.styles.scss
+++ b/client/src/components/layout-form-row/layout-form-row.styles.scss
@@ -50,11 +50,23 @@ html[data-theme="dark"] {
border-color: var(--imex-form-surface-border);
}
+ &.imex-form-row--error.ant-card {
+ border-color: var(--ant-color-error);
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--ant-color-error) 24%, transparent);
+ }
+
.ant-card-head {
background: var(--imex-form-surface-head);
border-bottom-color: var(--imex-form-surface-border);
}
+ &.imex-form-row--error {
+ .ant-card-head,
+ .ant-card-actions {
+ border-color: color-mix(in srgb, var(--ant-color-error) 34%, var(--imex-form-surface-border));
+ }
+ }
+
&.imex-form-row--compact {
.ant-card-head {
min-height: 40px;
@@ -189,3 +201,13 @@ html[data-theme="dark"] {
font-size: var(--ant-font-size);
line-height: 1.5;
}
+
+.imex-inline-form-row-errors {
+ color: var(--ant-color-error);
+
+ .ant-form-item-explain,
+ .ant-form-item-explain-error,
+ .ant-form-item-additional {
+ color: var(--ant-color-error);
+ }
+}
diff --git a/client/src/components/shop-employees/shop-employees-form.component.jsx b/client/src/components/shop-employees/shop-employees-form.component.jsx
index cd677e6d1..a47e577b3 100644
--- a/client/src/components/shop-employees/shop-employees-form.component.jsx
+++ b/client/src/components/shop-employees/shop-employees-form.component.jsx
@@ -3,9 +3,8 @@ import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
-import { useForm } from "antd/es/form/Form";
import queryString from "query-string";
-import { useEffect } from "react";
+import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
@@ -26,8 +25,10 @@ import { DateFormatter } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";
import AlertComponent from "../alert/alert.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
+import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx";
+import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import {
INLINE_TITLE_GROUP_STYLE,
@@ -49,9 +50,10 @@ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
-export function ShopEmployeesFormComponent({ bodyshop }) {
+export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDirty }) {
const { t } = useTranslation();
- const [form] = useForm();
+ const [internalIsDirty, setInternalIsDirty] = useState(false);
+ const resolvedIsDirty = typeof isDirty === "boolean" ? isDirty : internalIsDirty;
const employeeNumber = Form.useWatch("employee_number", form);
const firstName = Form.useWatch("first_name", form);
const lastName = Form.useWatch("last_name", form);
@@ -66,17 +68,19 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
const history = useNavigate();
const search = queryString.parse(useLocation().search);
const [deleteVacation] = useMutation(DELETE_VACATION);
- const { error, data } = useQuery(QUERY_EMPLOYEE_BY_ID, {
+ const { error, data, refetch } = useQuery(QUERY_EMPLOYEE_BY_ID, {
variables: { id: search.employeeId },
skip: !search.employeeId || search.employeeId === "new",
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
const notification = useNotification();
+ const isNewEmployee = search.employeeId === "new";
+ const currentEmployeeData = data?.employees_by_pk?.id === search.employeeId ? data.employees_by_pk : null;
const employeeTitleName = [firstName, lastName].filter(Boolean).join(" ").trim();
const employeeCardTitle =
[employeeNumber, employeeTitleName].filter(Boolean).join(" - ") ||
- (search.employeeId === "new" ? t("employees.actions.new") : t("bodyshop.labels.employees"));
+ (isNewEmployee ? t("employees.actions.new") : t("bodyshop.labels.employees"));
const {
treatments: { Enhanced_Payroll }
@@ -86,13 +90,49 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
splitKey: bodyshop.imexshopid
});
+ const updateDirtyState = useCallback(
+ (nextDirtyState) => {
+ if (typeof isDirty !== "boolean") {
+ setInternalIsDirty(nextDirtyState);
+ }
+
+ onDirtyChange?.(nextDirtyState);
+ },
+ [isDirty, onDirtyChange]
+ );
+
const client = useApolloClient();
- useEffect(() => {
- if (data && data.employees_by_pk) form.setFieldsValue(data.employees_by_pk);
- else {
- form.resetFields();
+ const clearEmployeeFormMeta = useCallback(() => {
+ const fieldMeta = form.getFieldsError().map(({ name }) => ({
+ name,
+ touched: false,
+ validating: false,
+ errors: [],
+ warnings: []
+ }));
+
+ if (fieldMeta.length > 0) {
+ form.setFields(fieldMeta);
}
- }, [form, data, search.employeeId]);
+
+ updateDirtyState(false);
+ }, [form, updateDirtyState]);
+
+ const resetEmployeeFormToCurrentData = useCallback(() => {
+ form.resetFields();
+
+ if (currentEmployeeData) {
+ form.setFieldsValue(currentEmployeeData);
+ }
+
+ window.requestAnimationFrame(() => {
+ clearEmployeeFormMeta();
+ });
+ }, [clearEmployeeFormMeta, currentEmployeeData, form]);
+
+ useEffect(() => {
+ resetEmployeeFormToCurrentData();
+ }, [resetEmployeeFormToCurrentData, search.employeeId]);
const [updateEmployee] = useMutation(UPDATE_EMPLOYEE);
const [insertEmployees] = useMutation(INSERT_EMPLOYEES);
@@ -112,6 +152,8 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
}
})
.then(() => {
+ updateDirtyState(false);
+ void refetch();
notification.success({
title: t("employees.successes.save")
});
@@ -131,6 +173,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
variables: { employees: [{ ...values, shopid: bodyshop.id }] },
refetchQueries: ["QUERY_EMPLOYEES"]
}).then((r) => {
+ updateDirtyState(false);
search.employeeId = r.data.insert_employees.returning[0].id;
history({ search: queryString.stringify(search) });
notification.success({
@@ -199,12 +242,21 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
form.submit()} style={{ minWidth: 170 }}>
+
}
>
-
);
})
diff --git a/client/src/components/shop-employees/shop-employees-list.component.jsx b/client/src/components/shop-employees/shop-employees-list.component.jsx
index 787554877..45e434b5a 100644
--- a/client/src/components/shop-employees/shop-employees-list.component.jsx
+++ b/client/src/components/shop-employees/shop-employees-list.component.jsx
@@ -8,7 +8,12 @@ import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.com
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import ResponsiveTable from "../responsive-table/responsive-table.component";
-export default function ShopEmployeesListComponent({ loading, employees }) {
+export default function ShopEmployeesListComponent({
+ loading,
+ employees,
+ onRequestEmployeeChange,
+ selectedEmployeeId
+}) {
const { t } = useTranslation();
const history = useNavigate();
const search = queryString.parse(useLocation().search);
@@ -19,6 +24,11 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
});
const navigateToEmployee = (employeeId) => {
+ if (onRequestEmployeeChange) {
+ onRequestEmployeeChange(employeeId);
+ return;
+ }
+
history({
search: queryString.stringify({
...search,
@@ -127,7 +137,7 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
rowSelection={{
onSelect: (props) => navigateToEmployee(props.id),
type: "radio",
- selectedRowKeys: [search.employeeId]
+ selectedRowKeys: [selectedEmployeeId || search.employeeId]
}}
onChange={handleTableChange}
onRow={(record) => {
diff --git a/client/src/components/shop-employees/shop-employees.container.jsx b/client/src/components/shop-employees/shop-employees.container.jsx
index 59ab1a611..3582ee40c 100644
--- a/client/src/components/shop-employees/shop-employees.container.jsx
+++ b/client/src/components/shop-employees/shop-employees.container.jsx
@@ -1,10 +1,12 @@
-import { Drawer, Grid } from "antd";
+import { Drawer, Form, Grid } from "antd";
import { useQuery } from "@apollo/client/react";
import queryString from "query-string";
import { connect } from "react-redux";
+import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_EMPLOYEES } from "../../graphql/employees.queries";
+import useConfirmDirtyFormNavigation from "../../hooks/useConfirmDirtyFormNavigation.jsx";
import AlertComponent from "../alert/alert.component";
import ShopEmployeesFormComponent from "./shop-employees-form.component";
import ShopEmployeesListComponent from "./shop-employees-list.component";
@@ -14,6 +16,8 @@ import "./shop-employees.styles.scss";
const mapStateToProps = createStructuredSelector({});
function ShopEmployeesContainer() {
+ const [form] = Form.useForm();
+ const [isEmployeeFormDirty, setIsEmployeeFormDirty] = useState(false);
const location = useLocation();
const navigate = useNavigate();
const search = queryString.parse(location.search);
@@ -41,10 +45,28 @@ function ShopEmployeesContainer() {
else if (screens.sm) drawerPercentage = bpoints.sm;
else if (screens.xs) drawerPercentage = bpoints.xs;
- const handleDrawerClose = () => {
- delete search.employeeId;
+ const hasDirtyEmployeeForm = Boolean(search.employeeId) && (isEmployeeFormDirty || form.isFieldsTouched());
+ const confirmCloseDirtyEmployee = useConfirmDirtyFormNavigation(hasDirtyEmployeeForm);
+
+ const navigateToEmployee = (employeeId) => {
+ if (employeeId === search.employeeId) return;
+ if (!confirmCloseDirtyEmployee()) return;
+
+ const nextSearch = { ...search, employeeId };
+ setIsEmployeeFormDirty(false);
navigate({
- search: queryString.stringify(search)
+ search: queryString.stringify(nextSearch)
+ });
+ };
+
+ const handleDrawerClose = () => {
+ if (!confirmCloseDirtyEmployee()) return;
+
+ const nextSearch = { ...search };
+ delete nextSearch.employeeId;
+ setIsEmployeeFormDirty(false);
+ navigate({
+ search: queryString.stringify(nextSearch)
});
};
@@ -54,7 +76,12 @@ function ShopEmployeesContainer() {
- {hasSelectedEmployee ? : null}
+ {hasSelectedEmployee ? (
+
+ ) : null}
);
diff --git a/client/src/components/shop-info/shop-info.component.jsx b/client/src/components/shop-info/shop-info.component.jsx
index ee7e40bd9..901a14afe 100644
--- a/client/src/components/shop-info/shop-info.component.jsx
+++ b/client/src/components/shop-info/shop-info.component.jsx
@@ -35,7 +35,7 @@ const mapDispatchToProps = () => ({
});
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent);
-export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
+export function ShopInfoComponent({ bodyshop, form, saveLoading, isDirty }) {
const {
treatments: { CriticalPartsScanning, Enhanced_Payroll }
} = useTreatmentsWithConfig({
@@ -165,6 +165,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
extra={