IO-3624 Finalize admin config UX and validation polish
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Alert } from "antd";
|
import { Alert } from "antd";
|
||||||
|
|
||||||
export default function AlertComponent(props) {
|
export default function AlertComponent({ title, message, ...props }) {
|
||||||
return <Alert {...props} />;
|
return <Alert {...props} title={title ?? message} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,200 @@ import AlertComponent from "../alert/alert.component";
|
|||||||
import "./form-fields-changed.styles.scss";
|
import "./form-fields-changed.styles.scss";
|
||||||
import Prompt from "../../utils/prompt";
|
import Prompt from "../../utils/prompt";
|
||||||
|
|
||||||
export default function FormsFieldChanged({ form, skipPrompt }) {
|
export default function FormsFieldChanged({ form, skipPrompt, onErrorNavigate, onReset, onDirtyChange }) {
|
||||||
const { t } = useTranslation();
|
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 = () => {
|
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 <></>;
|
//if (!form.isFieldsTouched()) return <></>;
|
||||||
return (
|
return (
|
||||||
<Form.Item className="form-fields-changed" shouldUpdate style={{ margin: 0, padding: 0, minHeight: "unset" }}>
|
<Form.Item className="form-fields-changed" shouldUpdate style={{ margin: 0, padding: 0, minHeight: "unset" }}>
|
||||||
{() => {
|
{() => {
|
||||||
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())
|
if (form.isFieldsTouched())
|
||||||
return (
|
return (
|
||||||
<Space orientation="vertical" style={{ width: "100%", marginBottom: 10 }}>
|
<Space orientation="vertical" style={{ width: "100%", marginBottom: 10 }}>
|
||||||
@@ -39,10 +222,35 @@ export default function FormsFieldChanged({ form, skipPrompt }) {
|
|||||||
{errors.length > 0 && (
|
{errors.length > 0 && (
|
||||||
<AlertComponent
|
<AlertComponent
|
||||||
type="error"
|
type="error"
|
||||||
message={t("general.labels.validationerror")}
|
title={t("general.labels.validationerror")}
|
||||||
description={
|
description={
|
||||||
<div>
|
<div className="form-fields-changed__error-groups">
|
||||||
<ul>{errors.map((e, idx) => e.errors.map((e2, idx2) => <li key={`${idx}${idx2}`}>{e2}</li>))}</ul>
|
{errorGroups.map((group) => (
|
||||||
|
<div key={group.key} className="form-fields-changed__error-group">
|
||||||
|
{hasTabbedErrorGroups && group.label ? (
|
||||||
|
<div className="form-fields-changed__error-group-title">{group.label}</div>
|
||||||
|
) : null}
|
||||||
|
<ul className="form-fields-changed__error-list">
|
||||||
|
{group.errors.map((error) => (
|
||||||
|
<li key={error.key}>
|
||||||
|
{Array.isArray(error.namePath) && error.namePath.length > 0 ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="form-fields-changed__error-link"
|
||||||
|
onClick={() => {
|
||||||
|
handleErrorClick(error.namePath);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error.message}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
error.message
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
showIcon
|
showIcon
|
||||||
|
|||||||
@@ -4,4 +4,47 @@
|
|||||||
min-height: unset !important;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Space.Compact style={{ width: "100%" }}>
|
||||||
|
<Input ref={ref} {...props} value={value} defaultValue={defaultValue} />
|
||||||
|
{urlActionHref ? (
|
||||||
|
<Button icon={<LinkOutlined />} href={urlActionHref} target="_blank" rel="noopener noreferrer" />
|
||||||
|
) : (
|
||||||
|
<Button icon={<LinkOutlined />} disabled />
|
||||||
|
)}
|
||||||
|
</Space.Compact>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default FormItemUrl;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Button } from "antd";
|
||||||
|
import ConfigListEmptyState from "./config-list-empty-state.component.jsx";
|
||||||
|
|
||||||
|
export const buildConfigListActionButton = ({ key, label, onClick, id }) => (
|
||||||
|
<Button key={key} type="primary" block id={id} onClick={onClick}>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const renderConfigListOrEmpty = ({ fields, actionLabel, renderItems }) =>
|
||||||
|
fields.length === 0 ? <ConfigListEmptyState actionLabel={actionLabel} /> : renderItems();
|
||||||
|
|
||||||
|
export const buildSectionActionButton = (key, label, onClick, id) =>
|
||||||
|
buildConfigListActionButton({ key, label, onClick, id });
|
||||||
|
|
||||||
|
export const renderListOrEmpty = (fields, actionLabel, renderItems) =>
|
||||||
|
renderConfigListOrEmpty({ fields, actionLabel, renderItems });
|
||||||
@@ -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 (
|
||||||
|
<Form.Item noStyle shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
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
|
||||||
|
? [
|
||||||
|
<div
|
||||||
|
key="inline-form-row-footer"
|
||||||
|
className="imex-inline-form-row-errors"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: normalizedActions.length > 0 ? 8 : 0,
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "left"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.ErrorList errors={errors} />
|
||||||
|
{normalizedActions.length > 0 ? <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>{normalizedActions}</div> : null}
|
||||||
|
</div>
|
||||||
|
]
|
||||||
|
: normalizedActions.length > 0
|
||||||
|
? normalizedActions
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return <LayoutFormRow {...layoutFormRowProps} className={resolvedClassName} actions={resolvedActions} />;
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -50,11 +50,23 @@ html[data-theme="dark"] {
|
|||||||
border-color: var(--imex-form-surface-border);
|
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 {
|
.ant-card-head {
|
||||||
background: var(--imex-form-surface-head);
|
background: var(--imex-form-surface-head);
|
||||||
border-bottom-color: var(--imex-form-surface-border);
|
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 {
|
&.imex-form-row--compact {
|
||||||
.ant-card-head {
|
.ant-card-head {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
@@ -189,3 +201,13 @@ html[data-theme="dark"] {
|
|||||||
font-size: var(--ant-font-size);
|
font-size: var(--ant-font-size);
|
||||||
line-height: 1.5;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
|||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
||||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||||
import { useForm } from "antd/es/form/Form";
|
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { useEffect } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -26,8 +25,10 @@ import { DateFormatter } from "../../utils/DateFormatter";
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
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 FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx";
|
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 LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import {
|
import {
|
||||||
INLINE_TITLE_GROUP_STYLE,
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
@@ -49,9 +50,10 @@ const mapDispatchToProps = () => ({
|
|||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ShopEmployeesFormComponent({ bodyshop }) {
|
export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDirty }) {
|
||||||
const { t } = useTranslation();
|
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 employeeNumber = Form.useWatch("employee_number", form);
|
||||||
const firstName = Form.useWatch("first_name", form);
|
const firstName = Form.useWatch("first_name", form);
|
||||||
const lastName = Form.useWatch("last_name", form);
|
const lastName = Form.useWatch("last_name", form);
|
||||||
@@ -66,17 +68,19 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const [deleteVacation] = useMutation(DELETE_VACATION);
|
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 },
|
variables: { id: search.employeeId },
|
||||||
skip: !search.employeeId || search.employeeId === "new",
|
skip: !search.employeeId || search.employeeId === "new",
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
const notification = useNotification();
|
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 employeeTitleName = [firstName, lastName].filter(Boolean).join(" ").trim();
|
||||||
const employeeCardTitle =
|
const employeeCardTitle =
|
||||||
[employeeNumber, employeeTitleName].filter(Boolean).join(" - ") ||
|
[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 {
|
const {
|
||||||
treatments: { Enhanced_Payroll }
|
treatments: { Enhanced_Payroll }
|
||||||
@@ -86,13 +90,49 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
splitKey: bodyshop.imexshopid
|
splitKey: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateDirtyState = useCallback(
|
||||||
|
(nextDirtyState) => {
|
||||||
|
if (typeof isDirty !== "boolean") {
|
||||||
|
setInternalIsDirty(nextDirtyState);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDirtyChange?.(nextDirtyState);
|
||||||
|
},
|
||||||
|
[isDirty, onDirtyChange]
|
||||||
|
);
|
||||||
|
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
useEffect(() => {
|
const clearEmployeeFormMeta = useCallback(() => {
|
||||||
if (data && data.employees_by_pk) form.setFieldsValue(data.employees_by_pk);
|
const fieldMeta = form.getFieldsError().map(({ name }) => ({
|
||||||
else {
|
name,
|
||||||
form.resetFields();
|
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 [updateEmployee] = useMutation(UPDATE_EMPLOYEE);
|
||||||
const [insertEmployees] = useMutation(INSERT_EMPLOYEES);
|
const [insertEmployees] = useMutation(INSERT_EMPLOYEES);
|
||||||
@@ -112,6 +152,8 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
updateDirtyState(false);
|
||||||
|
void refetch();
|
||||||
notification.success({
|
notification.success({
|
||||||
title: t("employees.successes.save")
|
title: t("employees.successes.save")
|
||||||
});
|
});
|
||||||
@@ -131,6 +173,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
variables: { employees: [{ ...values, shopid: bodyshop.id }] },
|
variables: { employees: [{ ...values, shopid: bodyshop.id }] },
|
||||||
refetchQueries: ["QUERY_EMPLOYEES"]
|
refetchQueries: ["QUERY_EMPLOYEES"]
|
||||||
}).then((r) => {
|
}).then((r) => {
|
||||||
|
updateDirtyState(false);
|
||||||
search.employeeId = r.data.insert_employees.returning[0].id;
|
search.employeeId = r.data.insert_employees.returning[0].id;
|
||||||
history({ search: queryString.stringify(search) });
|
history({ search: queryString.stringify(search) });
|
||||||
notification.success({
|
notification.success({
|
||||||
@@ -199,12 +242,21 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
<Card
|
<Card
|
||||||
title={employeeCardTitle}
|
title={employeeCardTitle}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" onClick={() => form.submit()} style={{ minWidth: 170 }}>
|
<Button type="primary" onClick={() => form.submit()} disabled={!resolvedIsDirty} style={{ minWidth: 170 }}>
|
||||||
{t("employees.actions.save_employee")}
|
{t("employees.actions.save_employee")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
<Form
|
||||||
|
onFinish={handleFinish}
|
||||||
|
autoComplete={"off"}
|
||||||
|
layout="vertical"
|
||||||
|
form={form}
|
||||||
|
onValuesChange={() => {
|
||||||
|
updateDirtyState(form.isFieldsTouched());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormsFieldChanged form={form} onReset={resetEmployeeFormToCurrentData} onDirtyChange={updateDirtyState} />
|
||||||
<LayoutFormRow
|
<LayoutFormRow
|
||||||
title={
|
title={
|
||||||
<div
|
<div
|
||||||
@@ -423,8 +475,10 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
) : (
|
) : (
|
||||||
fields.map((field, index) => {
|
fields.map((field, index) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
<Form.Item noStyle key={field.key}>
|
||||||
<LayoutFormRow
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[["rates", field.name, "cost_center"]]}
|
||||||
noDivider
|
noDivider
|
||||||
title={
|
title={
|
||||||
<div style={INLINE_TITLE_ROW_STYLE}>
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
@@ -495,7 +549,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
>
|
>
|
||||||
<InputNumber min={0} precision={2} style={{ width: "100%" }} />
|
<InputNumber min={0} precision={2} style={{ width: "100%" }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import ResponsiveTable from "../responsive-table/responsive-table.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 { t } = useTranslation();
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
@@ -19,6 +24,11 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const navigateToEmployee = (employeeId) => {
|
const navigateToEmployee = (employeeId) => {
|
||||||
|
if (onRequestEmployeeChange) {
|
||||||
|
onRequestEmployeeChange(employeeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
history({
|
history({
|
||||||
search: queryString.stringify({
|
search: queryString.stringify({
|
||||||
...search,
|
...search,
|
||||||
@@ -127,7 +137,7 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
|
|||||||
rowSelection={{
|
rowSelection={{
|
||||||
onSelect: (props) => navigateToEmployee(props.id),
|
onSelect: (props) => navigateToEmployee(props.id),
|
||||||
type: "radio",
|
type: "radio",
|
||||||
selectedRowKeys: [search.employeeId]
|
selectedRowKeys: [selectedEmployeeId || search.employeeId]
|
||||||
}}
|
}}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
onRow={(record) => {
|
onRow={(record) => {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Drawer, Grid } from "antd";
|
import { Drawer, Form, Grid } from "antd";
|
||||||
import { useQuery } from "@apollo/client/react";
|
import { useQuery } from "@apollo/client/react";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { useState } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_EMPLOYEES } from "../../graphql/employees.queries";
|
import { QUERY_EMPLOYEES } from "../../graphql/employees.queries";
|
||||||
|
import useConfirmDirtyFormNavigation from "../../hooks/useConfirmDirtyFormNavigation.jsx";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import ShopEmployeesFormComponent from "./shop-employees-form.component";
|
import ShopEmployeesFormComponent from "./shop-employees-form.component";
|
||||||
import ShopEmployeesListComponent from "./shop-employees-list.component";
|
import ShopEmployeesListComponent from "./shop-employees-list.component";
|
||||||
@@ -14,6 +16,8 @@ import "./shop-employees.styles.scss";
|
|||||||
const mapStateToProps = createStructuredSelector({});
|
const mapStateToProps = createStructuredSelector({});
|
||||||
|
|
||||||
function ShopEmployeesContainer() {
|
function ShopEmployeesContainer() {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [isEmployeeFormDirty, setIsEmployeeFormDirty] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const search = queryString.parse(location.search);
|
const search = queryString.parse(location.search);
|
||||||
@@ -41,10 +45,28 @@ function ShopEmployeesContainer() {
|
|||||||
else if (screens.sm) drawerPercentage = bpoints.sm;
|
else if (screens.sm) drawerPercentage = bpoints.sm;
|
||||||
else if (screens.xs) drawerPercentage = bpoints.xs;
|
else if (screens.xs) drawerPercentage = bpoints.xs;
|
||||||
|
|
||||||
const handleDrawerClose = () => {
|
const hasDirtyEmployeeForm = Boolean(search.employeeId) && (isEmployeeFormDirty || form.isFieldsTouched());
|
||||||
delete search.employeeId;
|
const confirmCloseDirtyEmployee = useConfirmDirtyFormNavigation(hasDirtyEmployeeForm);
|
||||||
|
|
||||||
|
const navigateToEmployee = (employeeId) => {
|
||||||
|
if (employeeId === search.employeeId) return;
|
||||||
|
if (!confirmCloseDirtyEmployee()) return;
|
||||||
|
|
||||||
|
const nextSearch = { ...search, employeeId };
|
||||||
|
setIsEmployeeFormDirty(false);
|
||||||
navigate({
|
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() {
|
|||||||
<RbacWrapper action="employees:page">
|
<RbacWrapper action="employees:page">
|
||||||
<div className="shop-employees-layout">
|
<div className="shop-employees-layout">
|
||||||
<div className="shop-employees-layout__list">
|
<div className="shop-employees-layout__list">
|
||||||
<ShopEmployeesListComponent employees={data ? data.employees : []} loading={loading} />
|
<ShopEmployeesListComponent
|
||||||
|
employees={data ? data.employees : []}
|
||||||
|
loading={loading}
|
||||||
|
onRequestEmployeeChange={navigateToEmployee}
|
||||||
|
selectedEmployeeId={search.employeeId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Drawer
|
<Drawer
|
||||||
@@ -64,7 +91,9 @@ function ShopEmployeesContainer() {
|
|||||||
size={drawerPercentage}
|
size={drawerPercentage}
|
||||||
onClose={handleDrawerClose}
|
onClose={handleDrawerClose}
|
||||||
>
|
>
|
||||||
{hasSelectedEmployee ? <ShopEmployeesFormComponent /> : null}
|
{hasSelectedEmployee ? (
|
||||||
|
<ShopEmployeesFormComponent form={form} onDirtyChange={setIsEmployeeFormDirty} isDirty={isEmployeeFormDirty} />
|
||||||
|
) : null}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</RbacWrapper>
|
</RbacWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const mapDispatchToProps = () => ({
|
|||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent);
|
||||||
|
|
||||||
export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
export function ShopInfoComponent({ bodyshop, form, saveLoading, isDirty }) {
|
||||||
const {
|
const {
|
||||||
treatments: { CriticalPartsScanning, Enhanced_Payroll }
|
treatments: { CriticalPartsScanning, Enhanced_Payroll }
|
||||||
} = useTreatmentsWithConfig({
|
} = useTreatmentsWithConfig({
|
||||||
@@ -165,6 +165,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
|||||||
extra={
|
extra={
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
disabled={!isDirty || saveLoading}
|
||||||
loading={saveLoading}
|
loading={saveLoading}
|
||||||
onClick={() => form.submit()}
|
onClick={() => form.submit()}
|
||||||
id="shop-info-save-button"
|
id="shop-info-save-button"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client/react";
|
import { useMutation, useQuery } from "@apollo/client/react";
|
||||||
import { Form } from "antd";
|
import { Form } from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
@@ -15,6 +15,7 @@ import { FEATURE_CONFIGS, useFormDataPreservation } from "./useFormDataPreservat
|
|||||||
export default function ShopInfoContainer() {
|
export default function ShopInfoContainer() {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [isShopInfoDirty, setIsShopInfoDirty] = useState(false);
|
||||||
const [saveLoading, setSaveLoading] = useState(false);
|
const [saveLoading, setSaveLoading] = useState(false);
|
||||||
const [updateBodyshop] = useMutation(UPDATE_SHOP);
|
const [updateBodyshop] = useMutation(UPDATE_SHOP);
|
||||||
const { loading, error, data, refetch } = useQuery(QUERY_BODYSHOP, {
|
const { loading, error, data, refetch } = useQuery(QUERY_BODYSHOP, {
|
||||||
@@ -33,7 +34,10 @@ export default function ShopInfoContainer() {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const combinedFeatureConfig = combineFeatureConfigs(FEATURE_CONFIGS.general, FEATURE_CONFIGS.responsibilitycenters);
|
const combinedFeatureConfig = useMemo(
|
||||||
|
() => combineFeatureConfigs(FEATURE_CONFIGS.general, FEATURE_CONFIGS.responsibilitycenters),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// Use form data preservation for all shop-info features
|
// Use form data preservation for all shop-info features
|
||||||
const { createSubmissionHandler, preserveHiddenFormData } = useFormDataPreservation(
|
const { createSubmissionHandler, preserveHiddenFormData } = useFormDataPreservation(
|
||||||
@@ -51,7 +55,10 @@ export default function ShopInfoContainer() {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification.success({ title: t("bodyshop.successes.save") });
|
notification.success({ title: t("bodyshop.successes.save") });
|
||||||
refetch().then(() => form.resetFields());
|
refetch().then(() => {
|
||||||
|
form.resetFields();
|
||||||
|
setIsShopInfoDirty(false);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
notification.error({
|
notification.error({
|
||||||
@@ -66,6 +73,7 @@ export default function ShopInfoContainer() {
|
|||||||
form.resetFields();
|
form.resetFields();
|
||||||
// After reset, re-apply hidden field preservation so values aren't wiped
|
// After reset, re-apply hidden field preservation so values aren't wiped
|
||||||
preserveHiddenFormData();
|
preserveHiddenFormData();
|
||||||
|
setIsShopInfoDirty(false);
|
||||||
}, [data, form, preserveHiddenFormData]);
|
}, [data, form, preserveHiddenFormData]);
|
||||||
|
|
||||||
if (error) return <AlertComponent title={error.message} type="error" />;
|
if (error) return <AlertComponent title={error.message} type="error" />;
|
||||||
@@ -76,6 +84,9 @@ export default function ShopInfoContainer() {
|
|||||||
layout="vertical"
|
layout="vertical"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
|
onValuesChange={() => {
|
||||||
|
setIsShopInfoDirty(form.isFieldsTouched());
|
||||||
|
}}
|
||||||
initialValues={
|
initialValues={
|
||||||
data
|
data
|
||||||
? data?.bodyshops?.[0]?.accountingconfig?.ClosingPeriod
|
? data?.bodyshops?.[0]?.accountingconfig?.ClosingPeriod
|
||||||
@@ -99,8 +110,8 @@ export default function ShopInfoContainer() {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FormsFieldChanged form={form} />
|
<FormsFieldChanged form={form} onDirtyChange={setIsShopInfoDirty} />
|
||||||
<ShopInfoComponent form={form} saveLoading={saveLoading} />
|
<ShopInfoComponent form={form} saveLoading={saveLoading} isDirty={isShopInfoDirty} />
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ import { TemplateList } from "../../utils/TemplateConstants";
|
|||||||
import ConfigFormTypes from "../config-form-components/config-form-types";
|
import ConfigFormTypes from "../config-form-components/config-form-types";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.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 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 LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import {
|
import {
|
||||||
INLINE_TITLE_GROUP_STYLE,
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
@@ -32,6 +33,7 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
|
|||||||
<SelectorDiv>
|
<SelectorDiv>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.intake_delivery")} id="intake-delivery">
|
<LayoutFormRow header={t("bodyshop.labels.intake_delivery")} id="intake-delivery">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
col={{ xs: 24, sm: 24, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||||
name={["intakechecklist", "templates"]}
|
name={["intakechecklist", "templates"]}
|
||||||
label={t("bodyshop.fields.intake.templates")}
|
label={t("bodyshop.fields.intake.templates")}
|
||||||
rules={[
|
rules={[
|
||||||
@@ -41,22 +43,17 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
|
|||||||
type: "array"
|
type: "array"
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
options={Object.keys(TemplateListGenerated).map((i) => ({
|
options={Object.keys(TemplateListGenerated).map((i) => ({
|
||||||
value: TemplateListGenerated[i].key,
|
value: TemplateListGenerated[i].key,
|
||||||
label: TemplateListGenerated[i].title
|
label: TemplateListGenerated[i].title
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
|
||||||
name={["intakechecklist", "next_contact_hours"]}
|
|
||||||
label={t("bodyshop.fields.intake.next_contact_hours")}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} precision={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
col={{ xs: 24, sm: 24, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||||
name={["deliverchecklist", "templates"]}
|
name={["deliverchecklist", "templates"]}
|
||||||
label={t("bodyshop.fields.deliver.templates")}
|
label={t("bodyshop.fields.deliver.templates")}
|
||||||
rules={[
|
rules={[
|
||||||
@@ -69,13 +66,21 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
|
|||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
options={Object.keys(TemplateListGenerated).map((i) => ({
|
options={Object.keys(TemplateListGenerated).map((i) => ({
|
||||||
value: TemplateListGenerated[i].key,
|
value: TemplateListGenerated[i].key,
|
||||||
label: TemplateListGenerated[i].title
|
label: TemplateListGenerated[i].title
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
col={{ xs: 24, sm: 10, md: 8, lg: 8, xl: 8, xxl: 8 }}
|
||||||
|
name={["intakechecklist", "next_contact_hours"]}
|
||||||
|
label={t("bodyshop.fields.intake.next_contact_hours")}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} precision={0} suffix="hrs" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
col={{ xs: 24, sm: 14, md: 16, lg: 16, xl: 16, xxl: 16 }}
|
||||||
name={["deliverchecklist", "actual_delivery"]}
|
name={["deliverchecklist", "actual_delivery"]}
|
||||||
label={t("bodyshop.fields.deliver.require_actual_delivery_date")}
|
label={t("bodyshop.fields.deliver.require_actual_delivery_date")}
|
||||||
rules={[
|
rules={[
|
||||||
@@ -114,8 +119,10 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
|
|||||||
) : (
|
) : (
|
||||||
fields.map((field, index) => {
|
fields.map((field, index) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item key={field.key}>
|
<Form.Item noStyle key={field.key}>
|
||||||
<LayoutFormRow
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[["intakechecklist", "form", field.name, "name"]]}
|
||||||
noDivider
|
noDivider
|
||||||
title={
|
title={
|
||||||
<div style={INLINE_TITLE_ROW_STYLE}>
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
@@ -234,7 +241,7 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -269,8 +276,10 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
|
|||||||
) : (
|
) : (
|
||||||
fields.map((field, index) => {
|
fields.map((field, index) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item key={field.key}>
|
<Form.Item noStyle key={field.key}>
|
||||||
<LayoutFormRow
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[["deliverchecklist", "form", field.name, "name"]]}
|
||||||
noDivider
|
noDivider
|
||||||
title={
|
title={
|
||||||
<div style={INLINE_TITLE_ROW_STYLE}>
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
@@ -391,7 +400,7 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.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 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 LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import {
|
import {
|
||||||
INLINE_TITLE_GROUP_STYLE,
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
@@ -15,15 +16,16 @@ import {
|
|||||||
|
|
||||||
export default function ShopInfoLaborRates() {
|
export default function ShopInfoLaborRates() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.shoprates")}>
|
<LayoutFormRow header={t("bodyshop.labels.shoprates")}>
|
||||||
<Form.Item label={t("jobs.fields.rate_ats")} name={["shoprates", "rate_ats"]}>
|
<Form.Item label={t("jobs.fields.rate_ats")} name={["shoprates", "rate_ats"]}>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_ats_flat")} name={["shoprates", "rate_ats_flat"]}>
|
<Form.Item label={t("jobs.fields.rate_ats_flat")} name={["shoprates", "rate_ats_flat"]}>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<Form.List name={["md_labor_rates"]}>
|
<Form.List name={["md_labor_rates"]}>
|
||||||
@@ -50,8 +52,10 @@ export default function ShopInfoLaborRates() {
|
|||||||
) : (
|
) : (
|
||||||
fields.map((field, index) => {
|
fields.map((field, index) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item key={field.key}>
|
<Form.Item noStyle key={field.key}>
|
||||||
<LayoutFormRow
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[["md_labor_rates", field.name, "rate_label"]]}
|
||||||
noDivider={index === 0}
|
noDivider={index === 0}
|
||||||
title={
|
title={
|
||||||
<div style={INLINE_TITLE_ROW_STYLE}>
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
@@ -110,7 +114,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_lab")}
|
label={t("jobs.fields.rate_lab")}
|
||||||
@@ -123,7 +127,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_lad")}
|
label={t("jobs.fields.rate_lad")}
|
||||||
@@ -136,7 +140,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_lae")}
|
label={t("jobs.fields.rate_lae")}
|
||||||
@@ -149,7 +153,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_laf")}
|
label={t("jobs.fields.rate_laf")}
|
||||||
@@ -162,7 +166,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_lag")}
|
label={t("jobs.fields.rate_lag")}
|
||||||
@@ -175,7 +179,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_lam")}
|
label={t("jobs.fields.rate_lam")}
|
||||||
@@ -188,7 +192,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_lar")}
|
label={t("jobs.fields.rate_lar")}
|
||||||
@@ -201,7 +205,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_las")}
|
label={t("jobs.fields.rate_las")}
|
||||||
@@ -214,7 +218,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_la1")}
|
label={t("jobs.fields.rate_la1")}
|
||||||
@@ -227,7 +231,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_la2")}
|
label={t("jobs.fields.rate_la2")}
|
||||||
@@ -240,7 +244,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_la3")}
|
label={t("jobs.fields.rate_la3")}
|
||||||
@@ -253,7 +257,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_la4")}
|
label={t("jobs.fields.rate_la4")}
|
||||||
@@ -266,7 +270,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_mash")}
|
label={t("jobs.fields.rate_mash")}
|
||||||
@@ -279,7 +283,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_mapa")}
|
label={t("jobs.fields.rate_mapa")}
|
||||||
@@ -292,7 +296,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_ma2s")}
|
label={t("jobs.fields.rate_ma2s")}
|
||||||
@@ -305,7 +309,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_ma3s")}
|
label={t("jobs.fields.rate_ma3s")}
|
||||||
@@ -318,7 +322,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{
|
{
|
||||||
// <Form.Item
|
// <Form.Item
|
||||||
@@ -359,7 +363,7 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.rate_mahw")}
|
label={t("jobs.fields.rate_mahw")}
|
||||||
@@ -372,9 +376,9 @@ export default function ShopInfoLaborRates() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useMemo } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.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 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 LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import {
|
import {
|
||||||
INLINE_TITLE_GROUP_STYLE,
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
@@ -109,8 +110,10 @@ export default function ShopInfoPartsScan({ form }) {
|
|||||||
const fieldType = getFieldType(selectedField);
|
const fieldType = getFieldType(selectedField);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item key={field.key}>
|
<Form.Item noStyle key={field.key}>
|
||||||
<LayoutFormRow
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[["md_parts_scan", field.name, "field"]]}
|
||||||
noDivider
|
noDivider
|
||||||
title={
|
title={
|
||||||
<div style={INLINE_TITLE_ROW_STYLE}>
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
@@ -284,7 +287,7 @@ export default function ShopInfoPartsScan({ form }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</LayoutFormRow>
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DeleteFilled, HolderOutlined } from "@ant-design/icons";
|
import { DeleteFilled, HolderOutlined } from "@ant-design/icons";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||||
import { Button, DatePicker, Form, Input, InputNumber, Radio, Select, Space, Switch } from "antd";
|
import { Button, Col, DatePicker, Divider, Form, Input, InputNumber, Radio, Row, Select, Space, Switch } from "antd";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -12,7 +12,8 @@ import DataLabel from "../data-label/data-label.component";
|
|||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils";
|
import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils";
|
||||||
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx";
|
import { buildSectionActionButton, renderListOrEmpty } from "../layout-form-row/config-list-actions.utils.jsx";
|
||||||
|
import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import {
|
import {
|
||||||
INLINE_TITLE_GROUP_STYLE,
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
@@ -20,7 +21,8 @@ import {
|
|||||||
INLINE_TITLE_INPUT_STYLE,
|
INLINE_TITLE_INPUT_STYLE,
|
||||||
INLINE_TITLE_LABEL_STYLE,
|
INLINE_TITLE_LABEL_STYLE,
|
||||||
INLINE_TITLE_ROW_STYLE,
|
INLINE_TITLE_ROW_STYLE,
|
||||||
INLINE_TITLE_SEPARATOR_STYLE
|
INLINE_TITLE_SEPARATOR_STYLE,
|
||||||
|
INLINE_TITLE_SWITCH_GROUP_STYLE
|
||||||
} from "../layout-form-row/inline-form-row-title.utils.js";
|
} from "../layout-form-row/inline-form-row-title.utils.js";
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import ShopInfoResponsibilitycentersTaxesComponent from "./shop-info.responsibilitycenters.taxes.component";
|
import ShopInfoResponsibilitycentersTaxesComponent from "./shop-info.responsibilitycenters.taxes.component";
|
||||||
@@ -42,14 +44,9 @@ export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoResponsibili
|
|||||||
|
|
||||||
export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const taxAccountRowCol = { xs: 24, sm: 12, md: 12, lg: 6, xl: 6, xxl: 6 };
|
||||||
|
const taxAccountFullRowCol = { xs: 24 };
|
||||||
const dmsPayers = Form.useWatch(["cdk_configuration", "payers"], form) || [];
|
const dmsPayers = Form.useWatch(["cdk_configuration", "payers"], form) || [];
|
||||||
const buildSectionActionButton = (key, label, onClick) => (
|
|
||||||
<Button key={key} type="primary" block onClick={onClick}>
|
|
||||||
{label}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
const renderListOrEmpty = (fields, actionLabel, renderItems) =>
|
|
||||||
fields.length === 0 ? <ConfigListEmptyState actionLabel={actionLabel} /> : renderItems();
|
|
||||||
|
|
||||||
const hasDMSKey = bodyshopHasDmsKey(bodyshop);
|
const hasDMSKey = bodyshopHasDmsKey(bodyshop);
|
||||||
|
|
||||||
@@ -98,229 +95,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
<div>
|
<div>
|
||||||
<RbacWrapper action="shop:responsibilitycenter">
|
<RbacWrapper action="shop:responsibilitycenter">
|
||||||
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")} id="accountingsetup">
|
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")} id="accountingsetup">
|
||||||
{[
|
<>
|
||||||
...(HasFeatureAccess({ featureName: "export", bodyshop })
|
|
||||||
? !hasDMSKey
|
|
||||||
? [
|
|
||||||
<Form.Item
|
|
||||||
key="qbo"
|
|
||||||
label={t("bodyshop.labels.qbo")}
|
|
||||||
valuePropName="checked"
|
|
||||||
name={["accountingconfig", "qbo"]}
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>,
|
|
||||||
InstanceRenderManager({
|
|
||||||
imex: (
|
|
||||||
<Form.Item key="qbo_usa_wrapper" shouldUpdate noStyle>
|
|
||||||
{() => (
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.labels.qbo_usa")}
|
|
||||||
shouldUpdate
|
|
||||||
valuePropName="checked"
|
|
||||||
name={["accountingconfig", "qbo_usa"]}
|
|
||||||
>
|
|
||||||
<Switch disabled={!form.getFieldValue(["accountingconfig", "qbo"])} />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
<Form.Item
|
|
||||||
key="qbo_departmentid"
|
|
||||||
label={t("bodyshop.labels.qbo_departmentid")}
|
|
||||||
name={["accountingconfig", "qbo_departmentid"]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="accountingtiers"
|
|
||||||
label={t("bodyshop.labels.accountingtiers")}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
name={["accountingconfig", "tiers"]}
|
|
||||||
>
|
|
||||||
<Radio.Group>
|
|
||||||
<Radio value={2}>2</Radio>
|
|
||||||
<Radio value={3}>3</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item key="twotierpref_wrapper" shouldUpdate>
|
|
||||||
{() => {
|
|
||||||
return (
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.labels.2tiersetup")}
|
|
||||||
shouldUpdate
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: form.getFieldValue(["accountingconfig", "tiers"]) === 2
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
name={["accountingconfig", "twotierpref"]}
|
|
||||||
>
|
|
||||||
<Radio.Group disabled={form.getFieldValue(["accountingconfig", "tiers"]) === 3}>
|
|
||||||
<Radio value="name">{t("bodyshop.labels.2tiername")}</Radio>
|
|
||||||
<Radio value="source">{t("bodyshop.labels.2tiersource")}</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="printlater"
|
|
||||||
label={t("bodyshop.labels.printlater")}
|
|
||||||
valuePropName="checked"
|
|
||||||
name={["accountingconfig", "printlater"]}
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="emaillater"
|
|
||||||
label={t("bodyshop.labels.emaillater")}
|
|
||||||
valuePropName="checked"
|
|
||||||
name={["accountingconfig", "emaillater"]}
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="ReceivableCustomField1"
|
|
||||||
name={["accountingconfig", "ReceivableCustomField1"]}
|
|
||||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
|
|
||||||
>
|
|
||||||
{ReceivableCustomFieldSelect}
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="ReceivableCustomField2"
|
|
||||||
name={["accountingconfig", "ReceivableCustomField2"]}
|
|
||||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
|
|
||||||
>
|
|
||||||
{ReceivableCustomFieldSelect}
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="ReceivableCustomField3"
|
|
||||||
name={["accountingconfig", "ReceivableCustomField3"]}
|
|
||||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
|
|
||||||
>
|
|
||||||
{ReceivableCustomFieldSelect}
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="md_classes"
|
|
||||||
name={["md_classes"]}
|
|
||||||
label={t("bodyshop.fields.md_classes")}
|
|
||||||
rules={[
|
|
||||||
({ getFieldValue }) => {
|
|
||||||
return {
|
|
||||||
required: getFieldValue("enforce_class"),
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
type: "array"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Select mode="tags" />
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="enforce_class"
|
|
||||||
name={["enforce_class"]}
|
|
||||||
label={t("bodyshop.fields.enforce_class")}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="accumulatePayableLines"
|
|
||||||
name={["accountingconfig", "accumulatePayableLines"]}
|
|
||||||
label={t("bodyshop.fields.accumulatePayableLines")}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
]
|
|
||||||
: []
|
|
||||||
: []),
|
|
||||||
<Form.Item
|
|
||||||
key="inhousevendorid"
|
|
||||||
label={t("bodyshop.fields.inhousevendorid")}
|
|
||||||
name={"inhousevendorid"}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="default_adjustment_rate"
|
|
||||||
label={t("bodyshop.fields.default_adjustment_rate")}
|
|
||||||
name={"default_adjustment_rate"}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} precision={2} />
|
|
||||||
</Form.Item>,
|
|
||||||
InstanceRenderManager({
|
|
||||||
imex: (
|
|
||||||
<Form.Item key="federal_tax_id" label={t("bodyshop.fields.federal_tax_id")} name="federal_tax_id">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
<Form.Item key="state_tax_id" label={t("bodyshop.fields.state_tax_id")} name="state_tax_id">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>,
|
|
||||||
...(HasFeatureAccess({ featureName: "bills", bodyshop })
|
|
||||||
? [
|
|
||||||
InstanceRenderManager({
|
|
||||||
imex: (
|
|
||||||
<Form.Item
|
|
||||||
key="invoice_federal_tax_rate"
|
|
||||||
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
|
||||||
name={["bill_tax_rates", "federal_tax_rate"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
<Form.Item
|
|
||||||
key="invoice_state_tax_rate"
|
|
||||||
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
|
||||||
name={["bill_tax_rates", "state_tax_rate"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="invoice_local_tax_rate"
|
|
||||||
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
|
||||||
name={["bill_tax_rates", "local_tax_rate"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key="md_payment_types"
|
key="md_payment_types"
|
||||||
name={["md_payment_types"]}
|
name={["md_payment_types"]}
|
||||||
@@ -333,85 +108,354 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select mode="tags" />
|
<Select mode="tags" />
|
||||||
</Form.Item>,
|
</Form.Item>
|
||||||
...(HasFeatureAccess({ featureName: "timetickets", bodyshop })
|
|
||||||
? [
|
<div style={{ display: "grid", gap: 16, marginTop: 16 }}>
|
||||||
|
<Row gutter={[16, 0]} wrap>
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key="tt_allow_post_to_invoiced"
|
key="inhousevendorid"
|
||||||
name={["tt_allow_post_to_invoiced"]}
|
label={t("bodyshop.fields.inhousevendorid")}
|
||||||
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
|
name={"inhousevendorid"}
|
||||||
valuePropName="checked"
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Switch />
|
<Input />
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="tt_enforce_hours_for_tech_console"
|
|
||||||
name={["tt_enforce_hours_for_tech_console"]}
|
|
||||||
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
]
|
</Col>
|
||||||
: []),
|
<Col xs={24} sm={12} xl={8}>
|
||||||
...(HasFeatureAccess({ featureName: "bills", bodyshop })
|
|
||||||
? [
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key="bill_allow_post_to_closed"
|
key="default_adjustment_rate"
|
||||||
name={["bill_allow_post_to_closed"]}
|
label={t("bodyshop.fields.default_adjustment_rate")}
|
||||||
label={t("bodyshop.fields.bill_allow_post_to_closed")}
|
name={"default_adjustment_rate"}
|
||||||
valuePropName="checked"
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Switch />
|
<InputNumber min={0} precision={2} />
|
||||||
</Form.Item>,
|
|
||||||
<Form.Item
|
|
||||||
key="disableBillCostCalculation"
|
|
||||||
name={["accountingconfig", "disableBillCostCalculation"]}
|
|
||||||
label={t("bodyshop.fields.disableBillCostCalculation")}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
]
|
</Col>
|
||||||
: []),
|
{InstanceRenderManager({
|
||||||
...(HasFeatureAccess({ featureName: "export", bodyshop })
|
imex: (
|
||||||
? [
|
<Col xs={24} sm={12} xl={8}>
|
||||||
...(ClosingPeriod.treatment === "on"
|
<Form.Item key="federal_tax_id" label={t("bodyshop.fields.federal_tax_id")} name="federal_tax_id">
|
||||||
? [
|
<Input />
|
||||||
<Form.Item
|
</Form.Item>
|
||||||
key="ClosingPeriod"
|
</Col>
|
||||||
name={["accountingconfig", "ClosingPeriod"]}
|
)
|
||||||
label={t("bodyshop.fields.closingperiod")}
|
})}
|
||||||
>
|
<Col xs={24} sm={12} xl={8}>
|
||||||
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
<Form.Item key="state_tax_id" label={t("bodyshop.fields.state_tax_id")} name="state_tax_id">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
{HasFeatureAccess({ featureName: "export", bodyshop }) &&
|
||||||
|
ClosingPeriod.treatment === "on" && (
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item
|
||||||
|
key="ClosingPeriod"
|
||||||
|
name={["accountingconfig", "ClosingPeriod"]}
|
||||||
|
label={t("bodyshop.fields.closingperiod")}
|
||||||
|
>
|
||||||
|
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
{HasFeatureAccess({ featureName: "export", bodyshop }) &&
|
||||||
|
ADPPayroll.treatment === "on" && (
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item
|
||||||
|
key="companyCode"
|
||||||
|
name={["accountingconfig", "companyCode"]}
|
||||||
|
label={t("bodyshop.fields.companycode")}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
{HasFeatureAccess({ featureName: "export", bodyshop }) &&
|
||||||
|
ADPPayroll.treatment === "on" && (
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item key="batchID" name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
{HasFeatureAccess({ featureName: "export", bodyshop }) && !hasDMSKey && (
|
||||||
|
<>
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item
|
||||||
|
key="accountingtiers"
|
||||||
|
label={t("bodyshop.labels.accountingtiers")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={["accountingconfig", "tiers"]}
|
||||||
|
>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio value={2}>2</Radio>
|
||||||
|
<Radio value={3}>3</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item key="twotierpref_wrapper" shouldUpdate noStyle>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.labels.2tiersetup")}
|
||||||
|
shouldUpdate
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["accountingconfig", "tiers"]) === 2
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={["accountingconfig", "twotierpref"]}
|
||||||
|
>
|
||||||
|
<Radio.Group disabled={form.getFieldValue(["accountingconfig", "tiers"]) === 3}>
|
||||||
|
<Radio value="name">{t("bodyshop.labels.2tiername")}</Radio>
|
||||||
|
<Radio value="source">{t("bodyshop.labels.2tiersource")}</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item
|
||||||
|
key="md_classes"
|
||||||
|
name={["md_classes"]}
|
||||||
|
label={t("bodyshop.fields.md_classes")}
|
||||||
|
rules={[
|
||||||
|
({ getFieldValue }) => {
|
||||||
|
return {
|
||||||
|
required: getFieldValue("enforce_class"),
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
type: "array"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select mode="tags" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Divider style={{ margin: "0" }} />
|
||||||
|
|
||||||
|
<Row gutter={[16, 0]} wrap>
|
||||||
|
{HasFeatureAccess({ featureName: "export", bodyshop }) && !hasDMSKey && (
|
||||||
|
<>
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item
|
||||||
|
key="printlater"
|
||||||
|
label={t("bodyshop.labels.printlater")}
|
||||||
|
valuePropName="checked"
|
||||||
|
name={["accountingconfig", "printlater"]}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item
|
||||||
|
key="emaillater"
|
||||||
|
label={t("bodyshop.labels.emaillater")}
|
||||||
|
valuePropName="checked"
|
||||||
|
name={["accountingconfig", "emaillater"]}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item
|
||||||
|
key="enforce_class"
|
||||||
|
name={["enforce_class"]}
|
||||||
|
label={t("bodyshop.fields.enforce_class")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item
|
||||||
|
key="accumulatePayableLines"
|
||||||
|
name={["accountingconfig", "accumulatePayableLines"]}
|
||||||
|
label={t("bodyshop.fields.accumulatePayableLines")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{HasFeatureAccess({ featureName: "timetickets", bodyshop }) && (
|
||||||
|
<>
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item
|
||||||
|
key="tt_allow_post_to_invoiced"
|
||||||
|
name={["tt_allow_post_to_invoiced"]}
|
||||||
|
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item
|
||||||
|
key="tt_enforce_hours_for_tech_console"
|
||||||
|
name={["tt_enforce_hours_for_tech_console"]}
|
||||||
|
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{HasFeatureAccess({ featureName: "bills", bodyshop }) && (
|
||||||
|
<>
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item
|
||||||
|
key="bill_allow_post_to_closed"
|
||||||
|
name={["bill_allow_post_to_closed"]}
|
||||||
|
label={t("bodyshop.fields.bill_allow_post_to_closed")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} xl={8}>
|
||||||
|
<Form.Item
|
||||||
|
key="disableBillCostCalculation"
|
||||||
|
name={["accountingconfig", "disableBillCostCalculation"]}
|
||||||
|
label={t("bodyshop.fields.disableBillCostCalculation")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{HasFeatureAccess({ featureName: "export", bodyshop }) && !hasDMSKey && (
|
||||||
|
<LayoutFormRow
|
||||||
|
header={t("bodyshop.labels.responsibilitycenters.quickbooks_qbd")}
|
||||||
|
grow
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
|
extra={
|
||||||
|
<Space size={12} align="center" split={<span style={INLINE_TITLE_SEPARATOR_STYLE} />}>
|
||||||
|
<div style={INLINE_TITLE_SWITCH_GROUP_STYLE}>
|
||||||
|
<span style={INLINE_TITLE_LABEL_STYLE}>{t("bodyshop.labels.qbo")}</span>
|
||||||
|
<Form.Item noStyle valuePropName="checked" name={["accountingconfig", "qbo"]}>
|
||||||
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
]
|
</div>
|
||||||
: []),
|
{InstanceRenderManager({
|
||||||
...(ADPPayroll.treatment === "on"
|
imex: (
|
||||||
? [
|
<div style={INLINE_TITLE_SWITCH_GROUP_STYLE}>
|
||||||
<Form.Item
|
<span style={INLINE_TITLE_LABEL_STYLE}>{t("bodyshop.labels.qbo_usa")}</span>
|
||||||
key="companyCode"
|
<Form.Item key="qbo_usa_wrapper" shouldUpdate noStyle>
|
||||||
name={["accountingconfig", "companyCode"]}
|
{() => (
|
||||||
label={t("bodyshop.fields.companycode")}
|
<Form.Item noStyle valuePropName="checked" name={["accountingconfig", "qbo_usa"]}>
|
||||||
>
|
<Switch disabled={!form.getFieldValue(["accountingconfig", "qbo"])} />
|
||||||
<Input />
|
</Form.Item>
|
||||||
</Form.Item>
|
)}
|
||||||
]
|
</Form.Item>
|
||||||
: []),
|
</div>
|
||||||
...(ADPPayroll.treatment === "on"
|
)
|
||||||
? [
|
})}
|
||||||
<Form.Item
|
</Space>
|
||||||
key="batchID"
|
}
|
||||||
name={["accountingconfig", "batchID"]}
|
>
|
||||||
label={t("bodyshop.fields.batchid")}
|
<Form.Item
|
||||||
>
|
key="qbo_departmentid"
|
||||||
<Input />
|
label={t("bodyshop.labels.qbo_departmentid")}
|
||||||
</Form.Item>
|
name={["accountingconfig", "qbo_departmentid"]}
|
||||||
]
|
>
|
||||||
: [])
|
<Input />
|
||||||
]
|
</Form.Item>
|
||||||
: [])
|
<Form.Item
|
||||||
]}
|
key="ReceivableCustomField1"
|
||||||
|
name={["accountingconfig", "ReceivableCustomField1"]}
|
||||||
|
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
|
||||||
|
>
|
||||||
|
{ReceivableCustomFieldSelect}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
key="ReceivableCustomField2"
|
||||||
|
name={["accountingconfig", "ReceivableCustomField2"]}
|
||||||
|
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
|
||||||
|
>
|
||||||
|
{ReceivableCustomFieldSelect}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
key="ReceivableCustomField3"
|
||||||
|
name={["accountingconfig", "ReceivableCustomField3"]}
|
||||||
|
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
|
||||||
|
>
|
||||||
|
{ReceivableCustomFieldSelect}
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{HasFeatureAccess({ featureName: "bills", bodyshop }) && (
|
||||||
|
<LayoutFormRow header={t("bodyshop.labels.responsibilitycenters.invoices")} grow style={{ marginBottom: 0 }}>
|
||||||
|
{InstanceRenderManager({
|
||||||
|
imex: (
|
||||||
|
<Form.Item
|
||||||
|
key="invoice_federal_tax_rate"
|
||||||
|
label={t("bodyshop.fields.responsibilitycenters.invoice_federal_tax_rate_short")}
|
||||||
|
name={["bill_tax_rates", "federal_tax_rate"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber suffix="%" />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<Form.Item
|
||||||
|
key="invoice_state_tax_rate"
|
||||||
|
label={t("bodyshop.fields.responsibilitycenters.invoice_state_tax_rate_short")}
|
||||||
|
name={["bill_tax_rates", "state_tax_rate"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber suffix="%" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
key="invoice_local_tax_rate"
|
||||||
|
label={t("bodyshop.fields.responsibilitycenters.invoice_local_tax_rate_short")}
|
||||||
|
name={["bill_tax_rates", "local_tax_rate"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber suffix="%" />
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
{hasDMSKey && (
|
{hasDMSKey && (
|
||||||
<LayoutFormRow header={t("bodyshop.labels.dms_setup")} id="dms_setup">
|
<LayoutFormRow header={t("bodyshop.labels.dms_setup")} id="dms_setup">
|
||||||
@@ -465,7 +509,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
label={t("bodyshop.fields.dms.sendmaterialscosting")}
|
label={t("bodyshop.fields.dms.sendmaterialscosting")}
|
||||||
name={["cdk_configuration", "sendmaterialscosting"]}
|
name={["cdk_configuration", "sendmaterialscosting"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} />
|
<InputNumber min={0} max={100} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{bodyshop.pbs_serialnumber && (
|
{bodyshop.pbs_serialnumber && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -687,8 +731,13 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
{renderListOrEmpty(fields, t("bodyshop.actions.add_cost_center"), () =>
|
{renderListOrEmpty(fields, t("bodyshop.actions.add_cost_center"), () =>
|
||||||
fields.map((field, index) => {
|
fields.map((field, index) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item key={field.key}>
|
<Form.Item noStyle key={field.key}>
|
||||||
<LayoutFormRow
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[
|
||||||
|
["md_responsibility_centers", "costs", field.name, "name"],
|
||||||
|
["md_responsibility_centers", "costs", field.name, "accountdesc"]
|
||||||
|
]}
|
||||||
noDivider
|
noDivider
|
||||||
title={
|
title={
|
||||||
<div style={INLINE_TITLE_ROW_STYLE}>
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
@@ -804,7 +853,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
<Input onBlur={handleBlur} />
|
<Input onBlur={handleBlur} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</LayoutFormRow>
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -830,8 +879,13 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
{renderListOrEmpty(fields, t("bodyshop.actions.add_profit_center"), () =>
|
{renderListOrEmpty(fields, t("bodyshop.actions.add_profit_center"), () =>
|
||||||
fields.map((field, index) => {
|
fields.map((field, index) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item key={field.key}>
|
<Form.Item noStyle key={field.key}>
|
||||||
<LayoutFormRow
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[
|
||||||
|
["md_responsibility_centers", "profits", field.name, "name"],
|
||||||
|
["md_responsibility_centers", "profits", field.name, "accountdesc"]
|
||||||
|
]}
|
||||||
noDivider
|
noDivider
|
||||||
title={
|
title={
|
||||||
<div style={INLINE_TITLE_ROW_STYLE}>
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
@@ -974,7 +1028,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
]}
|
]}
|
||||||
</LayoutFormRow>
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -3277,90 +3331,110 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<LayoutFormRow header={t("bodyshop.labels.responsibilitycenters.tax_accounts")} id="tax_accounts">
|
<LayoutFormRow header={t("bodyshop.labels.responsibilitycenters.tax_accounts")} id="tax_accounts">
|
||||||
<Form.Item
|
<div style={{ display: "grid", gap: 16 }}>
|
||||||
label={t("bodyshop.fields.responsibilitycenters.federal_tax")}
|
<LayoutFormRow
|
||||||
rules={[{ required: true }]}
|
header={t("bodyshop.fields.responsibilitycenters.federal_tax")}
|
||||||
name={["md_responsibility_centers", "taxes", "federal", "name"]}
|
grow
|
||||||
>
|
style={{ marginBottom: 0 }}
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.responsibilitycenter_accountdesc")}
|
|
||||||
rules={[{ required: true }]}
|
|
||||||
name={["md_responsibility_centers", "taxes", "federal", "accountdesc"]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
|
|
||||||
rules={[{ required: true }]}
|
|
||||||
name={["md_responsibility_centers", "taxes", "federal", "accountitem"]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
{hasDMSKey && (
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.dms.dms_acctnumber")}
|
|
||||||
rules={[{ required: true }]}
|
|
||||||
name={["md_responsibility_centers", "taxes", "federal", "dms_acctnumber"]}
|
|
||||||
>
|
>
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.responsibilitycenter_rate")}
|
|
||||||
rules={[{ required: true }]}
|
|
||||||
name={["md_responsibility_centers", "taxes", "federal", "rate"]}
|
|
||||||
>
|
|
||||||
<InputNumber precision={2} />
|
|
||||||
</Form.Item>
|
|
||||||
{InstanceRenderManager({
|
|
||||||
imex: [
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key="state_tax_name"
|
label={t("bodyshop.fields.responsibilitycenters.federal_tax")}
|
||||||
label={t("bodyshop.fields.responsibilitycenters.state_tax")}
|
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
name={["md_responsibility_centers", "taxes", "state", "name"]}
|
name={["md_responsibility_centers", "taxes", "federal", "name"]}
|
||||||
|
col={taxAccountRowCol}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>,
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key="state_tax_accountdesc"
|
|
||||||
label={t("bodyshop.fields.responsibilitycenter_accountdesc")}
|
label={t("bodyshop.fields.responsibilitycenter_accountdesc")}
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
name={["md_responsibility_centers", "taxes", "state", "accountdesc"]}
|
name={["md_responsibility_centers", "taxes", "federal", "accountdesc"]}
|
||||||
|
col={taxAccountRowCol}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>,
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key="state_tax_accountitem"
|
|
||||||
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
|
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
name={["md_responsibility_centers", "taxes", "state", "accountitem"]}
|
name={["md_responsibility_centers", "taxes", "federal", "accountitem"]}
|
||||||
|
col={taxAccountRowCol}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>,
|
</Form.Item>
|
||||||
hasDMSKey ? (
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.responsibilitycenter_rate")}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
name={["md_responsibility_centers", "taxes", "federal", "rate"]}
|
||||||
|
col={taxAccountRowCol}
|
||||||
|
>
|
||||||
|
<InputNumber precision={2} suffix="%" />
|
||||||
|
</Form.Item>
|
||||||
|
{hasDMSKey && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key="state_tax_dms_acctnumber"
|
|
||||||
label={t("bodyshop.fields.dms.dms_acctnumber")}
|
label={t("bodyshop.fields.dms.dms_acctnumber")}
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
name={["md_responsibility_centers", "taxes", "state", "dms_acctnumber"]}
|
name={["md_responsibility_centers", "taxes", "federal", "dms_acctnumber"]}
|
||||||
|
col={taxAccountFullRowCol}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
) : null,
|
)}
|
||||||
<Form.Item
|
</LayoutFormRow>
|
||||||
key="state_tax_rate"
|
|
||||||
label={t("bodyshop.fields.responsibilitycenter_rate")}
|
{InstanceRenderManager({
|
||||||
rules={[{ required: true }]}
|
imex: (
|
||||||
name={["md_responsibility_centers", "taxes", "state", "rate"]}
|
<LayoutFormRow
|
||||||
>
|
header={t("bodyshop.fields.responsibilitycenters.state_tax")}
|
||||||
<InputNumber precision={2} />
|
grow
|
||||||
</Form.Item>
|
style={{ marginBottom: 0 }}
|
||||||
],
|
>
|
||||||
rome: null
|
<Form.Item
|
||||||
})}
|
label={t("bodyshop.fields.responsibilitycenters.state_tax")}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
name={["md_responsibility_centers", "taxes", "state", "name"]}
|
||||||
|
col={taxAccountRowCol}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.responsibilitycenter_accountdesc")}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
name={["md_responsibility_centers", "taxes", "state", "accountdesc"]}
|
||||||
|
col={taxAccountRowCol}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
name={["md_responsibility_centers", "taxes", "state", "accountitem"]}
|
||||||
|
col={taxAccountRowCol}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.responsibilitycenter_rate")}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
name={["md_responsibility_centers", "taxes", "state", "rate"]}
|
||||||
|
col={taxAccountRowCol}
|
||||||
|
>
|
||||||
|
<InputNumber precision={2} suffix="%" />
|
||||||
|
</Form.Item>
|
||||||
|
{hasDMSKey && (
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.dms.dms_acctnumber")}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
name={["md_responsibility_centers", "taxes", "state", "dms_acctnumber"]}
|
||||||
|
col={taxAccountFullRowCol}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</LayoutFormRow>
|
||||||
|
),
|
||||||
|
rome: null
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
|
||||||
{DmsAp.treatment === "on" && (
|
{DmsAp.treatment === "on" && (
|
||||||
@@ -3400,7 +3474,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
name={["md_responsibility_centers", "taxes", "federal_itc", "rate"]}
|
name={["md_responsibility_centers", "taxes", "federal_itc", "rate"]}
|
||||||
>
|
>
|
||||||
<InputNumber precision={2} />
|
<InputNumber precision={2} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
)}
|
)}
|
||||||
@@ -3541,8 +3615,13 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
{renderListOrEmpty(fields, t("bodyshop.actions.newsalestaxcode"), () =>
|
{renderListOrEmpty(fields, t("bodyshop.actions.newsalestaxcode"), () =>
|
||||||
fields.map((field, index) => {
|
fields.map((field, index) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item key={field.key}>
|
<Form.Item noStyle key={field.key}>
|
||||||
<LayoutFormRow
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[
|
||||||
|
["md_responsibility_centers", "sales_tax_codes", field.name, "description"],
|
||||||
|
["md_responsibility_centers", "sales_tax_codes", field.name, "code"]
|
||||||
|
]}
|
||||||
id="sales_tax_codes"
|
id="sales_tax_codes"
|
||||||
noDivider
|
noDivider
|
||||||
title={
|
title={
|
||||||
@@ -3623,7 +3702,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAB", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAB", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -139,7 +139,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -185,7 +185,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAD", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAD", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -212,7 +212,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -258,7 +258,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAE", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAE", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -285,7 +285,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -331,7 +331,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAF", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAF", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -358,7 +358,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -404,7 +404,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAG", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAG", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -431,7 +431,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -477,7 +477,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAM", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAM", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -504,7 +504,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -550,7 +550,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAR", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAR", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -577,7 +577,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -623,7 +623,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAS", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAS", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -723,7 +723,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={2} />
|
<InputNumber min={0} max={100} precision={2} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -790,7 +790,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.materials.mat_adjp")}
|
label={t("jobs.fields.materials.mat_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfm", "MAPA", "mat_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfm", "MAPA", "mat_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.materials.tax_ind")}
|
label={t("jobs.fields.materials.tax_ind")}
|
||||||
@@ -817,7 +817,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -875,7 +875,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.materials.mat_adjp")}
|
label={t("jobs.fields.materials.mat_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfm", "MASH", "mat_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfm", "MASH", "mat_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.materials.tax_ind")}
|
label={t("jobs.fields.materials.tax_ind")}
|
||||||
@@ -902,7 +902,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -2318,7 +2318,7 @@ function getTierTaxFormItems({ typeNum, typeNumIterator, t }) {
|
|||||||
]}
|
]}
|
||||||
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `ty${typeNum}_rate${typeNumIterator}`]}
|
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `ty${typeNum}_rate${typeNumIterator}`]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} precision={2} />
|
<InputNumber min={0} precision={2} suffix="%" />
|
||||||
</Form.Item>,
|
</Form.Item>,
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key={`tax_type_${typeNum}_surcharge_${typeNumIterator}`}
|
key={`tax_type_${typeNum}_surcharge_${typeNumIterator}`}
|
||||||
@@ -2331,7 +2331,7 @@ function getTierTaxFormItems({ typeNum, typeNumIterator, t }) {
|
|||||||
]}
|
]}
|
||||||
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `ty${typeNum}_sur${typeNumIterator}`]}
|
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `ty${typeNum}_sur${typeNumIterator}`]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} precision={2} />
|
<InputNumber min={0} precision={2} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default function ShopInfoRoGuard({ form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={1} disabled={disabled} />
|
<InputNumber min={0} max={100} precision={1} suffix="%" disabled={disabled} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { CloseOutlined, DeleteFilled } from "@ant-design/icons";
|
import { CloseOutlined, DeleteFilled, HolderOutlined } from "@ant-design/icons";
|
||||||
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
||||||
import { arrayMove, rectSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable";
|
import { arrayMove, rectSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import { Button, Form, Select, Space } from "antd";
|
import { Button, Form, Select, Space } from "antd";
|
||||||
import { ChromePicker } from "react-color";
|
import { ChromePicker } from "react-color";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils";
|
import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils";
|
||||||
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx";
|
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 LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import { DEFAULT_TRANSLUCENT_CARD_COLOR, getTintedCardSurfaceStyles } from "./shop-info.color.utils";
|
import { DEFAULT_TRANSLUCENT_CARD_COLOR, getTintedCardSurfaceStyles } from "./shop-info.color.utils";
|
||||||
|
|
||||||
@@ -58,20 +60,43 @@ const SelectorDiv = styled.div`
|
|||||||
.job-statuses-source-tag-wrapper {
|
.job-statuses-source-tag-wrapper {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin-inline-end: 4px;
|
margin-inline-end: 6px;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-statuses-source-tag-wrapper .ant-select-selection-item {
|
.job-statuses-source-tag-wrapper .ant-select-selection-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 30px;
|
||||||
|
min-width: 132px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding-inline: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
background: var(--ant-color-fill-quaternary);
|
||||||
|
justify-content: space-between;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
margin-inline-end: 0;
|
margin-inline-end: 0;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.job-statuses-source-tag-wrapper .job-statuses-source-tag-handle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
flex: none;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.job-statuses-source-tag-wrapper .ant-select-selection-item-content {
|
.job-statuses-source-tag-wrapper .ant-select-selection-item-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-statuses-source-tag-wrapper .ant-select-selection-item:active {
|
.job-statuses-source-tag-wrapper .ant-select-selection-item:active {
|
||||||
@@ -79,7 +104,22 @@ const SelectorDiv = styled.div`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.job-statuses-source-tag-wrapper .ant-select-selection-item-remove {
|
.job-statuses-source-tag-wrapper .ant-select-selection-item-remove {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
transition:
|
||||||
|
background 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-statuses-source-tag-wrapper .ant-select-selection-item-remove:hover {
|
||||||
|
background: var(--ant-color-fill-secondary);
|
||||||
|
color: var(--ant-color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-statuses-source-tag-wrapper--dragging {
|
.job-statuses-source-tag-wrapper--dragging {
|
||||||
@@ -89,6 +129,30 @@ const SelectorDiv = styled.div`
|
|||||||
|
|
||||||
const normalizeStatuses = (statuses) => [...new Set((statuses || []).map((item) => item?.trim()).filter(Boolean))];
|
const normalizeStatuses = (statuses) => [...new Set((statuses || []).map((item) => item?.trim()).filter(Boolean))];
|
||||||
|
|
||||||
|
const getTranslatedDragRect = (active, delta) => {
|
||||||
|
const rect = active?.rect?.current?.initial || active?.rect?.current?.translated;
|
||||||
|
|
||||||
|
if (!rect) return null;
|
||||||
|
|
||||||
|
const x = delta?.x || 0;
|
||||||
|
const y = delta?.y || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: rect.left + x,
|
||||||
|
right: rect.right + x,
|
||||||
|
top: rect.top + y,
|
||||||
|
bottom: rect.bottom + y,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPointWithinRect = (point, rect) => {
|
||||||
|
if (!point || !rect) return false;
|
||||||
|
|
||||||
|
return point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom;
|
||||||
|
};
|
||||||
|
|
||||||
const DraggableStatusTag = ({ label, value, closable, onClose }) => {
|
const DraggableStatusTag = ({ label, value, closable, onClose }) => {
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
id: value
|
id: value
|
||||||
@@ -99,11 +163,13 @@ const DraggableStatusTag = ({ label, value, closable, onClose }) => {
|
|||||||
<span
|
<span
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
className={`job-statuses-source-tag-wrapper ${isDragging ? "job-statuses-source-tag-wrapper--dragging" : ""}`}
|
className={`job-statuses-source-tag-wrapper ${isDragging ? "job-statuses-source-tag-wrapper--dragging" : ""}`}
|
||||||
|
data-status-tag-value={value}
|
||||||
style={{ transform: CSS.Transform.toString(transform), transition }}
|
style={{ transform: CSS.Transform.toString(transform), transition }}
|
||||||
onPointerDownCapture={(event) => {
|
onMouseDown={(event) => {
|
||||||
if (event.target.closest(".ant-tag-close-icon")) {
|
event.stopPropagation();
|
||||||
event.stopPropagation();
|
}}
|
||||||
}
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
@@ -117,9 +183,21 @@ const DraggableStatusTag = ({ label, value, closable, onClose }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (event.target.closest(".ant-select-selection-item-remove")) {
|
||||||
|
event.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
title={labelText}
|
title={labelText}
|
||||||
>
|
>
|
||||||
|
<span className="job-statuses-source-tag-handle" aria-hidden>
|
||||||
|
<HolderOutlined />
|
||||||
|
</span>
|
||||||
<span className="ant-select-selection-item-content">{labelText}</span>
|
<span className="ant-select-selection-item-content">{labelText}</span>
|
||||||
{closable ? (
|
{closable ? (
|
||||||
<span
|
<span
|
||||||
@@ -140,8 +218,12 @@ const DraggableStatusTag = ({ label, value, closable, onClose }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SortableStatusesSelect = ({ value, onChange }) => {
|
const SortableStatusesSelect = ({ value, onChange, mode = "tags", options = [] }) => {
|
||||||
const statuses = normalizeStatuses(value);
|
const statuses = normalizeStatuses(value);
|
||||||
|
const isTagsMode = mode === "tags";
|
||||||
|
const [knownStatuses, setKnownStatuses] = useState(statuses);
|
||||||
|
const selectWrapperRef = useRef(null);
|
||||||
|
const dragRectRef = useRef(null);
|
||||||
const tagSensors = useSensors(
|
const tagSensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
activationConstraint: {
|
activationConstraint: {
|
||||||
@@ -151,16 +233,75 @@ const SortableStatusesSelect = ({ value, onChange }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleStatusesChange = (nextValues) => {
|
const handleStatusesChange = (nextValues) => {
|
||||||
onChange?.(normalizeStatuses(nextValues));
|
const normalizedNextValues = normalizeStatuses(nextValues);
|
||||||
|
if (isTagsMode) {
|
||||||
|
setKnownStatuses((currentKnownStatuses) => normalizeStatuses([...currentKnownStatuses, ...normalizedNextValues]));
|
||||||
|
}
|
||||||
|
onChange?.(normalizedNextValues);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStatusSortEnd = ({ active, over }) => {
|
useEffect(() => {
|
||||||
if (!over || active.id === over.id) return;
|
if (isTagsMode) {
|
||||||
|
setKnownStatuses((currentKnownStatuses) => normalizeStatuses([...currentKnownStatuses, ...statuses]));
|
||||||
|
}
|
||||||
|
}, [isTagsMode, statuses]);
|
||||||
|
|
||||||
|
const shouldMoveStatusToEnd = (activeId, dragRect) => {
|
||||||
|
const selectRect =
|
||||||
|
selectWrapperRef.current?.querySelector?.(".ant-select-selector")?.getBoundingClientRect?.() ||
|
||||||
|
selectWrapperRef.current?.getBoundingClientRect?.();
|
||||||
|
if (!dragRect || !selectRect) return false;
|
||||||
|
|
||||||
|
const dragLeadingPoint = {
|
||||||
|
x: dragRect.left,
|
||||||
|
y: dragRect.top
|
||||||
|
};
|
||||||
|
const dragTrailingPoint = {
|
||||||
|
x: dragRect.right,
|
||||||
|
y: dragRect.bottom
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isPointWithinRect(dragLeadingPoint, selectRect) && !isPointWithinRect(dragTrailingPoint, selectRect)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trailingStatus = statuses.filter((status) => status !== activeId).at(-1);
|
||||||
|
if (!trailingStatus) return false;
|
||||||
|
|
||||||
|
const trailingTagNode = selectWrapperRef.current?.querySelector?.(
|
||||||
|
`.job-statuses-source-tag-wrapper[data-status-tag-value="${CSS.escape(String(trailingStatus))}"]`
|
||||||
|
);
|
||||||
|
const trailingTagRect = trailingTagNode?.getBoundingClientRect?.();
|
||||||
|
|
||||||
|
if (!trailingTagRect) return false;
|
||||||
|
|
||||||
|
const isOnTrailingRow = dragRect.bottom >= trailingTagRect.top && dragRect.top <= trailingTagRect.bottom;
|
||||||
|
if (isOnTrailingRow) {
|
||||||
|
return dragRect.left >= trailingTagRect.right - 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dragRect.top >= trailingTagRect.bottom - 4;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusSortEnd = ({ active, over, delta }) => {
|
||||||
const oldIndex = statuses.indexOf(active.id);
|
const oldIndex = statuses.indexOf(active.id);
|
||||||
|
const dragRect = dragRectRef.current || getTranslatedDragRect(active, delta);
|
||||||
|
dragRectRef.current = null;
|
||||||
|
|
||||||
|
if (oldIndex < 0) return;
|
||||||
|
|
||||||
|
if (!over) {
|
||||||
|
if (oldIndex !== statuses.length - 1 && shouldMoveStatusToEnd(active.id, dragRect)) {
|
||||||
|
onChange?.(arrayMove(statuses, oldIndex, statuses.length - 1));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active.id === over.id) return;
|
||||||
|
|
||||||
const newIndex = statuses.indexOf(over.id);
|
const newIndex = statuses.indexOf(over.id);
|
||||||
|
|
||||||
if (oldIndex < 0 || newIndex < 0) return;
|
if (newIndex < 0) return;
|
||||||
|
|
||||||
onChange?.(arrayMove(statuses, oldIndex, newIndex));
|
onChange?.(arrayMove(statuses, oldIndex, newIndex));
|
||||||
};
|
};
|
||||||
@@ -169,18 +310,50 @@ const SortableStatusesSelect = ({ value, onChange }) => {
|
|||||||
return <DraggableStatusTag closable={closable} label={label} onClose={onClose} value={tagValue} />;
|
return <DraggableStatusTag closable={closable} label={label} onClose={onClose} value={tagValue} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const statusSelectOptions = isTagsMode
|
||||||
|
? knownStatuses.map((status) => ({
|
||||||
|
value: status,
|
||||||
|
label: status
|
||||||
|
}))
|
||||||
|
: options;
|
||||||
|
|
||||||
|
if (statuses.length === 0) {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className="job-statuses-source-select"
|
||||||
|
mode={mode}
|
||||||
|
onChange={handleStatusesChange}
|
||||||
|
options={statusSelectOptions}
|
||||||
|
value={statuses}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext collisionDetection={closestCenter} onDragEnd={handleStatusSortEnd} sensors={tagSensors}>
|
<div ref={selectWrapperRef}>
|
||||||
<SortableContext items={statuses} strategy={rectSortingStrategy}>
|
<DndContext
|
||||||
<Select
|
collisionDetection={closestCenter}
|
||||||
className="job-statuses-source-select"
|
onDragCancel={() => {
|
||||||
mode="tags"
|
dragRectRef.current = null;
|
||||||
onChange={handleStatusesChange}
|
}}
|
||||||
tagRender={renderStatusTag}
|
onDragEnd={handleStatusSortEnd}
|
||||||
value={statuses}
|
onDragMove={({ active, delta }) => {
|
||||||
/>
|
dragRectRef.current = getTranslatedDragRect(active, delta);
|
||||||
</SortableContext>
|
}}
|
||||||
</DndContext>
|
sensors={tagSensors}
|
||||||
|
>
|
||||||
|
<SortableContext items={statuses} strategy={rectSortingStrategy}>
|
||||||
|
<Select
|
||||||
|
className="job-statuses-source-select"
|
||||||
|
mode={mode}
|
||||||
|
onChange={handleStatusesChange}
|
||||||
|
options={statusSelectOptions}
|
||||||
|
tagRender={renderStatusTag}
|
||||||
|
value={statuses}
|
||||||
|
/>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -209,13 +382,20 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name={["md_ro_statuses", "statuses"]}
|
name={["md_ro_statuses", "statuses"]}
|
||||||
label={t("bodyshop.labels.alljobstatuses")}
|
label={t("bodyshop.labels.alljobstatuses")}
|
||||||
|
required
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
validator: async (_, value) => {
|
validator: async (_, value) => {
|
||||||
const populatedStatuses = normalizeStatuses(value);
|
const populatedStatuses = normalizeStatuses(value);
|
||||||
|
|
||||||
if (populatedStatuses.length === 0) {
|
if (populatedStatuses.length === 0) {
|
||||||
return Promise.reject(new Error(t("general.validation.required")));
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
t("general.validation.required", {
|
||||||
|
label: t("bodyshop.labels.alljobstatuses")
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (populatedStatuses.length !== (value || []).filter(Boolean).length) {
|
if (populatedStatuses.length !== (value || []).filter(Boolean).length) {
|
||||||
@@ -238,7 +418,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select mode="multiple" options={statusSelectOptions} />
|
<SortableStatusesSelect mode="multiple" options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["md_ro_statuses", "pre_production_statuses"]}
|
name={["md_ro_statuses", "pre_production_statuses"]}
|
||||||
@@ -251,7 +431,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select mode="multiple" options={statusSelectOptions} />
|
<SortableStatusesSelect mode="multiple" options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["md_ro_statuses", "production_statuses"]}
|
name={["md_ro_statuses", "production_statuses"]}
|
||||||
@@ -264,7 +444,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select mode="multiple" options={statusSelectOptions} />
|
<SortableStatusesSelect mode="multiple" options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["md_ro_statuses", "post_production_statuses"]}
|
name={["md_ro_statuses", "post_production_statuses"]}
|
||||||
@@ -277,7 +457,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select mode="multiple" options={statusSelectOptions} />
|
<SortableStatusesSelect mode="multiple" options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["md_ro_statuses", "ready_statuses"]}
|
name={["md_ro_statuses", "ready_statuses"]}
|
||||||
@@ -290,7 +470,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select mode="multiple" options={statusSelectOptions} />
|
<SortableStatusesSelect mode="multiple" options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["md_ro_statuses", "additional_board_statuses"]}
|
name={["md_ro_statuses", "additional_board_statuses"]}
|
||||||
@@ -303,7 +483,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select mode="multiple" options={statusSelectOptions} />
|
<SortableStatusesSelect mode="multiple" options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
@@ -449,7 +629,9 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutFormRow
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[["md_ro_statuses", "production_colors", field.name, "status"]]}
|
||||||
key={field.key}
|
key={field.key}
|
||||||
noDivider
|
noDivider
|
||||||
title={
|
title={
|
||||||
@@ -506,7 +688,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
<ColorPicker />
|
<ColorPicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
</LayoutFormRow>
|
</InlineValidatedFormRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DeleteFilled, ReloadOutlined } from "@ant-design/icons";
|
import { DeleteFilled, ReloadOutlined } from "@ant-design/icons";
|
||||||
import { Button, Form, Input, InputNumber, Select, Space, Switch, TimePicker, Tooltip } from "antd";
|
import { Button, Col, Form, Input, InputNumber, Row, Select, Space, Switch, TimePicker, Tooltip } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -8,6 +8,7 @@ import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
|||||||
import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component";
|
import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.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 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 LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import { ColorPicker } from "./shop-info.rostatus.component";
|
import { ColorPicker } from "./shop-info.rostatus.component";
|
||||||
import {
|
import {
|
||||||
@@ -142,82 +143,96 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<LayoutFormRow grow header={t("bodyshop.labels.scheduling")} id="shopinfo-scheduling">
|
<LayoutFormRow grow header={t("bodyshop.labels.scheduling")} id="shopinfo-scheduling">
|
||||||
<Form.Item
|
<>
|
||||||
label={t("bodyshop.fields.appt_length")}
|
<Form.Item
|
||||||
name={"appt_length"}
|
name={["appt_alt_transport"]}
|
||||||
rules={[
|
label={t("bodyshop.fields.appt_alt_transport")}
|
||||||
{
|
rules={[
|
||||||
required: true
|
{
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
}
|
type: "array"
|
||||||
]}
|
}
|
||||||
>
|
]}
|
||||||
<InputNumber min={15} precision={0} />
|
>
|
||||||
</Form.Item>
|
<Select mode="tags" />
|
||||||
<Form.Item
|
</Form.Item>
|
||||||
label={t("bodyshop.fields.schedule_start_time")}
|
<Form.Item
|
||||||
name={"schedule_start_time"}
|
name={["md_lost_sale_reasons"]}
|
||||||
rules={[
|
label={t("bodyshop.fields.md_lost_sale_reasons")}
|
||||||
{
|
rules={[
|
||||||
required: true
|
{
|
||||||
//message: t("general.validation.required"),
|
// required: true,
|
||||||
}
|
//message: t("general.validation.required"),
|
||||||
]}
|
type: "array"
|
||||||
id="schedule_start_time"
|
}
|
||||||
>
|
]}
|
||||||
<TimePicker disableSeconds={true} format="HH:mm" />
|
>
|
||||||
</Form.Item>
|
<Select mode="tags" />
|
||||||
<Form.Item
|
</Form.Item>
|
||||||
label={t("bodyshop.fields.schedule_end_time")}
|
<Row gutter={[16, 0]} wrap>
|
||||||
name={"schedule_end_time"}
|
<Col xs={24} sm={12} xl={6}>
|
||||||
rules={[
|
<Form.Item
|
||||||
{
|
label={t("bodyshop.fields.appt_length")}
|
||||||
required: true
|
name={"appt_length"}
|
||||||
//message: t("general.validation.required"),
|
rules={[
|
||||||
}
|
{
|
||||||
]}
|
required: true
|
||||||
id="schedule_end_time"
|
//message: t("general.validation.required"),
|
||||||
>
|
}
|
||||||
<TimePicker disableSeconds={true} format="HH:mm" />
|
]}
|
||||||
</Form.Item>
|
>
|
||||||
<Form.Item
|
<InputNumber min={15} precision={0} suffix="min" />
|
||||||
name={["appt_alt_transport"]}
|
</Form.Item>
|
||||||
label={t("bodyshop.fields.appt_alt_transport")}
|
</Col>
|
||||||
rules={[
|
<Col xs={24} sm={12} xl={6}>
|
||||||
{
|
<Form.Item
|
||||||
//message: t("general.validation.required"),
|
label={t("bodyshop.fields.schedule_start_time")}
|
||||||
type: "array"
|
name={"schedule_start_time"}
|
||||||
}
|
rules={[
|
||||||
]}
|
{
|
||||||
>
|
required: true
|
||||||
<Select mode="tags" />
|
//message: t("general.validation.required"),
|
||||||
</Form.Item>
|
}
|
||||||
<Form.Item
|
]}
|
||||||
name={["ss_configuration", "dailyhrslimit"]}
|
id="schedule_start_time"
|
||||||
label={t("bodyshop.fields.ss_configuration.dailyhrslimit")}
|
>
|
||||||
>
|
<TimePicker disableSeconds={true} format="HH:mm" />
|
||||||
<InputNumber min={0} />
|
</Form.Item>
|
||||||
</Form.Item>
|
</Col>
|
||||||
<Form.Item
|
<Col xs={24} sm={12} xl={6}>
|
||||||
name={["ss_configuration", "nobusinessdays"]}
|
<Form.Item
|
||||||
label={t("bodyshop.fields.ss_configuration.nobusinessdays")}
|
label={t("bodyshop.fields.schedule_end_time")}
|
||||||
valuePropName="checked"
|
name={"schedule_end_time"}
|
||||||
>
|
rules={[
|
||||||
<Switch />
|
{
|
||||||
</Form.Item>
|
required: true
|
||||||
<Form.Item
|
//message: t("general.validation.required"),
|
||||||
name={["md_lost_sale_reasons"]}
|
}
|
||||||
label={t("bodyshop.fields.md_lost_sale_reasons")}
|
]}
|
||||||
rules={[
|
id="schedule_end_time"
|
||||||
{
|
>
|
||||||
// required: true,
|
<TimePicker disableSeconds={true} format="HH:mm" />
|
||||||
//message: t("general.validation.required"),
|
</Form.Item>
|
||||||
type: "array"
|
</Col>
|
||||||
}
|
<Col xs={24} sm={12} xl={6}>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
name={["ss_configuration", "dailyhrslimit"]}
|
||||||
<Select mode="tags" />
|
label={t("bodyshop.fields.ss_configuration.dailyhrslimit")}
|
||||||
</Form.Item>
|
>
|
||||||
|
<InputNumber min={0} suffix="hrs" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} xl={6}>
|
||||||
|
<Form.Item
|
||||||
|
name={["ss_configuration", "nobusinessdays"]}
|
||||||
|
label={t("bodyshop.fields.ss_configuration.nobusinessdays")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.workingdays")} id="workingdays">
|
<LayoutFormRow header={t("bodyshop.labels.workingdays")} id="workingdays">
|
||||||
<Space wrap size="middle">
|
<Space wrap size="middle">
|
||||||
@@ -262,8 +277,10 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
|||||||
const appointmentColorSurfaceStyles = getTintedCardSurfaceStyles(appointmentColor.color);
|
const appointmentColorSurfaceStyles = getTintedCardSurfaceStyles(appointmentColor.color);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item key={field.key}>
|
<Form.Item noStyle key={field.key}>
|
||||||
<LayoutFormRow
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[["appt_colors", field.name, "label"]]}
|
||||||
noDivider
|
noDivider
|
||||||
title={
|
title={
|
||||||
<div style={{ minWidth: 180, maxWidth: "100%" }}>
|
<div style={{ minWidth: 180, maxWidth: "100%" }}>
|
||||||
@@ -318,7 +335,7 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
|||||||
>
|
>
|
||||||
<ColorpickerFormItemComponent styles={APPOINTMENT_COLOR_PICKER_STYLES} />
|
<ColorpickerFormItemComponent styles={APPOINTMENT_COLOR_PICKER_STYLES} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -360,8 +377,13 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
|||||||
const schedulingBucketSurfaceStyles = getTintedCardSurfaceStyles(schedulingBucket.color);
|
const schedulingBucketSurfaceStyles = getTintedCardSurfaceStyles(schedulingBucket.color);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item key={field.key}>
|
<Form.Item noStyle key={field.key}>
|
||||||
<LayoutFormRow
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[
|
||||||
|
["ssbuckets", field.name, "id"],
|
||||||
|
["ssbuckets", field.name, "label"]
|
||||||
|
]}
|
||||||
noDivider
|
noDivider
|
||||||
title={
|
title={
|
||||||
<div style={SECTION_TITLE_INPUT_ROW_STYLE}>
|
<div style={SECTION_TITLE_INPUT_ROW_STYLE}>
|
||||||
@@ -470,7 +492,7 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber />
|
<InputNumber suffix="hrs" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -478,7 +500,7 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
|||||||
key={`${index}lt`}
|
key={`${index}lt`}
|
||||||
name={[field.name, "lt"]}
|
name={[field.name, "lt"]}
|
||||||
>
|
>
|
||||||
<InputNumber />
|
<InputNumber suffix="hrs" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -501,7 +523,7 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</LayoutFormRow>
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,20 +29,19 @@ export default function ShopInfoSectionNavigator({ tabsRef, activeTabKey }) {
|
|||||||
const nextTargetMap = new Map();
|
const nextTargetMap = new Map();
|
||||||
const nextOptions = Array.from(activePane.querySelectorAll(".imex-form-row"))
|
const nextOptions = Array.from(activePane.querySelectorAll(".imex-form-row"))
|
||||||
.filter((card) => {
|
.filter((card) => {
|
||||||
const titleNode = getOwnCardTitleNode(card);
|
return shouldIncludeCardInNavigator(card, activePane);
|
||||||
if (!titleNode?.textContent?.trim()) return false;
|
|
||||||
|
|
||||||
const ancestorCard = card.parentElement?.closest(".imex-form-row");
|
|
||||||
return !ancestorCard || !activePane.contains(ancestorCard);
|
|
||||||
})
|
})
|
||||||
.map((card, index) => {
|
.map((card, index) => {
|
||||||
const label = getOwnCardTitleNode(card)?.textContent?.trim();
|
const { title, depth, searchLabel } = getCardNavigatorInfo(card, activePane);
|
||||||
const value = `${activeTabKey}-shop-info-section-${index}`;
|
const value = `${activeTabKey}-shop-info-section-${index}`;
|
||||||
|
|
||||||
nextTargetMap.set(value, card);
|
nextTargetMap.set(value, card);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label: renderNavigatorOptionLabel(title, depth),
|
||||||
|
labelText: title,
|
||||||
|
searchLabel,
|
||||||
|
depth,
|
||||||
value
|
value
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -103,12 +102,13 @@ export default function ShopInfoSectionNavigator({ tabsRef, activeTabKey }) {
|
|||||||
<div className="shop-info-section-navigator">
|
<div className="shop-info-section-navigator">
|
||||||
<Select
|
<Select
|
||||||
allowClear
|
allowClear
|
||||||
showSearch={{ optionFilterProp: "label" }}
|
showSearch
|
||||||
value={selectedSection}
|
value={selectedSection}
|
||||||
placeholder={t("bodyshop.labels.jump_to_section")}
|
placeholder={t("bodyshop.labels.jump_to_section")}
|
||||||
options={options}
|
options={options}
|
||||||
popupMatchSelectWidth={false}
|
popupMatchSelectWidth={false}
|
||||||
disabled={options.length === 0}
|
disabled={options.length === 0}
|
||||||
|
filterOption={(input, option) => option?.searchLabel?.toLowerCase().includes(input.toLowerCase())}
|
||||||
onChange={handleSectionChange}
|
onChange={handleSectionChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,6 +120,77 @@ function getOwnCardTitleNode(card) {
|
|||||||
return headNode?.querySelector(".ant-card-head-title");
|
return headNode?.querySelector(".ant-card-head-title");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOwnCardTitle(card) {
|
||||||
|
return getOwnCardTitleNode(card)?.textContent?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAncestorCards(card, activePane) {
|
||||||
|
const ancestors = [];
|
||||||
|
let currentCard = card.parentElement?.closest(".imex-form-row");
|
||||||
|
|
||||||
|
while (currentCard && activePane.contains(currentCard)) {
|
||||||
|
ancestors.push(currentCard);
|
||||||
|
currentCard = currentCard.parentElement?.closest(".imex-form-row");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ancestors.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCardDepth(card, activePane) {
|
||||||
|
return getAncestorCards(card, activePane).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisibleCard(card) {
|
||||||
|
return card.offsetParent !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNavigatorEligibleSubsection(card) {
|
||||||
|
return (
|
||||||
|
!card.classList.contains("imex-form-row--compact") &&
|
||||||
|
!card.classList.contains("imex-form-row--title-only") &&
|
||||||
|
!card.querySelector(":scope > .ant-card-actions")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIncludeCardInNavigator(card, activePane) {
|
||||||
|
const title = getOwnCardTitle(card);
|
||||||
|
if (!title || !isVisibleCard(card)) return false;
|
||||||
|
|
||||||
|
const depth = getCardDepth(card, activePane);
|
||||||
|
if (depth === 0) return true;
|
||||||
|
if (depth === 1) return isNavigatorEligibleSubsection(card);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCardNavigatorInfo(card, activePane) {
|
||||||
|
const title = getOwnCardTitle(card);
|
||||||
|
const ancestors = getAncestorCards(card, activePane);
|
||||||
|
const depth = ancestors.length;
|
||||||
|
const parentTitle = depth === 1 ? getOwnCardTitle(ancestors[0]) : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
depth,
|
||||||
|
searchLabel: parentTitle ? `${parentTitle} ${title}` : title
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNavigatorOptionLabel(title, depth) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"shop-info-section-navigator__option",
|
||||||
|
depth > 0 ? "shop-info-section-navigator__option--subsection" : null
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
<span className="shop-info-section-navigator__option-label">{title}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function clearHighlightedTarget(highlightedTargetRef) {
|
function clearHighlightedTarget(highlightedTargetRef) {
|
||||||
if (highlightedTargetRef.current) {
|
if (highlightedTargetRef.current) {
|
||||||
highlightedTargetRef.current.classList.remove(HIGHLIGHT_CLASS);
|
highlightedTargetRef.current.classList.remove(HIGHLIGHT_CLASS);
|
||||||
@@ -132,6 +203,11 @@ function areOptionsEqual(currentOptions, nextOptions) {
|
|||||||
|
|
||||||
return currentOptions.every((option, index) => {
|
return currentOptions.every((option, index) => {
|
||||||
const nextOption = nextOptions[index];
|
const nextOption = nextOptions[index];
|
||||||
return option.label === nextOption.label && option.value === nextOption.value;
|
return (
|
||||||
|
option.labelText === nextOption.labelText &&
|
||||||
|
option.searchLabel === nextOption.searchLabel &&
|
||||||
|
option.depth === nextOption.depth &&
|
||||||
|
option.value === nextOption.value
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shop-info-section-navigator__option {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-section-navigator__option--subsection {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-section-navigator__option--subsection::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
top: 50%;
|
||||||
|
width: 8px;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--ant-colorTextDescription);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-section-navigator__option-label {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.imex-form-row.shop-info-section-navigator__target--active.ant-card {
|
.imex-form-row.shop-info-section-navigator__target--active.ant-card {
|
||||||
border-color: color-mix(
|
border-color: color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.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 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 LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import {
|
import {
|
||||||
INLINE_TITLE_GROUP_STYLE,
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
@@ -17,6 +18,7 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|||||||
|
|
||||||
export default function ShopInfoSpeedPrint() {
|
export default function ShopInfoSpeedPrint() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
const allTemplates = TemplateList("job");
|
const allTemplates = TemplateList("job");
|
||||||
const TemplateListGenerated = InstanceRenderManager({
|
const TemplateListGenerated = InstanceRenderManager({
|
||||||
imex: Object.fromEntries(Object.entries(allTemplates).filter(([, { enhanced_payroll }]) => !enhanced_payroll)),
|
imex: Object.fromEntries(Object.entries(allTemplates).filter(([, { enhanced_payroll }]) => !enhanced_payroll)),
|
||||||
@@ -48,8 +50,13 @@ export default function ShopInfoSpeedPrint() {
|
|||||||
) : (
|
) : (
|
||||||
fields.map((field, index) => {
|
fields.map((field, index) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
<Form.Item noStyle key={field.key}>
|
||||||
<LayoutFormRow
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[
|
||||||
|
["speedprint", field.name, "id"],
|
||||||
|
["speedprint", field.name, "label"]
|
||||||
|
]}
|
||||||
noDivider
|
noDivider
|
||||||
title={
|
title={
|
||||||
<div style={INLINE_TITLE_ROW_STYLE}>
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
@@ -140,7 +147,7 @@ export default function ShopInfoSpeedPrint() {
|
|||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ export function ShopInfoTaskPresets({ bodyshop }) {
|
|||||||
]}
|
]}
|
||||||
name={[field.name, "percent"]}
|
name={[field.name, "percent"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} />
|
<InputNumber min={0} max={100} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.md_tasks_presets.memo")}
|
label={t("bodyshop.fields.md_tasks_presets.memo")}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useMutation, useQuery } from "@apollo/client/react";
|
|||||||
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Skeleton, Space, Switch, Typography } from "antd";
|
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Skeleton, Space, Switch, Typography } from "antd";
|
||||||
|
|
||||||
import querystring from "query-string";
|
import querystring from "query-string";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -11,9 +11,11 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.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 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 LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import {
|
import {
|
||||||
INLINE_TITLE_GROUP_STYLE,
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
@@ -61,16 +63,19 @@ const formatAllocationPercentage = (percentage) => {
|
|||||||
return `${numericValue.toFixed(2).replace(/\.?0+$/, "")}%`;
|
return `${numericValue.toFixed(2).replace(/\.?0+$/, "")}%`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
export function ShopEmployeeTeamsFormComponent({ bodyshop, form, onDirtyChange, isDirty }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [internalForm] = Form.useForm();
|
||||||
|
const [internalIsDirty, setInternalIsDirty] = useState(false);
|
||||||
|
const teamForm = form ?? internalForm;
|
||||||
|
const resolvedIsDirty = typeof isDirty === "boolean" ? isDirty : internalIsDirty;
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const search = querystring.parse(useLocation().search);
|
const search = querystring.parse(useLocation().search);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
const [hydratedTeamId, setHydratedTeamId] = useState(search.employeeTeamId === "new" ? "new" : null);
|
const [hydratedTeamId, setHydratedTeamId] = useState(search.employeeTeamId === "new" ? "new" : null);
|
||||||
const isNewTeam = search.employeeTeamId === "new";
|
const isNewTeam = search.employeeTeamId === "new";
|
||||||
|
|
||||||
const { error, data, loading } = useQuery(QUERY_EMPLOYEE_TEAM_BY_ID, {
|
const { error, data, loading, refetch } = useQuery(QUERY_EMPLOYEE_TEAM_BY_ID, {
|
||||||
variables: { id: search.employeeTeamId },
|
variables: { id: search.employeeTeamId },
|
||||||
skip: !search.employeeTeamId || isNewTeam,
|
skip: !search.employeeTeamId || isNewTeam,
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
@@ -78,38 +83,71 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
notifyOnNetworkStatusChange: true
|
notifyOnNetworkStatusChange: true
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const currentTeamData = data?.employee_teams_by_pk?.id === search.employeeTeamId ? data.employee_teams_by_pk : null;
|
||||||
if (!search.employeeTeamId) return;
|
|
||||||
|
const updateDirtyState = useCallback(
|
||||||
|
(nextDirtyState) => {
|
||||||
|
if (typeof isDirty !== "boolean") {
|
||||||
|
setInternalIsDirty(nextDirtyState);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDirtyChange?.(nextDirtyState);
|
||||||
|
},
|
||||||
|
[isDirty, onDirtyChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearTeamFormMeta = useCallback(() => {
|
||||||
|
const fieldMeta = teamForm.getFieldsError().map(({ name }) => ({
|
||||||
|
name,
|
||||||
|
touched: false,
|
||||||
|
validating: false,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (fieldMeta.length > 0) {
|
||||||
|
teamForm.setFields(fieldMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDirtyState(false);
|
||||||
|
}, [teamForm, updateDirtyState]);
|
||||||
|
|
||||||
|
const resetTeamFormToCurrentData = useCallback(() => {
|
||||||
|
let hydrationFrameId;
|
||||||
|
|
||||||
|
teamForm.resetFields();
|
||||||
|
|
||||||
if (isNewTeam) {
|
if (isNewTeam) {
|
||||||
form.resetFields();
|
|
||||||
setHydratedTeamId("new");
|
setHydratedTeamId("new");
|
||||||
return;
|
hydrationFrameId = window.requestAnimationFrame(() => {
|
||||||
|
clearTeamFormMeta();
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
if (hydrationFrameId) window.cancelAnimationFrame(hydrationFrameId);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setHydratedTeamId(null);
|
setHydratedTeamId(null);
|
||||||
}, [form, isNewTeam, search.employeeTeamId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (loading) {
|
||||||
if (!search.employeeTeamId || isNewTeam || loading) return;
|
return undefined;
|
||||||
let hydrationFrameId;
|
|
||||||
|
|
||||||
if (data?.employee_teams_by_pk?.id === search.employeeTeamId) {
|
|
||||||
form.setFieldsValue(normalizeEmployeeTeam(data.employee_teams_by_pk));
|
|
||||||
hydrationFrameId = window.requestAnimationFrame(() => {
|
|
||||||
setHydratedTeamId(search.employeeTeamId);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.resetFields();
|
|
||||||
hydrationFrameId = window.requestAnimationFrame(() => {
|
|
||||||
setHydratedTeamId(search.employeeTeamId);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentTeamData) {
|
||||||
|
teamForm.setFieldsValue(normalizeEmployeeTeam(currentTeamData));
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrationFrameId = window.requestAnimationFrame(() => {
|
||||||
|
setHydratedTeamId(search.employeeTeamId);
|
||||||
|
clearTeamFormMeta();
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (hydrationFrameId) window.cancelAnimationFrame(hydrationFrameId);
|
if (hydrationFrameId) window.cancelAnimationFrame(hydrationFrameId);
|
||||||
};
|
};
|
||||||
}, [data, form, isNewTeam, loading, search.employeeTeamId]);
|
}, [clearTeamFormMeta, currentTeamData, isNewTeam, loading, search.employeeTeamId, teamForm]);
|
||||||
|
|
||||||
|
useEffect(() => resetTeamFormToCurrentData(), [resetTeamFormToCurrentData]);
|
||||||
|
|
||||||
const [updateEmployeeTeam] = useMutation(UPDATE_EMPLOYEE_TEAM);
|
const [updateEmployeeTeam] = useMutation(UPDATE_EMPLOYEE_TEAM);
|
||||||
const [insertEmployeeTeam] = useMutation(INSERT_EMPLOYEE_TEAM);
|
const [insertEmployeeTeam] = useMutation(INSERT_EMPLOYEE_TEAM);
|
||||||
@@ -117,8 +155,8 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
label: t(labelKey),
|
label: t(labelKey),
|
||||||
value
|
value
|
||||||
}));
|
}));
|
||||||
const teamName = Form.useWatch("name", form);
|
const teamName = Form.useWatch("name", teamForm);
|
||||||
const teamMembers = Form.useWatch(["employee_team_members"], form) || [];
|
const teamMembers = Form.useWatch(["employee_team_members"], teamForm) || [];
|
||||||
const isTeamHydrating = !isNewTeam && Boolean(search.employeeTeamId) && hydratedTeamId !== search.employeeTeamId;
|
const isTeamHydrating = !isNewTeam && Boolean(search.employeeTeamId) && hydratedTeamId !== search.employeeTeamId;
|
||||||
const isAllocationTotalExact = hasExactSplitTotal(teamMembers);
|
const isAllocationTotalExact = hasExactSplitTotal(teamMembers);
|
||||||
const allocationTotalValue = formatAllocationPercentage(getSplitTotal(teamMembers))?.replace("%", "") || "0";
|
const allocationTotalValue = formatAllocationPercentage(getSplitTotal(teamMembers))?.replace("%", "") || "0";
|
||||||
@@ -172,6 +210,8 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!result.errors) {
|
if (!result.errors) {
|
||||||
|
updateDirtyState(false);
|
||||||
|
void refetch();
|
||||||
notification.success({
|
notification.success({
|
||||||
title: t("employees.successes.save")
|
title: t("employees.successes.save")
|
||||||
});
|
});
|
||||||
@@ -195,6 +235,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
},
|
},
|
||||||
refetchQueries: ["QUERY_TEAMS"]
|
refetchQueries: ["QUERY_TEAMS"]
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
|
updateDirtyState(false);
|
||||||
search.employeeTeamId = response.data.insert_employee_teams_one.id;
|
search.employeeTeamId = response.data.insert_employee_teams_one.id;
|
||||||
history({ search: querystring.stringify(search) });
|
history({ search: querystring.stringify(search) });
|
||||||
notification.success({
|
notification.success({
|
||||||
@@ -211,7 +252,12 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
<Card
|
<Card
|
||||||
title={isTeamHydrating ? undefined : teamCardTitle}
|
title={isTeamHydrating ? undefined : teamCardTitle}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" onClick={() => form.submit()} disabled={isTeamHydrating} style={{ minWidth: 190 }}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => teamForm.submit()}
|
||||||
|
disabled={isTeamHydrating || !resolvedIsDirty}
|
||||||
|
style={{ minWidth: 190 }}
|
||||||
|
>
|
||||||
{t("employee_teams.actions.save_team")}
|
{t("employee_teams.actions.save_team")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -219,7 +265,16 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
{isTeamHydrating ? (
|
{isTeamHydrating ? (
|
||||||
<Skeleton active title={false} paragraph={{ rows: 12 }} />
|
<Skeleton active title={false} paragraph={{ rows: 12 }} />
|
||||||
) : (
|
) : (
|
||||||
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
<Form
|
||||||
|
onFinish={handleFinish}
|
||||||
|
autoComplete={"off"}
|
||||||
|
layout="vertical"
|
||||||
|
form={teamForm}
|
||||||
|
onValuesChange={() => {
|
||||||
|
updateDirtyState(teamForm.isFieldsTouched());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormsFieldChanged form={teamForm} onReset={resetTeamFormToCurrentData} onDirtyChange={updateDirtyState} />
|
||||||
<LayoutFormRow
|
<LayoutFormRow
|
||||||
title={
|
title={
|
||||||
<div
|
<div
|
||||||
@@ -307,11 +362,17 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
) : (
|
) : (
|
||||||
fields.map((field, index) => {
|
fields.map((field, index) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
<Form.Item noStyle key={field.key}>
|
||||||
<Form.Item name={[field.name, "id"]} hidden>
|
<Form.Item name={[field.name, "id"]} hidden>
|
||||||
<Input type="hidden" />
|
<Input type="hidden" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<LayoutFormRow
|
<InlineValidatedFormRow
|
||||||
|
form={teamForm}
|
||||||
|
errorNames={[
|
||||||
|
["employee_team_members", field.name, "employeeid"],
|
||||||
|
["employee_team_members", field.name, "percentage"],
|
||||||
|
["employee_team_members", field.name, "payout_method"]
|
||||||
|
]}
|
||||||
grow
|
grow
|
||||||
title={
|
title={
|
||||||
<div style={INLINE_TITLE_ROW_STYLE}>
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
@@ -410,7 +471,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
>
|
>
|
||||||
{() => {
|
{() => {
|
||||||
const payoutMethod =
|
const payoutMethod =
|
||||||
form.getFieldValue(["employee_team_members", field.name, "payout_method"]) ||
|
teamForm.getFieldValue(["employee_team_members", field.name, "payout_method"]) ||
|
||||||
"hourly";
|
"hourly";
|
||||||
const fieldName = payoutMethod === "commission" ? "commission_rates" : "labor_rates";
|
const fieldName = payoutMethod === "commission" ? "commission_rates" : "labor_rates";
|
||||||
|
|
||||||
@@ -443,7 +504,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
</LayoutFormRow>
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ vi.mock("../../contexts/Notifications/notificationContext.jsx", () => ({
|
|||||||
useNotification: () => notification
|
useNotification: () => notification
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("../form-fields-changed-alert/form-fields-changed-alert.component.jsx", () => ({
|
||||||
|
default: () => null
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("../../firebase/firebase.utils", () => ({
|
vi.mock("../../firebase/firebase.utils", () => ({
|
||||||
logImEXEvent: vi.fn()
|
logImEXEvent: vi.fn()
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -6,12 +6,22 @@ import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.com
|
|||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||||
|
|
||||||
export default function ShopEmployeeTeamsListComponent({ loading, employee_teams }) {
|
export default function ShopEmployeeTeamsListComponent({
|
||||||
|
loading,
|
||||||
|
employee_teams,
|
||||||
|
onRequestTeamChange,
|
||||||
|
selectedTeamId
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
|
|
||||||
const navigateToTeam = (employeeTeamId) => {
|
const navigateToTeam = (employeeTeamId) => {
|
||||||
|
if (onRequestTeamChange) {
|
||||||
|
onRequestTeamChange(employeeTeamId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
history({
|
history({
|
||||||
search: queryString.stringify({
|
search: queryString.stringify({
|
||||||
...search,
|
...search,
|
||||||
@@ -65,7 +75,7 @@ export default function ShopEmployeeTeamsListComponent({ loading, employee_teams
|
|||||||
rowSelection={{
|
rowSelection={{
|
||||||
onSelect: (props) => navigateToTeam(props.id),
|
onSelect: (props) => navigateToTeam(props.id),
|
||||||
type: "radio",
|
type: "radio",
|
||||||
selectedRowKeys: [search.employeeTeamId]
|
selectedRowKeys: [selectedTeamId || search.employeeTeamId]
|
||||||
}}
|
}}
|
||||||
onRow={(record) => {
|
onRow={(record) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { Form } from "antd";
|
||||||
import { useQuery } from "@apollo/client/react";
|
import { useQuery } from "@apollo/client/react";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useState } from "react";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_TEAMS } from "../../graphql/employee_teams.queries";
|
import { QUERY_TEAMS } from "../../graphql/employee_teams.queries";
|
||||||
|
import useConfirmDirtyFormNavigation from "../../hooks/useConfirmDirtyFormNavigation.jsx";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import ShopEmployeeTeamsListComponent from "./shop-employee-teams.list";
|
import ShopEmployeeTeamsListComponent from "./shop-employee-teams.list";
|
||||||
@@ -13,12 +16,30 @@ import "./shop-teams.styles.scss";
|
|||||||
const mapStateToProps = createStructuredSelector({});
|
const mapStateToProps = createStructuredSelector({});
|
||||||
|
|
||||||
function ShopTeamsContainer() {
|
function ShopTeamsContainer() {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [isTeamFormDirty, setIsTeamFormDirty] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const { loading, error, data } = useQuery(QUERY_TEAMS, {
|
const { loading, error, data } = useQuery(QUERY_TEAMS, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
const hasSelectedTeam = Boolean(search.employeeTeamId);
|
const hasSelectedTeam = Boolean(search.employeeTeamId);
|
||||||
|
const hasDirtyTeamForm = Boolean(search.employeeTeamId) && isTeamFormDirty;
|
||||||
|
const confirmCloseDirtyTeam = useConfirmDirtyFormNavigation(hasDirtyTeamForm);
|
||||||
|
|
||||||
|
const navigateToTeam = (employeeTeamId) => {
|
||||||
|
if (employeeTeamId === search.employeeTeamId) return;
|
||||||
|
if (!confirmCloseDirtyTeam()) return;
|
||||||
|
|
||||||
|
setIsTeamFormDirty(false);
|
||||||
|
navigate({
|
||||||
|
search: queryString.stringify({
|
||||||
|
...search,
|
||||||
|
employeeTeamId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (error) return <AlertComponent title={error.message} type="error" />;
|
if (error) return <AlertComponent title={error.message} type="error" />;
|
||||||
|
|
||||||
@@ -30,11 +51,16 @@ function ShopTeamsContainer() {
|
|||||||
.join(" ")}
|
.join(" ")}
|
||||||
>
|
>
|
||||||
<div className="shop-teams-layout__list">
|
<div className="shop-teams-layout__list">
|
||||||
<ShopEmployeeTeamsListComponent employee_teams={data ? data.employee_teams : []} loading={loading} />
|
<ShopEmployeeTeamsListComponent
|
||||||
|
employee_teams={data ? data.employee_teams : []}
|
||||||
|
loading={loading}
|
||||||
|
onRequestTeamChange={navigateToTeam}
|
||||||
|
selectedTeamId={search.employeeTeamId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{hasSelectedTeam ? (
|
{hasSelectedTeam ? (
|
||||||
<div className="shop-teams-layout__details">
|
<div className="shop-teams-layout__details">
|
||||||
<ShopEmployeeTeamsFormComponent />
|
<ShopEmployeeTeamsFormComponent form={form} onDirtyChange={setIsTeamFormDirty} isDirty={isTeamFormDirty} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
11
client/src/hooks/useConfirmDirtyFormNavigation.jsx
Normal file
11
client/src/hooks/useConfirmDirtyFormNavigation.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function useConfirmDirtyFormNavigation(isDirty) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
if (!isDirty) return true;
|
||||||
|
return window.confirm(t("general.messages.unsavedchangespopup"));
|
||||||
|
}, [isDirty, t]);
|
||||||
|
}
|
||||||
@@ -421,6 +421,35 @@
|
|||||||
"logo_img_path": "Shop Logo",
|
"logo_img_path": "Shop Logo",
|
||||||
"logo_img_path_height": "Logo Image Height",
|
"logo_img_path_height": "Logo Image Height",
|
||||||
"logo_img_path_width": "Logo Image Width",
|
"logo_img_path_width": "Logo Image Width",
|
||||||
|
"scoreboard_setup": {
|
||||||
|
"daily_body_target": "Daily Body Target",
|
||||||
|
"daily_paint_target": "Daily Paint Target",
|
||||||
|
"ignore_blocked_days": "Ignore Blocked Days",
|
||||||
|
"last_number_working_days": "Last Number of Working Days",
|
||||||
|
"production_target_hours": "Production Target Hours"
|
||||||
|
},
|
||||||
|
"system_settings": {
|
||||||
|
"auto_email": {
|
||||||
|
"attach_pdf_to_email": "Attach PDF to Sent Emails?",
|
||||||
|
"from_emails": "Additional From Emails",
|
||||||
|
"parts_order_cc": "Parts Orders CC",
|
||||||
|
"parts_return_slip_cc": "Parts Returns CC"
|
||||||
|
},
|
||||||
|
"job_costing": {
|
||||||
|
"paint_hour_split": "Paint Hour Split",
|
||||||
|
"paint_materials_hourly_cost_rate": "Paint Materials Hourly Cost Rate",
|
||||||
|
"prep_hour_split": "Prep Hour Split",
|
||||||
|
"shop_materials_hourly_cost_rate": "Shop Materials Hourly Cost Rate",
|
||||||
|
"target_touch_time": "Target Touch Time",
|
||||||
|
"use_paint_scale_data": "Use Paint Scale Data"
|
||||||
|
},
|
||||||
|
"local_media_server": {
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"http_path": "HTTP Path",
|
||||||
|
"network_path": "Network Path",
|
||||||
|
"token": "Token"
|
||||||
|
}
|
||||||
|
},
|
||||||
"md_categories": "Categories",
|
"md_categories": "Categories",
|
||||||
"md_ccc_rates": "Courtesy Car Contract Rate Presets",
|
"md_ccc_rates": "Courtesy Car Contract Rate Presets",
|
||||||
"md_classes": "Classes",
|
"md_classes": "Classes",
|
||||||
@@ -621,6 +650,9 @@
|
|||||||
"federal_tax_itc": "Federal Tax Credit",
|
"federal_tax_itc": "Federal Tax Credit",
|
||||||
"gogcode": "GOG Code (BreakOut)",
|
"gogcode": "GOG Code (BreakOut)",
|
||||||
"gst_override": "GST Override Account #",
|
"gst_override": "GST Override Account #",
|
||||||
|
"invoice_federal_tax_rate_short": "Federal Tax Rate",
|
||||||
|
"invoice_local_tax_rate_short": "Local Tax Rate",
|
||||||
|
"invoice_state_tax_rate_short": "State Tax Rate",
|
||||||
"invoiceexemptcode": "QuickBooks US - Invoice Tax Exempt Code",
|
"invoiceexemptcode": "QuickBooks US - Invoice Tax Exempt Code",
|
||||||
"invoiceexemptcode_short": "Invoice Tax Exempt Code",
|
"invoiceexemptcode_short": "Invoice Tax Exempt Code",
|
||||||
"item_type": "Item Type",
|
"item_type": "Item Type",
|
||||||
@@ -806,7 +838,9 @@
|
|||||||
"responsibilitycenters": {
|
"responsibilitycenters": {
|
||||||
"costs": "Cost Centers",
|
"costs": "Cost Centers",
|
||||||
"default_tax_setup": "Default Tax Setup",
|
"default_tax_setup": "Default Tax Setup",
|
||||||
|
"invoices": "Invoices",
|
||||||
"profits": "Profit Centers",
|
"profits": "Profit Centers",
|
||||||
|
"quickbooks_qbd": "QuickBooks / QBD",
|
||||||
"quickbooks_us": "QuickBooks US",
|
"quickbooks_us": "QuickBooks US",
|
||||||
"sales_tax_codes": "Sales Tax Codes",
|
"sales_tax_codes": "Sales Tax Codes",
|
||||||
"tax_accounts": "Tax Accounts",
|
"tax_accounts": "Tax Accounts",
|
||||||
@@ -823,6 +857,9 @@
|
|||||||
"roguard": {
|
"roguard": {
|
||||||
"title": "RO Guard"
|
"title": "RO Guard"
|
||||||
},
|
},
|
||||||
|
"autoemail": "Auto Email",
|
||||||
|
"jobcosting": "Job Costing",
|
||||||
|
"localmediaserver": "Local Media Server",
|
||||||
"romepay": "Rome Pay",
|
"romepay": "Rome Pay",
|
||||||
"scheduling": "SMART Scheduling",
|
"scheduling": "SMART Scheduling",
|
||||||
"scoreboardsetup": "Scoreboard Setup",
|
"scoreboardsetup": "Scoreboard Setup",
|
||||||
|
|||||||
@@ -421,6 +421,35 @@
|
|||||||
"logo_img_path": "",
|
"logo_img_path": "",
|
||||||
"logo_img_path_height": "",
|
"logo_img_path_height": "",
|
||||||
"logo_img_path_width": "",
|
"logo_img_path_width": "",
|
||||||
|
"scoreboard_setup": {
|
||||||
|
"daily_body_target": "",
|
||||||
|
"daily_paint_target": "",
|
||||||
|
"ignore_blocked_days": "",
|
||||||
|
"last_number_working_days": "",
|
||||||
|
"production_target_hours": ""
|
||||||
|
},
|
||||||
|
"system_settings": {
|
||||||
|
"auto_email": {
|
||||||
|
"attach_pdf_to_email": "",
|
||||||
|
"from_emails": "",
|
||||||
|
"parts_order_cc": "",
|
||||||
|
"parts_return_slip_cc": ""
|
||||||
|
},
|
||||||
|
"job_costing": {
|
||||||
|
"paint_hour_split": "",
|
||||||
|
"paint_materials_hourly_cost_rate": "",
|
||||||
|
"prep_hour_split": "",
|
||||||
|
"shop_materials_hourly_cost_rate": "",
|
||||||
|
"target_touch_time": "",
|
||||||
|
"use_paint_scale_data": ""
|
||||||
|
},
|
||||||
|
"local_media_server": {
|
||||||
|
"enabled": "",
|
||||||
|
"http_path": "",
|
||||||
|
"network_path": "",
|
||||||
|
"token": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"md_categories": "",
|
"md_categories": "",
|
||||||
"md_ccc_rates": "",
|
"md_ccc_rates": "",
|
||||||
"md_classes": "",
|
"md_classes": "",
|
||||||
@@ -621,6 +650,9 @@
|
|||||||
"federal_tax_itc": "",
|
"federal_tax_itc": "",
|
||||||
"gogcode": "",
|
"gogcode": "",
|
||||||
"gst_override": "",
|
"gst_override": "",
|
||||||
|
"invoice_federal_tax_rate_short": "",
|
||||||
|
"invoice_local_tax_rate_short": "",
|
||||||
|
"invoice_state_tax_rate_short": "",
|
||||||
"invoiceexemptcode": "",
|
"invoiceexemptcode": "",
|
||||||
"invoiceexemptcode_short": "",
|
"invoiceexemptcode_short": "",
|
||||||
"item_type": "Item Type",
|
"item_type": "Item Type",
|
||||||
@@ -806,7 +838,9 @@
|
|||||||
"responsibilitycenters": {
|
"responsibilitycenters": {
|
||||||
"costs": "",
|
"costs": "",
|
||||||
"default_tax_setup": "",
|
"default_tax_setup": "",
|
||||||
|
"invoices": "",
|
||||||
"profits": "",
|
"profits": "",
|
||||||
|
"quickbooks_qbd": "",
|
||||||
"quickbooks_us": "",
|
"quickbooks_us": "",
|
||||||
"sales_tax_codes": "",
|
"sales_tax_codes": "",
|
||||||
"tax_accounts": "",
|
"tax_accounts": "",
|
||||||
@@ -823,6 +857,9 @@
|
|||||||
"roguard": {
|
"roguard": {
|
||||||
"title": ""
|
"title": ""
|
||||||
},
|
},
|
||||||
|
"autoemail": "",
|
||||||
|
"jobcosting": "",
|
||||||
|
"localmediaserver": "",
|
||||||
"romepay": "",
|
"romepay": "",
|
||||||
"scheduling": "",
|
"scheduling": "",
|
||||||
"scoreboardsetup": "",
|
"scoreboardsetup": "",
|
||||||
|
|||||||
@@ -421,6 +421,35 @@
|
|||||||
"logo_img_path": "",
|
"logo_img_path": "",
|
||||||
"logo_img_path_height": "",
|
"logo_img_path_height": "",
|
||||||
"logo_img_path_width": "",
|
"logo_img_path_width": "",
|
||||||
|
"scoreboard_setup": {
|
||||||
|
"daily_body_target": "",
|
||||||
|
"daily_paint_target": "",
|
||||||
|
"ignore_blocked_days": "",
|
||||||
|
"last_number_working_days": "",
|
||||||
|
"production_target_hours": ""
|
||||||
|
},
|
||||||
|
"system_settings": {
|
||||||
|
"auto_email": {
|
||||||
|
"attach_pdf_to_email": "",
|
||||||
|
"from_emails": "",
|
||||||
|
"parts_order_cc": "",
|
||||||
|
"parts_return_slip_cc": ""
|
||||||
|
},
|
||||||
|
"job_costing": {
|
||||||
|
"paint_hour_split": "",
|
||||||
|
"paint_materials_hourly_cost_rate": "",
|
||||||
|
"prep_hour_split": "",
|
||||||
|
"shop_materials_hourly_cost_rate": "",
|
||||||
|
"target_touch_time": "",
|
||||||
|
"use_paint_scale_data": ""
|
||||||
|
},
|
||||||
|
"local_media_server": {
|
||||||
|
"enabled": "",
|
||||||
|
"http_path": "",
|
||||||
|
"network_path": "",
|
||||||
|
"token": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"md_categories": "",
|
"md_categories": "",
|
||||||
"md_ccc_rates": "",
|
"md_ccc_rates": "",
|
||||||
"md_classes": "",
|
"md_classes": "",
|
||||||
@@ -621,6 +650,9 @@
|
|||||||
"federal_tax_itc": "",
|
"federal_tax_itc": "",
|
||||||
"gogcode": "",
|
"gogcode": "",
|
||||||
"gst_override": "",
|
"gst_override": "",
|
||||||
|
"invoice_federal_tax_rate_short": "",
|
||||||
|
"invoice_local_tax_rate_short": "",
|
||||||
|
"invoice_state_tax_rate_short": "",
|
||||||
"invoiceexemptcode": "",
|
"invoiceexemptcode": "",
|
||||||
"invoiceexemptcode_short": "",
|
"invoiceexemptcode_short": "",
|
||||||
"item_type": "Item Type",
|
"item_type": "Item Type",
|
||||||
@@ -806,7 +838,9 @@
|
|||||||
"responsibilitycenters": {
|
"responsibilitycenters": {
|
||||||
"costs": "",
|
"costs": "",
|
||||||
"default_tax_setup": "",
|
"default_tax_setup": "",
|
||||||
|
"invoices": "",
|
||||||
"profits": "",
|
"profits": "",
|
||||||
|
"quickbooks_qbd": "",
|
||||||
"quickbooks_us": "",
|
"quickbooks_us": "",
|
||||||
"sales_tax_codes": "",
|
"sales_tax_codes": "",
|
||||||
"tax_accounts": "",
|
"tax_accounts": "",
|
||||||
@@ -823,6 +857,9 @@
|
|||||||
"roguard": {
|
"roguard": {
|
||||||
"title": ""
|
"title": ""
|
||||||
},
|
},
|
||||||
|
"autoemail": "",
|
||||||
|
"jobcosting": "",
|
||||||
|
"localmediaserver": "",
|
||||||
"romepay": "",
|
"romepay": "",
|
||||||
"scheduling": "",
|
"scheduling": "",
|
||||||
"scoreboardsetup": "",
|
"scoreboardsetup": "",
|
||||||
|
|||||||
Reference in New Issue
Block a user