diff --git a/client/src/components/layout-form-row/inline-form-row-title.utils.js b/client/src/components/layout-form-row/inline-form-row-title.utils.js new file mode 100644 index 000000000..df659026a --- /dev/null +++ b/client/src/components/layout-form-row/inline-form-row-title.utils.js @@ -0,0 +1,66 @@ +export const inlineFormRowTitleStyles = Object.freeze({ + input: Object.freeze({ + background: "var(--imex-form-title-input-bg)", + border: "1px solid var(--imex-form-title-input-border)", + borderRadius: 6, + paddingInline: 10, + paddingBlock: 4, + lineHeight: 1.3 + }), + row: Object.freeze({ + display: "flex", + gap: 4, + flexWrap: "wrap", + alignItems: "center", + width: "100%", + paddingInline: 4 + }), + group: Object.freeze({ + display: "flex", + alignItems: "center", + gap: 6, + minWidth: 0 + }), + label: Object.freeze({ + color: "var(--ant-color-text)", + fontSize: "var(--ant-font-size)", + fontWeight: 500, + whiteSpace: "nowrap" + }), + handle: Object.freeze({ + color: "var(--ant-color-text-tertiary)", + fontSize: 14, + flex: "0 0 auto", + marginRight: 4 + }), + separator: Object.freeze({ + width: 1, + height: 18, + background: "color-mix(in srgb, var(--imex-form-surface-border) 78%, transparent)", + borderRadius: 999, + flex: "0 0 auto", + marginInline: 2 + }), + text: Object.freeze({ + whiteSpace: "nowrap", + fontWeight: 500, + fontSize: "var(--ant-font-size-lg)", + lineHeight: 1.2 + }) +}); + +export const INLINE_TITLE_INPUT_STYLE = inlineFormRowTitleStyles.input; +export const INLINE_TITLE_ROW_STYLE = inlineFormRowTitleStyles.row; +export const INLINE_TITLE_GROUP_STYLE = inlineFormRowTitleStyles.group; +export const INLINE_TITLE_LABEL_STYLE = inlineFormRowTitleStyles.label; +export const INLINE_TITLE_HANDLE_STYLE = inlineFormRowTitleStyles.handle; +export const INLINE_TITLE_SEPARATOR_STYLE = inlineFormRowTitleStyles.separator; +export const INLINE_TITLE_TEXT_STYLE = inlineFormRowTitleStyles.text; + +export const INLINE_FORM_ROW_WRAP_TITLE_STYLES = Object.freeze({ + title: Object.freeze({ + whiteSpace: "normal", + overflow: "visible", + textOverflow: "unset" + }) +}); diff --git a/client/src/components/layout-form-row/layout-form-row.component.jsx b/client/src/components/layout-form-row/layout-form-row.component.jsx index c45e5c75b..ade2cbaab 100644 --- a/client/src/components/layout-form-row/layout-form-row.component.jsx +++ b/client/src/components/layout-form-row/layout-form-row.component.jsx @@ -1,5 +1,6 @@ import { Card, Col, Row } from "antd"; import { Children, isValidElement } from "react"; +import { INLINE_FORM_ROW_WRAP_TITLE_STYLES } from "./inline-form-row-title.utils.js"; import "./layout-form-row.styles.scss"; export default function LayoutFormRow({ @@ -7,6 +8,8 @@ export default function LayoutFormRow({ children, grow = false, noDivider = false, + titleOnly = false, + wrapTitle = false, gutter, rowProps, @@ -19,10 +22,14 @@ export default function LayoutFormRow({ ...cardProps }) { const items = Children.toArray(children).filter(Boolean); - if (items.length === 0) return null; const isCompactRow = noDivider; const title = !noDivider && header ? header : undefined; + const resolvedTitle = cardProps.title ?? title; + const isHeaderOnly = titleOnly || items.length === 0; + const hideBody = isHeaderOnly; + + if (items.length === 0 && !resolvedTitle) return null; const resolvedGutter = gutter ?? [16, isCompactRow ? 8 : 16]; const bg = surfaceBg ?? (surface ? "var(--imex-form-surface)" : undefined); @@ -31,13 +38,15 @@ export default function LayoutFormRow({ const mergedStyles = mergeSemanticStyles( { + ...(wrapTitle ? INLINE_FORM_ROW_WRAP_TITLE_STYLES : null), header: { - paddingInline: isCompactRow ? 12 : 16, + paddingInline: isHeaderOnly ? 8 : isCompactRow ? 12 : 16, background: headBg, borderBottomColor: borderColor }, body: { - padding: isCompactRow ? 12 : 16, + padding: hideBody ? 0 : isCompactRow ? 12 : 16, + display: hideBody ? "none" : undefined, background: bg } }, @@ -45,31 +54,12 @@ export default function LayoutFormRow({ ); const baseCardStyle = { - marginBottom: isCompactRow ? "8px" : ".8rem", + marginBottom: isHeaderOnly ? "0" : isCompactRow ? "8px" : ".8rem", ...(bg ? { background: bg } : null), // ensures the “circled area” is tinted ...(borderColor ? { borderColor } : null), ...cardProps.style }; - // single child => just render it - if (items.length === 1) { - return ( - - {items[0]} - - ); - } - const count = items.length; // Modern responsive strategy leveraging Ant Design 6: @@ -133,22 +123,32 @@ export default function LayoutFormRow({ return ( - - {items.map((child, idx) => ( - - {child} - + {!isHeaderOnly && + (items.length === 1 ? ( + items[0] + ) : ( + + {items.map((child, idx) => ( + + {child} + + ))} + ))} - ); } @@ -162,6 +162,7 @@ function mergeSemanticStyles(defaults, userStyles) { return { ...defaults, ...computed, + title: { ...(defaults.title || {}), ...(computed.title || {}) }, header: { ...defaults.header, ...(computed.header || {}) }, body: { ...defaults.body, ...(computed.body || {}) } }; @@ -171,6 +172,7 @@ function mergeSemanticStyles(defaults, userStyles) { return { ...defaults, ...userStyles, + title: { ...(defaults.title || {}), ...(userStyles.title || {}) }, header: { ...defaults.header, ...(userStyles.header || {}) }, body: { ...defaults.body, ...(userStyles.body || {}) } }; diff --git a/client/src/components/layout-form-row/layout-form-row.styles.scss b/client/src/components/layout-form-row/layout-form-row.styles.scss index 9bc77277b..4284c5223 100644 --- a/client/src/components/layout-form-row/layout-form-row.styles.scss +++ b/client/src/components/layout-form-row/layout-form-row.styles.scss @@ -13,6 +13,8 @@ --imex-form-surface: #fafafa; /* subtle contrast vs white page */ --imex-form-surface-head: #f5f5f5; /* header strip */ --imex-form-surface-border: #d9d9d9; /* matches AntD-ish border */ + --imex-form-title-input-bg: rgba(255, 255, 255, 0.96); + --imex-form-title-input-border: rgba(0, 0, 0, 0.08); } /* Pick the selector that matches your app and remove the rest */ @@ -20,6 +22,8 @@ html[data-theme="dark"] { --imex-form-surface: rgba(255, 255, 255, 0.01); /* subtle lift off page bg */ --imex-form-surface-head: rgba(255, 255, 255, 0.06); /* slightly stronger for header strip */ --imex-form-surface-border: rgba(5, 5, 5, 0.12); + --imex-form-title-input-bg: rgba(255, 255, 255, 0.12); + --imex-form-title-input-border: rgba(255, 255, 255, 0.2); } .imex-form-row { @@ -58,6 +62,54 @@ html[data-theme="dark"] { } } + &.imex-form-row--title-only { + .ant-card-head { + min-height: auto; + padding-inline: 6px; + padding-block: 0; + border-radius: inherit; + } + + .ant-card-head-wrapper { + gap: 2px; + align-items: center; + } + + .ant-card-head-title, + .ant-card-extra { + padding-block: 0; + display: flex; + align-items: center; + } + + .ant-card-head-title { + white-space: normal; + overflow: visible; + text-overflow: unset; + font-size: var(--ant-font-size); + line-height: 1.1; + padding-inline: 4px; + } + + .ant-card-body { + display: none; + padding: 0; + } + + .ant-input, + .ant-input-number, + .ant-input-affix-wrapper, + .ant-select-selector, + .ant-picker { + background: var(--imex-form-title-input-bg); + border-color: var(--imex-form-title-input-border); + } + + .ant-input-number-input { + background: transparent; + } + } + .ant-card-body { background: var(--imex-form-surface); } @@ -68,7 +120,7 @@ html[data-theme="dark"] { /* Optional: tighter spacing on phones for better space usage */ @media (max-width: 575px) { - .ant-card-head { + &:not(.imex-form-row--title-only) .ant-card-head { padding-inline: 12px; padding-block: 12px; } @@ -89,10 +141,14 @@ html[data-theme="dark"] { width: 100%; } - .ant-form-item:has(> .imex-form-row--compact) { + .ant-form-item:has(.imex-form-row--compact) { margin-bottom: 8px; } + .ant-form-item:has(.imex-form-row--title-only) { + margin-bottom: 4px; + } + /* Better form item spacing on mobile */ @media (max-width: 575px) { .ant-form-item { diff --git a/client/src/components/shop-employees/shop-employees-form.component.jsx b/client/src/components/shop-employees/shop-employees-form.component.jsx index e9e87ec89..89cc115ff 100644 --- a/client/src/components/shop-employees/shop-employees-form.component.jsx +++ b/client/src/components/shop-employees/shop-employees-form.component.jsx @@ -1,4 +1,4 @@ -import { DeleteFilled } from "@ant-design/icons"; +import { DeleteFilled, HolderOutlined } from "@ant-design/icons"; import { useApolloClient, useMutation, useQuery } from "@apollo/client/react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd"; @@ -27,8 +27,16 @@ import dayjs from "../../utils/day"; import AlertComponent from "../alert/alert.component"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; -import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import { + INLINE_TITLE_GROUP_STYLE, + INLINE_TITLE_HANDLE_STYLE, + INLINE_TITLE_INPUT_STYLE, + INLINE_TITLE_LABEL_STYLE, + INLINE_TITLE_ROW_STYLE, + INLINE_TITLE_SEPARATOR_STYLE, + INLINE_TITLE_TEXT_STYLE +} from "../layout-form-row/inline-form-row-title.utils.js"; import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component.jsx"; @@ -42,17 +50,16 @@ const mapDispatchToProps = () => ({ export function ShopEmployeesFormComponent({ bodyshop }) { const { t } = useTranslation(); const [form] = useForm(); - const employeeRates = Form.useWatch(["rates"], form) || []; const employeeNumber = Form.useWatch("employee_number", form); const firstName = Form.useWatch("first_name", form); const lastName = Form.useWatch("last_name", form); const employeeOptionsColProps = { xs: 24, sm: 12, - md: 8, - lg: { flex: "0 0 320px" }, - xl: { flex: "0 0 280px" }, - xxl: { flex: "0 0 380px" } + md: 12, + lg: 8, + xl: 8, + xxl: 8 }; const history = useNavigate(); const search = queryString.parse(useLocation().search); @@ -194,161 +201,200 @@ export function ShopEmployeesFormComponent({ bodyshop }) { } >
- -
- - - +
+ {t("bodyshop.labels.employee_options")} +
+
+
+
- - - - - {t("employees.labels.active")}
+ + + +
+
+
- - - - - ({ - async validator(rule, value) { - if (value) { - const response = await client.query({ - query: CHECK_EMPLOYEE_NUMBER, - variables: { - employeenumber: value - } - }); - - if (response.data.employees_aggregate.aggregate.count === 0) { - return Promise.resolve(); - } else if ( - response.data.employees_aggregate.nodes.length === 1 && - response.data.employees_aggregate.nodes[0].id === form.getFieldValue("id") - ) { - return Promise.resolve(); +
{t("employees.fields.flat_rate")}
+ + + +
+
+
+ } + wrapTitle + > + + + + + + + + + + + + + ({ + async validator(rule, value) { + if (value) { + const response = await client.query({ + query: CHECK_EMPLOYEE_NUMBER, + variables: { + employeenumber: value } - return Promise.reject(t("employees.validation.unique_employee_number")); - } else { + }); + + if (response.data.employees_aggregate.aggregate.count === 0) { + return Promise.resolve(); + } else if ( + response.data.employees_aggregate.nodes.length === 1 && + response.data.employees_aggregate.nodes[0].id === form.getFieldValue("id") + ) { return Promise.resolve(); } + return Promise.reject(t("employees.validation.unique_employee_number")); + } else { + return Promise.resolve(); } - }) - ]} - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ({ - async validator(rule, value) { - const user_email = getFieldValue("user_email"); + }) + ]} + > + + + + + + + + + + + + + + + + + + + + ({ + async validator(rule, value) { + const user_email = getFieldValue("user_email"); - if (user_email && value) { - const response = await client.query({ - query: QUERY_USERS_BY_EMAIL, - variables: { - email: user_email - } - }); - - if (response.data.users.length === 1) { - return Promise.resolve(); + if (user_email && value) { + const response = await client.query({ + query: QUERY_USERS_BY_EMAIL, + variables: { + email: user_email } - return Promise.reject(t("bodyshop.validation.useremailmustexist")); - } else { + }); + + if (response.data.users.length === 1) { return Promise.resolve(); } + return Promise.reject(t("bodyshop.validation.useremailmustexist")); + } else { + return Promise.resolve(); } - }) - ]} - > - - - - - - - - - -
+ } + }) + ]} + > + + + + + + + + +
@@ -356,17 +402,82 @@ export function ShopEmployeesFormComponent({ bodyshop }) { return (
{fields.map((field, index) => { - const employeeRate = employeeRates[field.name] || {}; - return ( + +
+
{t("employees.fields.cost_center")}
+ + ({ - value: c.name, - label: c.name - }))) - ]} - /> - - - - - + /> ); })} diff --git a/client/src/components/shop-employees/shop-employees.container.jsx b/client/src/components/shop-employees/shop-employees.container.jsx index 6aa89fd76..59ab1a611 100644 --- a/client/src/components/shop-employees/shop-employees.container.jsx +++ b/client/src/components/shop-employees/shop-employees.container.jsx @@ -1,7 +1,8 @@ +import { Drawer, Grid } from "antd"; import { useQuery } from "@apollo/client/react"; import queryString from "query-string"; import { connect } from "react-redux"; -import { useLocation } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { QUERY_EMPLOYEES } from "../../graphql/employees.queries"; import AlertComponent from "../alert/alert.component"; @@ -13,34 +14,58 @@ import "./shop-employees.styles.scss"; const mapStateToProps = createStructuredSelector({}); function ShopEmployeesContainer() { - const search = queryString.parse(useLocation().search); + const location = useLocation(); + const navigate = useNavigate(); + const search = queryString.parse(location.search); const { loading, error, data } = useQuery(QUERY_EMPLOYEES, { fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); + const screens = Grid.useBreakpoint(); const hasSelectedEmployee = Boolean(search.employeeId); + const bpoints = { + xs: "100%", + sm: "100%", + md: "92%", + lg: "80%", + xl: "80%", + xxl: "80%" + }; + + let drawerPercentage = "100%"; + if (screens.xxl) drawerPercentage = bpoints.xxl; + else if (screens.xl) drawerPercentage = bpoints.xl; + else if (screens.lg) drawerPercentage = bpoints.lg; + else if (screens.md) drawerPercentage = bpoints.md; + else if (screens.sm) drawerPercentage = bpoints.sm; + else if (screens.xs) drawerPercentage = bpoints.xs; + + const handleDrawerClose = () => { + delete search.employeeId; + navigate({ + search: queryString.stringify(search) + }); + }; + if (error) return ; return ( -
+
- {hasSelectedEmployee ? ( -
- -
- ) : null}
+ + {hasSelectedEmployee ? : null} + ); } diff --git a/client/src/components/shop-employees/shop-employees.styles.scss b/client/src/components/shop-employees/shop-employees.styles.scss index 13d893a07..69ee5c1d0 100644 --- a/client/src/components/shop-employees/shop-employees.styles.scss +++ b/client/src/components/shop-employees/shop-employees.styles.scss @@ -1,16 +1,7 @@ .shop-employees-layout { - display: grid; - gap: 16px; - align-items: start; -} - -.shop-employees-layout__list, -.shop-employees-layout__details { min-width: 0; } -@media (min-width: 1700px) { - .shop-employees-layout--with-detail { - grid-template-columns: minmax(420px, 500px) minmax(0, 1fr); - } +.shop-employees-layout__list { + min-width: 0; } diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx index 41a0e0f65..00006c1f2 100644 --- a/client/src/components/shop-info/shop-info.general.component.jsx +++ b/client/src/components/shop-info/shop-info.general.component.jsx @@ -1,4 +1,4 @@ -import { DeleteFilled } from "@ant-design/icons"; +import { DeleteFilled, HolderOutlined } from "@ant-design/icons"; import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -8,10 +8,18 @@ import CurrencyInput from "../form-items-formatted/currency-form-item.component" import FormItemEmail from "../form-items-formatted/email-form-item.component"; import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.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 LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import { + INLINE_TITLE_GROUP_STYLE, + INLINE_TITLE_HANDLE_STYLE, + INLINE_TITLE_INPUT_STYLE, + INLINE_TITLE_LABEL_STYLE, + INLINE_TITLE_ROW_STYLE, + INLINE_TITLE_SEPARATOR_STYLE +} from "../layout-form-row/inline-form-row-title.utils.js"; const timeZonesList = Intl.supportedValuesOf("timeZone"); + const mapStateToProps = createStructuredSelector({}); const mapDispatchToProps = () => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) @@ -20,16 +28,6 @@ export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral); export function ShopInfoGeneral({ form }) { const { t } = useTranslation(); - const messagingPresets = Form.useWatch(["md_messaging_presets"], form) || []; - const notesPresets = Form.useWatch(["md_notes_presets"], form) || []; - const partsLocations = Form.useWatch(["md_parts_locations"], form) || []; - const insuranceCompanies = Form.useWatch(["md_ins_cos"], form) || []; - const estimators = Form.useWatch(["md_estimators"], form) || []; - const fileHandlers = Form.useWatch(["md_filehandlers"], form) || []; - const courtesyCarRates = Form.useWatch(["md_ccc_rates"], form) || []; - const joblinePresets = Form.useWatch(["md_jobline_presets"], form) || []; - const partOrderComments = Form.useWatch(["md_parts_order_comment"], form) || []; - const notificationEmails = Form.useWatch(["md_to_emails"], form) || []; return (
@@ -457,18 +455,72 @@ export function ShopInfoGeneral({ form }) { return (
{fields.map((field, index) => { - const messagingPreset = messagingPresets[field.name] || {}; - return ( + +
+
{t("bodyshop.fields.messaginglabel_short")}
+ + + +
+
+
+
{t("bodyshop.fields.messagingtext_short")}
+ + + +
+
+ } extra={
+ } + wrapTitle extra={
+ } extra={
+ } extra={ - - } - key={`${index}color`} - name={[field.name, "color"]} - > - - + /> + + + + } + {...schedulingBucketSurfaceStyles} + > +
+
+ + + + + + + + + + + +
+
+ + + +
+
); diff --git a/client/src/components/shop-info/shop-info.scheduling.styles.scss b/client/src/components/shop-info/shop-info.scheduling.styles.scss new file mode 100644 index 000000000..b04a92e15 --- /dev/null +++ b/client/src/components/shop-info/shop-info.scheduling.styles.scss @@ -0,0 +1,58 @@ +.shop-info-scheduling__bucket-card-body { + display: flex; + gap: 12px; + align-items: stretch; +} + +.shop-info-scheduling__bucket-card-fields { + flex: 1 1 0; + min-width: 0; + display: grid; + grid-template-columns: repeat(3, minmax(92px, 1fr)); + gap: 0 12px; +} + +.shop-info-scheduling__bucket-card-fields .ant-form-item { + margin-bottom: 10px; +} + +.shop-info-scheduling__bucket-card-color { + flex: 0 0 360px; + min-width: 360px; + max-width: 360px; + display: flex; + align-items: stretch; +} + +.shop-info-scheduling__bucket-card-color .ant-form-item { + margin-bottom: 0; + width: 100%; +} + +.shop-info-scheduling__bucket-card-color .ant-form-item-control, +.shop-info-scheduling__bucket-card-color .ant-form-item-control-input, +.shop-info-scheduling__bucket-card-color .ant-form-item-control-input-content { + height: 100%; +} + +@media (max-width: 1199px) { + .shop-info-scheduling__bucket-card-body { + flex-direction: column; + } + + .shop-info-scheduling__bucket-card-fields { + grid-template-columns: repeat(2, minmax(120px, 1fr)); + } + + .shop-info-scheduling__bucket-card-color { + flex-basis: auto; + min-width: 0; + max-width: none; + } +} + +@media (max-width: 575px) { + .shop-info-scheduling__bucket-card-fields { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/client/src/components/shop-info/shop-info.speedprint.component.jsx b/client/src/components/shop-info/shop-info.speedprint.component.jsx index 4b9f919a8..062bea41e 100644 --- a/client/src/components/shop-info/shop-info.speedprint.component.jsx +++ b/client/src/components/shop-info/shop-info.speedprint.component.jsx @@ -1,17 +1,22 @@ -import { DeleteFilled } from "@ant-design/icons"; +import { DeleteFilled, HolderOutlined } from "@ant-design/icons"; import { Button, Form, Input, Select, Space } from "antd"; import { useTranslation } from "react-i18next"; import { TemplateList } from "../../utils/TemplateConstants"; 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 LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import { + INLINE_TITLE_GROUP_STYLE, + INLINE_TITLE_HANDLE_STYLE, + INLINE_TITLE_INPUT_STYLE, + INLINE_TITLE_LABEL_STYLE, + INLINE_TITLE_ROW_STYLE, + INLINE_TITLE_SEPARATOR_STYLE +} from "../layout-form-row/inline-form-row-title.utils.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; export default function ShopInfoSpeedPrint() { const { t } = useTranslation(); - const form = Form.useFormInstance(); const allTemplates = TemplateList("job"); - const speedPrintItems = Form.useWatch(["speedprint"], form) || []; const TemplateListGenerated = InstanceRenderManager({ imex: Object.fromEntries(Object.entries(allTemplates).filter(([, { enhanced_payroll }]) => !enhanced_payroll)), rome: allTemplates @@ -24,18 +29,71 @@ export default function ShopInfoSpeedPrint() { return (
{fields.map((field, index) => { - const speedPrintItem = speedPrintItems[field.name] || {}; - return ( + +
+
{t("bodyshop.fields.speedprint.id")}
+ + + +
+
+
+
{t("bodyshop.fields.speedprint.label")}
+ + + +
+
+ } + wrapTitle extra={