IO-3624 Polish config empty states and admin cards

This commit is contained in:
Dave
2026-03-25 10:16:48 -04:00
parent 3aa19ec09f
commit b8246e03c1
22 changed files with 4976 additions and 4967 deletions

View File

@@ -0,0 +1,11 @@
import { useTranslation } from "react-i18next";
export default function ConfigListEmptyState({ actionLabel, minHeight = 96 }) {
const { t } = useTranslation();
return (
<div className="imex-form-row-empty-state" style={{ minHeight }}>
{t("general.labels.click_to_begin", { action: actionLabel })}
</div>
);
}

View File

@@ -1,15 +1,19 @@
export const inlineFormRowTitleStyles = Object.freeze({ export const inlineFormRowTitleStyles = Object.freeze({
input: Object.freeze({ input: Object.freeze({
background: "var(--imex-form-title-input-bg)", background: "transparent",
border: "1px solid var(--imex-form-title-input-border)", border: "none",
borderRadius: 6, borderRadius: 0,
paddingInline: 10, boxShadow: "none",
paddingBlock: 4, paddingInline: 0,
lineHeight: 1.3 paddingBlock: 0,
lineHeight: 1.35,
flex: "1 1 auto",
minWidth: 0,
width: "100%"
}), }),
row: Object.freeze({ row: Object.freeze({
display: "flex", display: "flex",
gap: 4, gap: 6,
flexWrap: "wrap", flexWrap: "wrap",
alignItems: "center", alignItems: "center",
width: "100%", width: "100%",
@@ -18,25 +22,37 @@ export const inlineFormRowTitleStyles = Object.freeze({
group: Object.freeze({ group: Object.freeze({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: 6, gap: 8,
minWidth: 0 paddingInline: 8,
paddingBlock: 4,
borderRadius: 10,
border: "1px solid var(--imex-form-title-group-border)",
background: "var(--imex-form-title-group-bg)",
minWidth: 0,
flex: "1 1 0"
}), }),
label: Object.freeze({ label: Object.freeze({
color: "var(--ant-color-text)", color: "var(--ant-color-text-secondary)",
fontSize: "var(--ant-font-size)", fontSize: 12,
fontWeight: 500, fontWeight: 600,
whiteSpace: "nowrap" lineHeight: 1,
whiteSpace: "nowrap",
paddingInline: 6,
paddingBlock: 3,
borderRadius: 999,
border: "1px solid var(--imex-form-title-label-border)",
background: "var(--imex-form-title-label-bg)"
}), }),
handle: Object.freeze({ handle: Object.freeze({
color: "var(--ant-color-text-tertiary)", color: "var(--ant-color-text-tertiary)",
fontSize: 14, fontSize: 14,
flex: "0 0 auto", flex: "0 0 auto",
marginRight: 4 marginRight: 2
}), }),
separator: Object.freeze({ separator: Object.freeze({
width: 1, width: 1,
height: 18, height: 16,
background: "color-mix(in srgb, var(--imex-form-surface-border) 78%, transparent)", background: "color-mix(in srgb, var(--imex-form-surface-border) 58%, transparent)",
borderRadius: 999, borderRadius: 999,
flex: "0 0 auto", flex: "0 0 auto",
marginInline: 2 marginInline: 2
@@ -52,6 +68,10 @@ export const inlineFormRowTitleStyles = Object.freeze({
export const INLINE_TITLE_INPUT_STYLE = inlineFormRowTitleStyles.input; export const INLINE_TITLE_INPUT_STYLE = inlineFormRowTitleStyles.input;
export const INLINE_TITLE_ROW_STYLE = inlineFormRowTitleStyles.row; export const INLINE_TITLE_ROW_STYLE = inlineFormRowTitleStyles.row;
export const INLINE_TITLE_GROUP_STYLE = inlineFormRowTitleStyles.group; export const INLINE_TITLE_GROUP_STYLE = inlineFormRowTitleStyles.group;
export const INLINE_TITLE_SWITCH_GROUP_STYLE = Object.freeze({
...inlineFormRowTitleStyles.group,
flex: "0 0 auto"
});
export const INLINE_TITLE_LABEL_STYLE = inlineFormRowTitleStyles.label; export const INLINE_TITLE_LABEL_STYLE = inlineFormRowTitleStyles.label;
export const INLINE_TITLE_HANDLE_STYLE = inlineFormRowTitleStyles.handle; export const INLINE_TITLE_HANDLE_STYLE = inlineFormRowTitleStyles.handle;
export const INLINE_TITLE_SEPARATOR_STYLE = inlineFormRowTitleStyles.separator; export const INLINE_TITLE_SEPARATOR_STYLE = inlineFormRowTitleStyles.separator;

View File

@@ -15,6 +15,10 @@
--imex-form-surface-border: #d9d9d9; /* matches AntD-ish border */ --imex-form-surface-border: #d9d9d9; /* matches AntD-ish border */
--imex-form-title-input-bg: rgba(255, 255, 255, 0.96); --imex-form-title-input-bg: rgba(255, 255, 255, 0.96);
--imex-form-title-input-border: rgba(0, 0, 0, 0.08); --imex-form-title-input-border: rgba(0, 0, 0, 0.08);
--imex-form-title-group-bg: rgba(255, 255, 255, 0.72);
--imex-form-title-group-border: rgba(0, 0, 0, 0.08);
--imex-form-title-label-bg: rgba(0, 0, 0, 0.04);
--imex-form-title-label-border: rgba(0, 0, 0, 0.06);
} }
/* Pick the selector that matches your app and remove the rest */ /* Pick the selector that matches your app and remove the rest */
@@ -24,6 +28,10 @@ html[data-theme="dark"] {
--imex-form-surface-border: rgba(5, 5, 5, 0.12); --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-bg: rgba(255, 255, 255, 0.12);
--imex-form-title-input-border: rgba(255, 255, 255, 0.2); --imex-form-title-input-border: rgba(255, 255, 255, 0.2);
--imex-form-title-group-bg: rgba(255, 255, 255, 0.08);
--imex-form-title-group-border: rgba(255, 255, 255, 0.16);
--imex-form-title-label-bg: rgba(255, 255, 255, 0.06);
--imex-form-title-label-border: rgba(255, 255, 255, 0.12);
} }
.imex-form-row { .imex-form-row {
@@ -114,6 +122,20 @@ html[data-theme="dark"] {
background: var(--imex-form-surface); background: var(--imex-form-surface);
} }
.ant-card-actions {
background: var(--imex-form-surface-head);
border-top-color: var(--imex-form-surface-border);
}
.ant-card-actions > li {
margin: 10px 0;
padding-inline: 12px;
}
.ant-card-actions .ant-btn {
width: 100%;
}
.ant-form-item:last-child { .ant-form-item:last-child {
margin-bottom: 4px; margin-bottom: 4px;
} }
@@ -156,3 +178,14 @@ html[data-theme="dark"] {
} }
} }
} }
.imex-form-row-empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
text-align: center;
color: var(--ant-color-text-description);
font-size: var(--ant-font-size);
line-height: 1.5;
}

View File

@@ -27,6 +27,7 @@ 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 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 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,
@@ -35,6 +36,7 @@ import {
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,
INLINE_TITLE_TEXT_STYLE INLINE_TITLE_TEXT_STYLE
} from "../layout-form-row/inline-form-row-title.utils.js"; } from "../layout-form-row/inline-form-row-title.utils.js";
import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component"; import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component";
@@ -166,6 +168,8 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
key: "actions", key: "actions",
render: (text, record) => ( render: (text, record) => (
<Button <Button
type="text"
danger
onClick={async () => { onClick={async () => {
await deleteVacation({ await deleteVacation({
variables: { id: record.id }, variables: { id: record.id },
@@ -195,8 +199,8 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
<Card <Card
title={employeeCardTitle} title={employeeCardTitle}
extra={ extra={
<Button type="primary" onClick={() => form.submit()}> <Button type="primary" onClick={() => form.submit()} style={{ minWidth: 170 }}>
{t("general.actions.save")} {t("employees.actions.save_employee")}
</Button> </Button>
} }
> >
@@ -229,8 +233,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} /> <div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
<div <div
style={{ style={{
...INLINE_TITLE_GROUP_STYLE, ...INLINE_TITLE_SWITCH_GROUP_STYLE
flex: "0 0 auto"
}} }}
> >
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employees.labels.active")}</div> <div style={INLINE_TITLE_LABEL_STYLE}>{t("employees.labels.active")}</div>
@@ -241,8 +244,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} /> <div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
<div <div
style={{ style={{
...INLINE_TITLE_GROUP_STYLE, ...INLINE_TITLE_SWITCH_GROUP_STYLE
flex: "0 0 auto"
}} }}
> >
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employees.fields.flat_rate")}</div> <div style={INLINE_TITLE_LABEL_STYLE}>{t("employees.fields.flat_rate")}</div>
@@ -396,147 +398,141 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
</Col> </Col>
</Row> </Row>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow title={t("bodyshop.labels.employee_rates")}> <Form.List name={["rates"]}>
<Form.List name={["rates"]}> {(fields, { add, remove, move }) => {
{(fields, { add, remove, move }) => { return (
return ( <LayoutFormRow
title={t("bodyshop.labels.employee_rates")}
actions={[
<Button
key="add-rate"
type="primary"
block
onClick={() => {
add();
}}
id="add-employee-rate-button"
>
<span id="new-employee-rate">{t("employees.actions.addrate")}</span>
</Button>
]}
>
<div> <div>
{fields.map((field, index) => { {fields.length === 0 ? (
return ( <ConfigListEmptyState actionLabel={t("employees.actions.addrate")} />
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}> ) : (
<LayoutFormRow fields.map((field, index) => {
noDivider return (
titleOnly <Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
title={ <LayoutFormRow
<div style={INLINE_TITLE_ROW_STYLE}> noDivider
<HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} /> title={
<div <div style={INLINE_TITLE_ROW_STYLE}>
style={{ <HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} />
...INLINE_TITLE_GROUP_STYLE, <div style={INLINE_TITLE_GROUP_STYLE}>
flex: "1 1 260px" <div style={INLINE_TITLE_LABEL_STYLE}>{t("employees.fields.cost_center")}</div>
}} <Form.Item
> noStyle
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employees.fields.cost_center")}</div> name={[field.name, "cost_center"]}
<Form.Item rules={[
noStyle {
name={[field.name, "cost_center"]} required: true
rules={[ }
{
required: true
}
]}
>
<Select
size="small"
options={[
{ value: "timetickets.labels.shift", label: t("timetickets.labels.shift") },
...(bodyshop.cdk_dealerid ||
bodyshop.pbs_serialnumber ||
bodyshop.rr_dealerid ||
Enhanced_Payroll.treatment === "on"
? CiecaSelect(false, true)
: bodyshop.md_responsibility_centers.costs.map((c) => ({
value: c.name,
label: c.name
})))
]} ]}
style={{ width: "100%" }} >
styles={{ <Select
selector: INLINE_TITLE_INPUT_STYLE size="small"
}} options={[
/> { value: "timetickets.labels.shift", label: t("timetickets.labels.shift") },
</Form.Item> ...(bodyshop.cdk_dealerid ||
bodyshop.pbs_serialnumber ||
bodyshop.rr_dealerid ||
Enhanced_Payroll.treatment === "on"
? CiecaSelect(false, true)
: bodyshop.md_responsibility_centers.costs.map((c) => ({
value: c.name,
label: c.name
})))
]}
style={{ width: "100%" }}
styles={{
selector: INLINE_TITLE_INPUT_STYLE
}}
/>
</Form.Item>
</div>
</div> </div>
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} /> }
<div wrapTitle
style={{ extra={
...INLINE_TITLE_GROUP_STYLE, <Space align="center" size="small">
flex: "0 1 190px" <Button
}} type="text"
> danger
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employees.fields.rate")}</div> icon={<DeleteFilled />}
<Form.Item onClick={() => {
noStyle remove(field.name);
name={[field.name, "rate"]} }}
rules={[ />
{ <FormListMoveArrows
required: true move={move}
} index={index}
]} total={fields.length}
> orientation="horizontal"
<InputNumber />
min={0} </Space>
precision={2} }
size="small" >
style={{ <Form.Item
...INLINE_TITLE_INPUT_STYLE, label={t("employees.fields.rate")}
width: "100%" name={[field.name, "rate"]}
}} rules={[
/> {
</Form.Item> required: true
</div> }
</div> ]}
} style={{ marginBottom: 0 }}
wrapTitle >
extra={ <InputNumber min={0} precision={2} style={{ width: "100%" }} />
<Space align="center" size="small"> </Form.Item>
<Button </LayoutFormRow>
type="text" </Form.Item>
icon={<DeleteFilled />} );
onClick={() => { })
remove(field.name); )}
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
orientation="horizontal"
/>
</Space>
}
/>
</Form.Item>
);
})}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
id="add-employee-rate-button"
>
<span id="new-employee-rate">{t("employees.actions.newrate")}</span>
</Button>
</Form.Item>
</div> </div>
); </LayoutFormRow>
}} );
</Form.List> }}
</LayoutFormRow> </Form.List>
</Form> </Form>
<LayoutFormRow title={t("bodyshop.labels.employee_vacation")}> <LayoutFormRow
<div> title={t("bodyshop.labels.employee_vacation")}
<ResponsiveTable actions={[
columns={columns} <ShopEmployeeAddVacation
mobileColumnKeys={["start", "length", "actions"]} key="add-vacation"
rowKey={"id"} employee={data && data.employees_by_pk}
dataSource={data?.employees_by_pk?.employee_vacations ?? []} buttonProps={{
pagination={false} type: "primary",
block: true
}}
/> />
<div style={{ marginTop: 12 }}> ]}
<ShopEmployeeAddVacation >
employee={data && data.employees_by_pk} {(data?.employees_by_pk?.employee_vacations ?? []).length === 0 ? (
buttonProps={{ <ConfigListEmptyState actionLabel={t("employees.actions.addvacation")} />
type: "dashed", ) : (
block: true <div>
}} <ResponsiveTable
columns={columns}
mobileColumnKeys={["start", "length", "actions"]}
rowKey={"id"}
dataSource={data?.employees_by_pk?.employee_vacations ?? []}
pagination={false}
/> />
</div> </div>
</div> )}
</LayoutFormRow> </LayoutFormRow>
</Card> </Card>
); );

View File

@@ -4,6 +4,8 @@ import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx";
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 }) {
@@ -16,13 +18,28 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
filteredInfo: { text: "" } filteredInfo: { text: "" }
}); });
const navigateToEmployee = (employeeId) => {
history({
search: queryString.stringify({
...search,
employeeId
})
});
};
const clearEmployeeSelection = () => {
const { employeeId, ...nextSearch } = search;
void employeeId;
history({
search: queryString.stringify(nextSearch)
});
};
const handleOnRowClick = (record) => { const handleOnRowClick = (record) => {
if (record) { if (record) {
search.employeeId = record.id; navigateToEmployee(record.id);
history({ search: queryString.stringify(search) });
} else { } else {
delete search.employeeId; clearEmployeeSelection();
history({ search: queryString.stringify(search) });
} }
}; };
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
@@ -89,44 +106,39 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
} }
]; ];
return ( return (
<div> <LayoutFormRow
<ResponsiveTable title={t("bodyshop.labels.employees")}
title={() => { actions={[
return ( <Button key="new-employee" type="primary" block onClick={() => navigateToEmployee("new")}>
<Button {t("employees.actions.new")}
type="primary" </Button>
onClick={() => { ]}
search.employeeId = "new"; >
history({ search: queryString.stringify(search) }); {employees.length === 0 ? (
}} <ConfigListEmptyState actionLabel={t("employees.actions.new")} />
> ) : (
{t("employees.actions.new")} <ResponsiveTable
</Button> loading={loading}
); pagination={{ placement: "top" }}
}} columns={columns}
loading={loading} mobileColumnKeys={["employee_number", "employee_name", "active"]}
pagination={{ placement: "top" }} rowKey="id"
columns={columns} dataSource={employees}
mobileColumnKeys={["employee_number", "employee_name", "active"]} rowSelection={{
rowKey="id" onSelect: (props) => navigateToEmployee(props.id),
dataSource={employees} type: "radio",
rowSelection={{ selectedRowKeys: [search.employeeId]
onSelect: (props) => { }}
search.employeeId = props.id; onChange={handleTableChange}
history({ search: queryString.stringify(search) }); onRow={(record) => {
}, return {
type: "radio", onClick: () => {
selectedRowKeys: [search.employeeId] handleOnRowClick(record);
}} }
onChange={handleTableChange} };
onRow={(record) => { }}
return { />
onClick: () => { )}
handleOnRowClick(record); </LayoutFormRow>
}
};
}}
/>
</div>
); );
} }

View File

@@ -163,8 +163,14 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
<Card <Card
title={<ShopInfoSectionNavigator tabsRef={tabsRef} activeTabKey={activeTabKey} />} title={<ShopInfoSectionNavigator tabsRef={tabsRef} activeTabKey={activeTabKey} />}
extra={ extra={
<Button type="primary" loading={saveLoading} onClick={() => form.submit()} id="shop-info-save-button"> <Button
{t("general.actions.save")} type="primary"
loading={saveLoading}
onClick={() => form.submit()}
id="shop-info-save-button"
style={{ minWidth: 210 }}
>
{t("bodyshop.actions.save_shop_information")}
</Button> </Button>
} }
> >

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ import styled from "styled-components";
import { TemplateList } from "../../utils/TemplateConstants"; 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 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,
@@ -12,7 +13,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";
const SelectorDiv = styled.div` const SelectorDiv = styled.div`
@@ -87,320 +89,318 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
</SelectorDiv> </SelectorDiv>
<LayoutFormRow header={t("bodyshop.labels.intakechecklist")} id="intakechecklist"> <Form.List name={["intakechecklist", "form"]}>
<Form.List name={["intakechecklist", "form"]}> {(fields, { add, remove, move }) => {
{(fields, { add, remove, move }) => { return (
return ( <LayoutFormRow
header={t("bodyshop.labels.intakechecklist")}
id="intakechecklist"
actions={[
<Button
key="add-intake-checklist-item"
type="primary"
block
onClick={() => {
add();
}}
>
{t("bodyshop.actions.add_intake_checklist_item")}
</Button>
]}
>
<div> <div>
{fields.map((field, index) => { {fields.length === 0 ? (
return ( <ConfigListEmptyState actionLabel={t("bodyshop.actions.add_intake_checklist_item")} />
<Form.Item key={field.key}> ) : (
<LayoutFormRow fields.map((field, index) => {
noDivider return (
title={ <Form.Item key={field.key}>
<div style={INLINE_TITLE_ROW_STYLE}> <LayoutFormRow
<HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} /> noDivider
<div title={
style={{ <div style={INLINE_TITLE_ROW_STYLE}>
...INLINE_TITLE_GROUP_STYLE, <HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} />
flex: "1 1 320px" <div style={INLINE_TITLE_GROUP_STYLE}>
}} <div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.intake.name")}</div>
>
<div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.intake.name")}</div>
<Form.Item
noStyle
name={[field.name, "name"]}
rules={[
{
required: true
}
]}
>
<Input
size="small"
placeholder={t("jobs.fields.intake.name")}
style={{
...INLINE_TITLE_INPUT_STYLE,
width: "100%"
}}
/>
</Form.Item>
</div>
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
<div
style={{
...INLINE_TITLE_GROUP_STYLE,
flex: "0 0 auto"
}}
>
<div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.intake.required")}</div>
<Form.Item noStyle name={[field.name, "required"]} valuePropName="checked">
<Switch />
</Form.Item>
</div>
</div>
}
wrapTitle
extra={
<Space align="center" size="small">
<Button
type="text"
icon={<DeleteFilled />}
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
orientation="horizontal"
/>
</Space>
}
>
<Form.Item
label={t("jobs.fields.intake.type")}
key={`${index}type`}
name={[field.name, "type"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} />
</Form.Item>
<Form.Item
label={t("jobs.fields.intake.label")}
key={`${index}label`}
name={[field.name, "label"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
if (form.getFieldValue(["intakechecklist", "form", index, "type"]) !== "slider")
return null;
return (
<>
<Form.Item <Form.Item
label={t("jobs.fields.intake.min")} noStyle
key={`${index}min`} name={[field.name, "name"]}
name={[field.name, "min"]}
dependencies={[[field.name, "type"]]}
rules={[ rules={[
{ {
required: true required: true
//message: t("general.validation.required"),
} }
]} ]}
> >
<InputNumber /> <Input
size="small"
placeholder={t("jobs.fields.intake.name")}
style={{
...INLINE_TITLE_INPUT_STYLE,
width: "100%"
}}
/>
</Form.Item> </Form.Item>
</div>
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
<div style={INLINE_TITLE_SWITCH_GROUP_STYLE}>
<div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.intake.required")}</div>
<Form.Item noStyle name={[field.name, "required"]} valuePropName="checked">
<Switch />
</Form.Item>
</div>
</div>
}
wrapTitle
extra={
<Space align="center" size="small">
<Button
type="text"
danger
icon={<DeleteFilled />}
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
orientation="horizontal"
/>
</Space>
}
>
<Form.Item
label={t("jobs.fields.intake.type")}
key={`${index}type`}
name={[field.name, "type"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} />
</Form.Item>
<Form.Item
label={t("jobs.fields.intake.label")}
key={`${index}label`}
name={[field.name, "label"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
if (form.getFieldValue(["intakechecklist", "form", index, "type"]) !== "slider")
return null;
return (
<>
<Form.Item
label={t("jobs.fields.intake.min")}
key={`${index}min`}
name={[field.name, "min"]}
dependencies={[[field.name, "type"]]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("jobs.fields.intake.max")}
key={`${index}max`}
name={[field.name, "max"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<InputNumber />
</Form.Item>
</>
);
}}
</Form.Item>
</LayoutFormRow>
</Form.Item>
);
})
)}
</div>
</LayoutFormRow>
);
}}
</Form.List>
<Form.List name={["deliverchecklist", "form"]}>
{(fields, { add, remove, move }) => {
return (
<LayoutFormRow
header={t("bodyshop.labels.deliverchecklist")}
id="deliverchecklist"
actions={[
<Button
key="add-delivery-checklist-item"
type="primary"
block
onClick={() => {
add();
}}
>
{t("bodyshop.actions.add_delivery_checklist_item")}
</Button>
]}
>
<div>
{fields.length === 0 ? (
<ConfigListEmptyState actionLabel={t("bodyshop.actions.add_delivery_checklist_item")} />
) : (
fields.map((field, index) => {
return (
<Form.Item key={field.key}>
<LayoutFormRow
noDivider
title={
<div style={INLINE_TITLE_ROW_STYLE}>
<HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} />
<div style={INLINE_TITLE_GROUP_STYLE}>
<div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.intake.name")}</div>
<Form.Item <Form.Item
label={t("jobs.fields.intake.max")} noStyle
key={`${index}max`} name={[field.name, "name"]}
name={[field.name, "max"]}
rules={[ rules={[
{ {
required: true required: true
//message: t("general.validation.required"),
} }
]} ]}
> >
<InputNumber /> <Input
size="small"
placeholder={t("jobs.fields.intake.name")}
style={{
...INLINE_TITLE_INPUT_STYLE,
width: "100%"
}}
/>
</Form.Item> </Form.Item>
</> </div>
); <div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
}} <div style={INLINE_TITLE_SWITCH_GROUP_STYLE}>
</Form.Item> <div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.intake.required")}</div>
</LayoutFormRow> <Form.Item noStyle name={[field.name, "required"]} valuePropName="checked">
</Form.Item> <Switch />
); </Form.Item>
})} </div>
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.deliverchecklist")} id="deliverchecklist">
<Form.List name={["deliverchecklist", "form"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => {
return (
<Form.Item key={field.key}>
<LayoutFormRow
noDivider
title={
<div style={INLINE_TITLE_ROW_STYLE}>
<HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} />
<div
style={{
...INLINE_TITLE_GROUP_STYLE,
flex: "1 1 320px"
}}
>
<div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.intake.name")}</div>
<Form.Item
noStyle
name={[field.name, "name"]}
rules={[
{
required: true
}
]}
>
<Input
size="small"
placeholder={t("jobs.fields.intake.name")}
style={{
...INLINE_TITLE_INPUT_STYLE,
width: "100%"
}}
/>
</Form.Item>
</div> </div>
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} /> }
<div wrapTitle
style={{ extra={
...INLINE_TITLE_GROUP_STYLE, <Space align="center" size="small">
flex: "0 0 auto" <Button
}} type="text"
> danger
<div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.intake.required")}</div> icon={<DeleteFilled />}
<Form.Item noStyle name={[field.name, "required"]} valuePropName="checked"> onClick={() => {
<Switch /> remove(field.name);
</Form.Item> }}
</div> />
</div> <FormListMoveArrows
} move={move}
wrapTitle index={index}
extra={ total={fields.length}
<Space align="center" size="small"> orientation="horizontal"
<Button />
type="text" </Space>
icon={<DeleteFilled />} }
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
orientation="horizontal"
/>
</Space>
}
>
<Form.Item
label={t("jobs.fields.intake.type")}
key={`${index}typed`}
name={[field.name, "type"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
> >
<Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} /> <Form.Item
</Form.Item> label={t("jobs.fields.intake.type")}
key={`${index}typed`}
name={[field.name, "type"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} />
</Form.Item>
<Form.Item <Form.Item
label={t("jobs.fields.intake.label")} label={t("jobs.fields.intake.label")}
key={`${index}labeld`} key={`${index}labeld`}
name={[field.name, "label"]} name={[field.name, "label"]}
rules={[ rules={[
{ {
required: true required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
} }
]} ]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item shouldUpdate> <Form.Item shouldUpdate>
{() => { {() => {
if (form.getFieldValue(["deliverchecklist", "form", index, "type"]) !== "slider") if (form.getFieldValue(["deliverchecklist", "form", index, "type"]) !== "slider")
return null; return null;
return ( return (
<> <>
<Form.Item <Form.Item
label={t("jobs.fields.intake.min")} label={t("jobs.fields.intake.min")}
key={`${index}mind`} key={`${index}mind`}
name={[field.name, "min"]} name={[field.name, "min"]}
dependencies={[[field.name, "type"]]} dependencies={[[field.name, "type"]]}
rules={[ rules={[
{ {
required: form.getFieldValue([field.name, "type"]) === "slider" required: form.getFieldValue([field.name, "type"]) === "slider"
//message: t("general.validation.required"), //message: t("general.validation.required"),
} }
]} ]}
> >
<InputNumber /> <InputNumber />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t("jobs.fields.intake.max")} label={t("jobs.fields.intake.max")}
key={`${index}maxd`} key={`${index}maxd`}
name={[field.name, "max"]} name={[field.name, "max"]}
dependencies={[[field.name, "type"]]} dependencies={[[field.name, "type"]]}
rules={[ rules={[
{ {
required: form.getFieldValue([field.name, "type"]) === "slider" required: form.getFieldValue([field.name, "type"]) === "slider"
//message: t("general.validation.required"), //message: t("general.validation.required"),
} }
]} ]}
> >
<InputNumber /> <InputNumber />
</Form.Item> </Form.Item>
</> </>
); );
}} }}
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
</Form.Item> </Form.Item>
); );
})} })
<Form.Item> )}
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div> </div>
); </LayoutFormRow>
}} );
</Form.List> }}
</LayoutFormRow> </Form.List>
</div> </div>
); );
} }

View File

@@ -3,6 +3,7 @@ import { Button, Form, Input, Space } from "antd";
import { useTranslation } from "react-i18next"; 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 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,
@@ -25,361 +26,364 @@ export default function ShopInfoLaborRates() {
<CurrencyInput min={0} /> <CurrencyInput min={0} />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.laborrates")}> <Form.List name={["md_labor_rates"]}>
<Form.List name={["md_labor_rates"]}> {(fields, { add, remove, move }) => {
{(fields, { add, remove, move }) => { return (
return ( <LayoutFormRow
header={t("bodyshop.labels.laborrates")}
actions={[
<Button
key="add-labor-rate"
type="primary"
block
onClick={() => {
add();
}}
>
{t("bodyshop.actions.newlaborrate")}
</Button>
]}
>
<div> <div>
{fields.map((field, index) => { {fields.length === 0 ? (
return ( <ConfigListEmptyState actionLabel={t("bodyshop.actions.newlaborrate")} />
<Form.Item key={field.key}> ) : (
<LayoutFormRow fields.map((field, index) => {
noDivider={index === 0} return (
title={ <Form.Item key={field.key}>
<div style={INLINE_TITLE_ROW_STYLE}> <LayoutFormRow
<HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} /> noDivider={index === 0}
<div title={
style={{ <div style={INLINE_TITLE_ROW_STYLE}>
...INLINE_TITLE_GROUP_STYLE, <HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} />
flex: "1 1 340px" <div style={INLINE_TITLE_GROUP_STYLE}>
}} <div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.labor_rate_desc")}</div>
> <Form.Item
<div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.labor_rate_desc")}</div> noStyle
<Form.Item name={[field.name, "rate_label"]}
noStyle rules={[
name={[field.name, "rate_label"]} {
rules={[ required: true
{ }
required: true ]}
} >
]} <Input
> size="small"
<Input placeholder={t("jobs.fields.labor_rate_desc")}
size="small" style={{
placeholder={t("jobs.fields.labor_rate_desc")} ...INLINE_TITLE_INPUT_STYLE,
style={{ width: "100%"
...INLINE_TITLE_INPUT_STYLE, }}
width: "100%" />
}} </Form.Item>
/> </div>
</Form.Item>
</div> </div>
</div> }
} wrapTitle
wrapTitle extra={
extra={ <Space align="center" size="small">
<Space align="center" size="small"> <Button
<Button type="text"
type="text" danger
icon={<DeleteFilled />} icon={<DeleteFilled />}
onClick={() => { onClick={() => {
remove(field.name); remove(field.name);
}} }}
/> />
<FormListMoveArrows <FormListMoveArrows
move={move} move={move}
index={index} index={index}
total={fields.length} total={fields.length}
orientation="horizontal" orientation="horizontal"
/> />
</Space> </Space>
} }
>
<Form.Item
label={t("jobs.fields.rate_laa")}
key={`${index}rate_laa`}
name={[field.name, "rate_laa"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
> >
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_laa")}
<Form.Item key={`${index}rate_laa`}
label={t("jobs.fields.rate_lab")} name={[field.name, "rate_laa"]}
key={`${index}rate_lab`} rules={[
name={[field.name, "rate_lab"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_lab")}
<Form.Item key={`${index}rate_lab`}
label={t("jobs.fields.rate_lad")} name={[field.name, "rate_lab"]}
key={`${index}rate_lad`} rules={[
name={[field.name, "rate_lad"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_lad")}
<Form.Item key={`${index}rate_lad`}
label={t("jobs.fields.rate_lae")} name={[field.name, "rate_lad"]}
key={`${index}rate_lae`} rules={[
name={[field.name, "rate_lae"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_lae")}
<Form.Item key={`${index}rate_lae`}
label={t("jobs.fields.rate_laf")} name={[field.name, "rate_lae"]}
key={`${index}rate_laf`} rules={[
name={[field.name, "rate_laf"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_laf")}
<Form.Item key={`${index}rate_laf`}
label={t("jobs.fields.rate_lag")} name={[field.name, "rate_laf"]}
key={`${index}rate_lag`} rules={[
name={[field.name, "rate_lag"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_lag")}
<Form.Item key={`${index}rate_lag`}
label={t("jobs.fields.rate_lam")} name={[field.name, "rate_lag"]}
key={`${index}rate_lam`} rules={[
name={[field.name, "rate_lam"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_lam")}
<Form.Item key={`${index}rate_lam`}
label={t("jobs.fields.rate_lar")} name={[field.name, "rate_lam"]}
key={`${index}rate_lar`} rules={[
name={[field.name, "rate_lar"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_lar")}
<Form.Item key={`${index}rate_lar`}
label={t("jobs.fields.rate_las")} name={[field.name, "rate_lar"]}
key={`${index}rate_las`} rules={[
name={[field.name, "rate_las"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_las")}
<Form.Item key={`${index}rate_las`}
label={t("jobs.fields.rate_la1")} name={[field.name, "rate_las"]}
key={`${index}rate_la1`} rules={[
name={[field.name, "rate_la1"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_la1")}
<Form.Item key={`${index}rate_la1`}
label={t("jobs.fields.rate_la2")} name={[field.name, "rate_la1"]}
key={`${index}rate_la2`} rules={[
name={[field.name, "rate_la2"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_la2")}
<Form.Item key={`${index}rate_la2`}
label={t("jobs.fields.rate_la3")} name={[field.name, "rate_la2"]}
key={`${index}rate_la3`} rules={[
name={[field.name, "rate_la3"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_la3")}
<Form.Item key={`${index}rate_la3`}
label={t("jobs.fields.rate_la4")} name={[field.name, "rate_la3"]}
key={`${index}rate_la4`} rules={[
name={[field.name, "rate_la4"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_la4")}
<Form.Item key={`${index}rate_la4`}
label={t("jobs.fields.rate_mash")} name={[field.name, "rate_la4"]}
key={`${index}rate_mash`} rules={[
name={[field.name, "rate_mash"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_mash")}
<Form.Item key={`${index}rate_mash`}
label={t("jobs.fields.rate_mapa")} name={[field.name, "rate_mash"]}
key={`${index}rate_mapa`} rules={[
name={[field.name, "rate_mapa"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_mapa")}
<Form.Item key={`${index}rate_mapa`}
label={t("jobs.fields.rate_ma2s")} name={[field.name, "rate_mapa"]}
key={`${index}rate_ma2s`} rules={[
name={[field.name, "rate_ma2s"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_ma2s")}
<Form.Item key={`${index}rate_ma2s`}
label={t("jobs.fields.rate_ma3s")} name={[field.name, "rate_ma2s"]}
key={`${index}rate_ma3s`} rules={[
name={[field.name, "rate_ma3s"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_ma3s")}
{ key={`${index}rate_ma3s`}
// <Form.Item name={[field.name, "rate_ma3s"]}
// label={t("jobs.fields.rate_mabl")} rules={[
// key={`${index}rate_mabl`} {
// name={[field.name, "rate_mabl"]} required: true
// rules={[ //message: t("general.validation.required"),
// { }
// required: true, ]}
// //message: t("general.validation.required"), >
// }, <CurrencyInput min={0} />
// ]} </Form.Item>
// > {
// <CurrencyInput min={0} /> // <Form.Item
// </Form.Item> // label={t("jobs.fields.rate_mabl")}
// <Form.Item // key={`${index}rate_mabl`}
// label={t("jobs.fields.rate_macs")} // name={[field.name, "rate_mabl"]}
// key={`${index}rate_macs`} // rules={[
// name={[field.name, "rate_macs"]} // {
// rules={[ // required: true,
// { // //message: t("general.validation.required"),
// required: true, // },
// //message: t("general.validation.required"), // ]}
// }, // >
// ]} // <CurrencyInput min={0} />
// > // </Form.Item>
// <CurrencyInput min={0} /> // <Form.Item
// </Form.Item> // label={t("jobs.fields.rate_macs")}
} // key={`${index}rate_macs`}
<Form.Item // name={[field.name, "rate_macs"]}
label={t("jobs.fields.rate_matd")} // rules={[
key={`${index}rate_matd`} // {
name={[field.name, "rate_matd"]} // required: true,
rules={[ // //message: t("general.validation.required"),
{ // },
required: true // ]}
//message: t("general.validation.required"), // >
} // <CurrencyInput min={0} />
]} // </Form.Item>
> }
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_matd")}
<Form.Item key={`${index}rate_matd`}
label={t("jobs.fields.rate_mahw")} name={[field.name, "rate_matd"]}
key={`${index}rate_mahw`} rules={[
name={[field.name, "rate_mahw"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <CurrencyInput min={0} />
> </Form.Item>
<CurrencyInput min={0} /> <Form.Item
</Form.Item> label={t("jobs.fields.rate_mahw")}
</LayoutFormRow> key={`${index}rate_mahw`}
</Form.Item> name={[field.name, "rate_mahw"]}
); rules={[
})} {
<Form.Item> required: true
<Button //message: t("general.validation.required"),
type="dashed" }
onClick={() => { ]}
add(); >
}} <CurrencyInput min={0} />
style={{ width: "100%" }} </Form.Item>
> </LayoutFormRow>
{t("bodyshop.actions.newlaborrate")} </Form.Item>
</Button> );
</Form.Item> })
)}
</div> </div>
); </LayoutFormRow>
}} );
</Form.List> }}
</LayoutFormRow> </Form.List>
</> </>
); );
} }

View File

@@ -3,6 +3,7 @@ import { Button, Col, Form, Input, Row, Select, Space, Switch } from "antd";
import { useMemo } from "react"; 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 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,
@@ -10,7 +11,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 i18n from "i18next"; import i18n from "i18next";
@@ -76,229 +78,221 @@ export default function ShopInfoPartsScan({ form }) {
return ( return (
<div> <div>
<LayoutFormRow header={t("bodyshop.labels.md_parts_scan")}> <Form.List name={["md_parts_scan"]}>
<Form.List name={["md_parts_scan"]}> {(fields, { add, remove, move }) => (
{(fields, { add, remove, move }) => ( <LayoutFormRow
header={t("bodyshop.labels.md_parts_scan")}
actions={[
<Button
key="add-parts-scan-rule"
type="primary"
block
onClick={() =>
add({
field: "line_desc",
operation: "contains",
mark_critical: true,
caseInsensitive: true
})
}
>
{t("bodyshop.actions.addpartsrule")}
</Button>
]}
>
<div> <div>
{fields.map((field, index) => { {fields.length === 0 ? (
const selectedField = watchedFields?.[index]?.field || "line_desc"; <ConfigListEmptyState actionLabel={t("bodyshop.actions.addpartsrule")} />
const fieldType = getFieldType(selectedField); ) : (
fields.map((field, index) => {
const selectedField = watchedFields?.[index]?.field || "line_desc";
const fieldType = getFieldType(selectedField);
return ( return (
<Form.Item key={field.key}> <Form.Item key={field.key}>
<LayoutFormRow <LayoutFormRow
noDivider noDivider
title={ title={
<div style={INLINE_TITLE_ROW_STYLE}> <div style={INLINE_TITLE_ROW_STYLE}>
<HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} /> <HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} />
<div <div style={INLINE_TITLE_GROUP_STYLE}>
style={{ <div style={INLINE_TITLE_LABEL_STYLE}>{t("bodyshop.fields.md_parts_scan.field")}</div>
...INLINE_TITLE_GROUP_STYLE, <Form.Item
flex: "1 1 260px" noStyle
}} name={[field.name, "field"]}
> rules={[
<div style={INLINE_TITLE_LABEL_STYLE}>{t("bodyshop.fields.md_parts_scan.field")}</div> {
required: true,
message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.field")
})
}
]}
>
<Select
options={fieldSelectOptions}
onChange={() => {
form.setFields([
{ name: ["md_parts_scan", index, "operation"], value: "contains" },
{ name: ["md_parts_scan", index, "value"], value: undefined }
]);
}}
style={{
width: "100%"
}}
styles={{
selector: INLINE_TITLE_INPUT_STYLE
}}
size="small"
/>
</Form.Item>
</div>
{fieldType === "string" && (
<>
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
<div style={INLINE_TITLE_SWITCH_GROUP_STYLE}>
<div style={INLINE_TITLE_LABEL_STYLE}>
{t("bodyshop.fields.md_parts_scan.caseInsensitive")}
</div>
<Form.Item noStyle name={[field.name, "caseInsensitive"]} valuePropName="checked">
<Switch />
</Form.Item>
</div>
</>
)}
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
<div style={INLINE_TITLE_SWITCH_GROUP_STYLE}>
<div style={INLINE_TITLE_LABEL_STYLE}>
{t("bodyshop.fields.md_parts_scan.mark_critical")}
</div>
<Form.Item noStyle name={[field.name, "mark_critical"]} valuePropName="checked">
<Switch />
</Form.Item>
</div>
</div>
}
wrapTitle
extra={
<Space align="center" size="small">
<Button
type="text"
danger
icon={<DeleteFilled />}
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
orientation="horizontal"
/>
</Space>
}
>
<Row gutter={[16, 16]} align="middle">
{/* Operation */}
{fieldType !== "predefined" && fieldType && (
<Col span={6}>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.operation")}
name={[field.name, "operation"]}
rules={[
{
required: true,
message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.operation")
})
}
]}
>
<Select options={operationOptions[fieldType]} />
</Form.Item>
</Col>
)}
{/* Value */}
{fieldType && (
<Col span={6}>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.value")}
name={[field.name, "value"]}
rules={[
{
required: true,
message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.value")
})
}
]}
>
{fieldType === "predefined" ? (
<Select
options={
selectedField === "part_type"
? predefinedPartTypes.map((type) => ({
label: type,
value: type
}))
: predefinedModLbrTypes.map((type) => ({
label: type,
value: type
}))
}
/>
) : (
<Input />
)}
</Form.Item>
</Col>
)}
{/* Update Field */}
<Col span={4}>
<Form.Item <Form.Item
noStyle label={t("bodyshop.fields.md_parts_scan.update_field")}
name={[field.name, "field"]} name={[field.name, "update_field"]}
rules={[
{
required: true,
message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.field")
})
}
]}
> >
<Select <Select
options={fieldSelectOptions} options={fieldSelectOptions}
onChange={() => { allowClear
form.setFields([ onClear={() =>
{ name: ["md_parts_scan", index, "operation"], value: "contains" }, form.setFields([{ name: ["md_parts_scan", index, "update_field"], value: null }])
{ name: ["md_parts_scan", index, "value"], value: undefined } }
]);
}}
style={{
width: "100%"
}}
styles={{
selector: INLINE_TITLE_INPUT_STYLE
}}
size="small"
/> />
</Form.Item> </Form.Item>
</div> </Col>
{fieldType === "string" && (
<> {/* Update Field */}
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} /> <Col span={4}>
<div
style={{
...INLINE_TITLE_GROUP_STYLE,
flex: "0 0 auto"
}}
>
<div style={INLINE_TITLE_LABEL_STYLE}>
{t("bodyshop.fields.md_parts_scan.caseInsensitive")}
</div>
<Form.Item noStyle name={[field.name, "caseInsensitive"]} valuePropName="checked">
<Switch />
</Form.Item>
</div>
</>
)}
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
<div
style={{
...INLINE_TITLE_GROUP_STYLE,
flex: "0 0 auto"
}}
>
<div style={INLINE_TITLE_LABEL_STYLE}>
{t("bodyshop.fields.md_parts_scan.mark_critical")}
</div>
<Form.Item noStyle name={[field.name, "mark_critical"]} valuePropName="checked">
<Switch />
</Form.Item>
</div>
</div>
}
wrapTitle
extra={
<Space align="center" size="small">
<Button
type="text"
icon={<DeleteFilled />}
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
orientation="horizontal"
/>
</Space>
}
>
<Row gutter={[16, 16]} align="middle">
{/* Operation */}
{fieldType !== "predefined" && fieldType && (
<Col span={6}>
<Form.Item <Form.Item
label={t("bodyshop.fields.md_parts_scan.operation")} label={t("bodyshop.fields.md_parts_scan.update_value")}
name={[field.name, "operation"]} name={[field.name, "update_value"]}
dependencies={[["md_parts_scan", index, "update_field"]]}
tooltip={t("bodyshop.tooltips.md_parts_scan.update_value_tooltip")}
rules={[ rules={[
{ {
required: true, required: form.getFieldValue(["md_parts_scan", index, "update_field"]),
message: t("general.validation.required", { message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.operation") label: t("bodyshop.fields.md_parts_scan.update_value")
}) })
} }
]} ]}
> >
<Select options={operationOptions[fieldType]} /> <Input />
</Form.Item> </Form.Item>
</Col> </Col>
)} </Row>
</LayoutFormRow>
{/* Value */} </Form.Item>
{fieldType && ( );
<Col span={6}> })
<Form.Item )}
label={t("bodyshop.fields.md_parts_scan.value")}
name={[field.name, "value"]}
rules={[
{
required: true,
message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.value")
})
}
]}
>
{fieldType === "predefined" ? (
<Select
options={
selectedField === "part_type"
? predefinedPartTypes.map((type) => ({
label: type,
value: type
}))
: predefinedModLbrTypes.map((type) => ({
label: type,
value: type
}))
}
/>
) : (
<Input />
)}
</Form.Item>
</Col>
)}
{/* Update Field */}
<Col span={4}>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.update_field")}
name={[field.name, "update_field"]}
>
<Select
options={fieldSelectOptions}
allowClear
onClear={() =>
form.setFields([{ name: ["md_parts_scan", index, "update_field"], value: null }])
}
/>
</Form.Item>
</Col>
{/* Update Field */}
<Col span={4}>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.update_value")}
name={[field.name, "update_value"]}
dependencies={[["md_parts_scan", index, "update_field"]]}
tooltip={t("bodyshop.tooltips.md_parts_scan.update_value_tooltip")}
rules={[
{
required: form.getFieldValue(["md_parts_scan", index, "update_field"]),
message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.update_value")
})
}
]}
>
<Input />
</Form.Item>
</Col>
</Row>
</LayoutFormRow>
</Form.Item>
);
})}
<Form.Item>
<Button
type="dashed"
onClick={() =>
add({
field: "line_desc",
operation: "contains",
mark_critical: true,
caseInsensitive: true
})
}
style={{ width: "100%" }}
>
{t("bodyshop.actions.addpartsrule")}
</Button>
</Form.Item>
</div> </div>
)} </LayoutFormRow>
</Form.List> )}
</LayoutFormRow> </Form.List>
</div> </div>
); );
} }

View File

@@ -7,6 +7,7 @@ import { ChromePicker } from "react-color";
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 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";
@@ -405,106 +406,116 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
{Production_List_Status_Colors.treatment === "on" && ( {Production_List_Status_Colors.treatment === "on" && (
<LayoutFormRow grow header={t("bodyshop.fields.statuses.production_colors")} id="production_colors"> <Form.List name={["md_ro_statuses", "production_colors"]}>
<Form.List name={["md_ro_statuses", "production_colors"]}> {(fields, { add, remove }) => {
{(fields, { add, remove }) => { return (
return ( <LayoutFormRow
grow
header={t("bodyshop.fields.statuses.production_colors")}
id="production_colors"
actions={[
<Button
key="add-production-status-color"
type="primary"
block
onClick={() => {
add({
color: { ...DEFAULT_TRANSLUCENT_CARD_COLOR }
});
}}
>
{t("bodyshop.actions.add_production_status_color")}
</Button>
]}
>
<div> <div>
<Space size="large" wrap align="start"> {fields.length === 0 ? (
{fields.map((field, index) => { <ConfigListEmptyState actionLabel={t("bodyshop.actions.add_production_status_color")} />
const productionColor = productionColors[field.name] || {}; ) : (
const productionColorSurfaceStyles = getTintedCardSurfaceStyles(productionColor.color); <Space size="large" wrap align="start">
const selectedProductionColorStatuses = productionColors {fields.map((field, index) => {
.map((item) => item?.status) const productionColor = productionColors[field.name] || {};
.filter(Boolean); const productionColorSurfaceStyles = getTintedCardSurfaceStyles(productionColor.color);
const productionColorStatusOptions = [ const selectedProductionColorStatuses = productionColors
...new Set([productionColor.status, ...availableProductionStatuses]) .map((item) => item?.status)
] .filter(Boolean);
.filter(Boolean) const productionColorStatusOptions = [
.filter( ...new Set([productionColor.status, ...availableProductionStatuses])
(status) => ]
status === productionColor.status || !selectedProductionColorStatuses.includes(status) .filter(Boolean)
); .filter(
(status) =>
status === productionColor.status || !selectedProductionColorStatuses.includes(status)
);
return ( return (
<LayoutFormRow <LayoutFormRow
key={field.key} key={field.key}
noDivider noDivider
title={ title={
<Form.Item <Form.Item
noStyle noStyle
key={`${index}status`} key={`${index}status`}
name={[field.name, "status"]} name={[field.name, "status"]}
rules={[ rules={[
{ {
required: true required: true
//message: t("general.validation.required"), //message: t("general.validation.required"),
} }
]} ]}
> >
<Select <Select
className="production-status-color-title-select" className="production-status-color-title-select"
variant="borderless" variant="borderless"
placeholder={getFormListItemTitle( placeholder={getFormListItemTitle(
t("jobs.fields.status"), t("jobs.fields.status"),
index, index,
productionColor.status productionColor.status
)} )}
options={productionColorStatusOptions.map((item) => ({ options={productionColorStatusOptions.map((item) => ({
value: item, value: item,
label: item label: item
}))} }))}
/>
</Form.Item>
}
extra={
<Button
type="text"
danger
icon={<DeleteFilled />}
onClick={() => {
remove(field.name);
}}
/> />
</Form.Item> }
} {...productionColorSurfaceStyles}
extra={ style={{ width: 260, marginBottom: 0 }}
<Button >
type="text" <div>
icon={<DeleteFilled />} <Form.Item
onClick={() => { key={`${index}color`}
remove(field.name); name={[field.name, "color"]}
}} rules={[
/> {
} required: true
{...productionColorSurfaceStyles} //message: t("general.validation.required"),
style={{ width: 260, marginBottom: 0 }} }
> ]}
<div> >
<Form.Item <ColorPicker />
key={`${index}color`} </Form.Item>
name={[field.name, "color"]} </div>
rules={[ </LayoutFormRow>
{ );
required: true })}
//message: t("general.validation.required"), </Space>
} )}
]}
>
<ColorPicker />
</Form.Item>
</div>
</LayoutFormRow>
);
})}
</Space>
<Form.Item style={{ marginTop: 8 }}>
<Button
type="dashed"
onClick={() => {
add({
color: { ...DEFAULT_TRANSLUCENT_CARD_COLOR }
});
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div> </div>
); </LayoutFormRow>
}} );
</Form.List> }}
</LayoutFormRow> </Form.List>
)} )}
</SelectorDiv> </SelectorDiv>
); );

View File

@@ -7,6 +7,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; 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 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 {
@@ -227,195 +228,74 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
))} ))}
</Space> </Space>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.apptcolors")} id="apptcolors"> <Form.List name={["appt_colors"]}>
<Form.List name={["appt_colors"]}> {(fields, { add, remove, move }) => {
{(fields, { add, remove, move }) => { return (
return ( <LayoutFormRow
header={t("bodyshop.labels.apptcolors")}
id="apptcolors"
actions={[
<Button
key="add-appointment-color"
type="primary"
block
onClick={() => {
add({
color: {
...DEFAULT_TRANSLUCENT_PICKER_COLOR,
rgb: { ...DEFAULT_TRANSLUCENT_PICKER_COLOR.rgb }
}
});
}}
>
{t("bodyshop.actions.addapptcolor")}
</Button>
]}
>
<div> <div>
{fields.map((field, index) => { {fields.length === 0 ? (
const appointmentColor = <ConfigListEmptyState actionLabel={t("bodyshop.actions.addapptcolor")} />
appointmentColors[field.name] || form.getFieldValue(["appt_colors", field.name]) || {}; ) : (
const appointmentColorSurfaceStyles = getTintedCardSurfaceStyles(appointmentColor.color); fields.map((field, index) => {
const appointmentColor =
return ( appointmentColors[field.name] || form.getFieldValue(["appt_colors", field.name]) || {};
<Form.Item key={field.key}> const appointmentColorSurfaceStyles = getTintedCardSurfaceStyles(appointmentColor.color);
<LayoutFormRow
noDivider
title={
<div style={{ minWidth: 180, maxWidth: "100%" }}>
<Form.Item
noStyle
key={`${index}aptcolorlabel`}
name={[field.name, "label"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input
size="small"
placeholder={t("bodyshop.fields.appt_colors.label")}
style={SECTION_TITLE_INPUT_STYLE}
/>
</Form.Item>
</div>
}
extra={
<Space align="center" size="small">
<Button
type="text"
icon={<DeleteFilled />}
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
orientation="horizontal"
/>
</Space>
}
{...appointmentColorSurfaceStyles}
>
<Form.Item
key={`${index}aptcolorcolor`}
name={[field.name, "color"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<ColorpickerFormItemComponent styles={APPOINTMENT_COLOR_PICKER_STYLES} />
</Form.Item>
</LayoutFormRow>
</Form.Item>
);
})}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add({
color: {
...DEFAULT_TRANSLUCENT_PICKER_COLOR,
rgb: { ...DEFAULT_TRANSLUCENT_PICKER_COLOR.rgb }
}
});
}}
style={{ width: "100%" }}
>
{t("bodyshop.actions.addapptcolor")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</LayoutFormRow>
{HasFeatureAccess({ featureName: "smartscheduling", bodyshop }) && (
<LayoutFormRow header={t("bodyshop.labels.ssbuckets")} id="ssbuckets">
<Form.List name={["ssbuckets"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => {
const schedulingBucket =
schedulingBuckets[field.name] || form.getFieldValue(["ssbuckets", field.name]) || {};
const schedulingBucketSurfaceStyles = getTintedCardSurfaceStyles(schedulingBucket.color);
return ( return (
<Form.Item key={field.key}> <Form.Item key={field.key}>
<LayoutFormRow <LayoutFormRow
noDivider noDivider
title={ title={
<div style={SECTION_TITLE_INPUT_ROW_STYLE}> <div style={{ minWidth: 180, maxWidth: "100%" }}>
<div style={SECTION_TITLE_INPUT_GROUP_STYLE}> <Form.Item
<div style={SECTION_TITLE_INPUT_LABEL_STYLE}>{t("bodyshop.fields.ssbuckets.id")}</div> noStyle
<Form.Item key={`${index}aptcolorlabel`}
noStyle name={[field.name, "label"]}
key={`${index}id`} rules={[
name={[field.name, "id"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
}
]}
>
<Input
size="small"
placeholder={t("bodyshop.fields.ssbuckets.id")}
style={{
...SECTION_TITLE_INPUT_STYLE,
width: 72
}}
/>
</Form.Item>
</div>
<div
style={{
...SECTION_TITLE_INPUT_GROUP_STYLE,
flex: 1,
minWidth: 0
}}
> >
<div style={SECTION_TITLE_INPUT_LABEL_STYLE}> <Input
{t("bodyshop.fields.ssbuckets.label")} size="small"
</div> placeholder={t("bodyshop.fields.appt_colors.label")}
<Form.Item style={SECTION_TITLE_INPUT_STYLE}
noStyle />
key={`${index}label`} </Form.Item>
name={[field.name, "label"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input
size="small"
placeholder={t("bodyshop.fields.ssbuckets.label")}
style={{
...SECTION_TITLE_INPUT_STYLE,
width: "100%"
}}
/>
</Form.Item>
</div>
</div> </div>
} }
extra={ extra={
<Space align="center" size="small"> <Space align="center" size="small">
<Button <Button
type="text" type="text"
danger
icon={<DeleteFilled />} icon={<DeleteFilled />}
onClick={() => { onClick={() => {
remove(field.name); remove(field.name);
}} }}
/> />
<Tooltip title={t("bodyshop.tooltips.reset-color")}>
<Button
type="text"
icon={<ReloadOutlined />}
onClick={() => {
form.setFieldValue(["ssbuckets", field.name, "color"]);
form.setFields([
{
name: ["ssbuckets", field.name, "color"],
touched: true
}
]);
}}
/>
</Tooltip>
<FormListMoveArrows <FormListMoveArrows
move={move} move={move}
index={index} index={index}
@@ -424,74 +304,213 @@ export function ShopInfoSchedulingComponent({ form, bodyshop }) {
/> />
</Space> </Space>
} }
{...schedulingBucketSurfaceStyles} {...appointmentColorSurfaceStyles}
> >
<div className="shop-info-scheduling__bucket-card-body"> <Form.Item
<div className="shop-info-scheduling__bucket-card-fields"> key={`${index}aptcolorcolor`}
<Form.Item name={[field.name, "color"]}
label={t("bodyshop.fields.ssbuckets.gte")} rules={[
key={`${index}gte`} {
name={[field.name, "gte"]} required: true
rules={[ //message: t("general.validation.required"),
{ }
required: true ]}
//message: t("general.validation.required"), >
} <ColorpickerFormItemComponent styles={APPOINTMENT_COLOR_PICKER_STYLES} />
]} </Form.Item>
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.ssbuckets.lt")}
key={`${index}lt`}
name={[field.name, "lt"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.ssbuckets.target")}
key={`${index}target`}
name={[field.name, "target"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<InputNumber />
</Form.Item>
</div>
<div className="shop-info-scheduling__bucket-card-color">
<Form.Item key={`${index}color`} name={[field.name, "color"]}>
<ColorPicker styles={SCHEDULING_BUCKET_COLOR_PICKER_STYLES} />
</Form.Item>
</div>
</div>
</LayoutFormRow> </LayoutFormRow>
</Form.Item> </Form.Item>
); );
})} })
<Form.Item> )}
<Button </div>
type="dashed" </LayoutFormRow>
onClick={() => { );
add({ }}
color: { ...DEFAULT_TRANSLUCENT_CARD_COLOR } </Form.List>
}); {HasFeatureAccess({ featureName: "smartscheduling", bodyshop }) && (
}} <Form.List name={["ssbuckets"]}>
style={{ width: "100%" }} {(fields, { add, remove, move }) => {
> return (
{t("bodyshop.actions.addbucket")} <LayoutFormRow
</Button> header={t("bodyshop.labels.ssbuckets")}
</Form.Item> id="ssbuckets"
actions={[
<Button
key="add-job-size-definition"
type="primary"
block
onClick={() => {
add({
color: { ...DEFAULT_TRANSLUCENT_CARD_COLOR }
});
}}
>
{t("bodyshop.actions.addbucket")}
</Button>
]}
>
<div>
{fields.length === 0 ? (
<ConfigListEmptyState actionLabel={t("bodyshop.actions.addbucket")} />
) : (
fields.map((field, index) => {
const schedulingBucket =
schedulingBuckets[field.name] || form.getFieldValue(["ssbuckets", field.name]) || {};
const schedulingBucketSurfaceStyles = getTintedCardSurfaceStyles(schedulingBucket.color);
return (
<Form.Item key={field.key}>
<LayoutFormRow
noDivider
title={
<div style={SECTION_TITLE_INPUT_ROW_STYLE}>
<div style={SECTION_TITLE_INPUT_GROUP_STYLE}>
<div style={SECTION_TITLE_INPUT_LABEL_STYLE}>{t("bodyshop.fields.ssbuckets.id")}</div>
<Form.Item
noStyle
key={`${index}id`}
name={[field.name, "id"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input
size="small"
placeholder={t("bodyshop.fields.ssbuckets.id")}
style={{
...SECTION_TITLE_INPUT_STYLE,
width: 72
}}
/>
</Form.Item>
</div>
<div
style={{
...SECTION_TITLE_INPUT_GROUP_STYLE,
flex: 1,
minWidth: 0
}}
>
<div style={SECTION_TITLE_INPUT_LABEL_STYLE}>
{t("bodyshop.fields.ssbuckets.label")}
</div>
<Form.Item
noStyle
key={`${index}label`}
name={[field.name, "label"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input
size="small"
placeholder={t("bodyshop.fields.ssbuckets.label")}
style={{
...SECTION_TITLE_INPUT_STYLE,
width: "100%"
}}
/>
</Form.Item>
</div>
</div>
}
extra={
<Space align="center" size="small">
<Button
type="text"
danger
icon={<DeleteFilled />}
onClick={() => {
remove(field.name);
}}
/>
<Tooltip title={t("bodyshop.tooltips.reset-color")}>
<Button
type="text"
icon={<ReloadOutlined />}
onClick={() => {
form.setFieldValue(["ssbuckets", field.name, "color"]);
form.setFields([
{
name: ["ssbuckets", field.name, "color"],
touched: true
}
]);
}}
/>
</Tooltip>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
orientation="horizontal"
/>
</Space>
}
{...schedulingBucketSurfaceStyles}
>
<div className="shop-info-scheduling__bucket-card-body">
<div className="shop-info-scheduling__bucket-card-fields">
<Form.Item
label={t("bodyshop.fields.ssbuckets.gte")}
key={`${index}gte`}
name={[field.name, "gte"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.ssbuckets.lt")}
key={`${index}lt`}
name={[field.name, "lt"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.ssbuckets.target")}
key={`${index}target`}
name={[field.name, "target"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<InputNumber />
</Form.Item>
</div>
<div className="shop-info-scheduling__bucket-card-color">
<Form.Item key={`${index}color`} name={[field.name, "color"]}>
<ColorPicker styles={SCHEDULING_BUCKET_COLOR_PICKER_STYLES} />
</Form.Item>
</div>
</div>
</LayoutFormRow>
</Form.Item>
);
})
)}
</div> </div>
); </LayoutFormRow>
}} );
</Form.List> }}
</LayoutFormRow> </Form.List>
)} )}
</div> </div>
); );

View File

@@ -3,6 +3,7 @@ import { Button, Form, Input, Select, Space } from "antd";
import { useTranslation } from "react-i18next"; 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 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,
@@ -23,133 +24,131 @@ export default function ShopInfoSpeedPrint() {
}); });
return ( return (
<LayoutFormRow header={t("bodyshop.labels.speedprint_configurations")}> <Form.List name={["speedprint"]}>
<Form.List name={["speedprint"]}> {(fields, { add, remove, move }) => {
{(fields, { add, remove, move }) => { return (
return ( <LayoutFormRow
header={t("bodyshop.labels.speedprint_configurations")}
actions={[
<Button
key="add-speedprint"
type="primary"
block
onClick={() => {
add();
}}
>
{t("bodyshop.actions.addspeedprint")}
</Button>
]}
>
<div> <div>
{fields.map((field, index) => { {fields.length === 0 ? (
return ( <ConfigListEmptyState actionLabel={t("bodyshop.actions.addspeedprint")} />
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}> ) : (
<LayoutFormRow fields.map((field, index) => {
noDivider return (
title={ <Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
<div style={INLINE_TITLE_ROW_STYLE}> <LayoutFormRow
<HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} /> noDivider
<div title={
style={{ <div style={INLINE_TITLE_ROW_STYLE}>
...INLINE_TITLE_GROUP_STYLE, <HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} />
flex: "0 1 180px" <div style={INLINE_TITLE_GROUP_STYLE}>
}} <div style={INLINE_TITLE_LABEL_STYLE}>{t("bodyshop.fields.speedprint.id")}</div>
> <Form.Item
<div style={INLINE_TITLE_LABEL_STYLE}>{t("bodyshop.fields.speedprint.id")}</div> noStyle
<Form.Item name={[field.name, "id"]}
noStyle rules={[
name={[field.name, "id"]} {
rules={[ required: true
{ //message: t("general.validation.required"),
required: true }
//message: t("general.validation.required"), ]}
} >
]} <Input
> size="small"
<Input placeholder={t("bodyshop.fields.speedprint.id")}
size="small" style={{
placeholder={t("bodyshop.fields.speedprint.id")} ...INLINE_TITLE_INPUT_STYLE,
style={{ width: "100%"
...INLINE_TITLE_INPUT_STYLE, }}
width: "100%" />
}} </Form.Item>
/> </div>
</Form.Item> <div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
<div style={INLINE_TITLE_GROUP_STYLE}>
<div style={INLINE_TITLE_LABEL_STYLE}>{t("bodyshop.fields.speedprint.label")}</div>
<Form.Item
noStyle
name={[field.name, "label"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input
size="small"
placeholder={t("bodyshop.fields.speedprint.label")}
style={{
...INLINE_TITLE_INPUT_STYLE,
width: "100%"
}}
/>
</Form.Item>
</div>
</div> </div>
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} /> }
<div wrapTitle
style={{ extra={
...INLINE_TITLE_GROUP_STYLE, <Space align="center" size="small">
flex: "1 1 280px" <Button
}} type="text"
> danger
<div style={INLINE_TITLE_LABEL_STYLE}>{t("bodyshop.fields.speedprint.label")}</div> icon={<DeleteFilled />}
<Form.Item onClick={() => {
noStyle remove(field.name);
name={[field.name, "label"]} }}
rules={[ />
{ <FormListMoveArrows
required: true move={move}
//message: t("general.validation.required"), index={index}
} total={fields.length}
]} orientation="horizontal"
> />
<Input </Space>
size="small" }
placeholder={t("bodyshop.fields.speedprint.label")}
style={{
...INLINE_TITLE_INPUT_STYLE,
width: "100%"
}}
/>
</Form.Item>
</div>
</div>
}
wrapTitle
extra={
<Space align="center" size="small">
<Button
type="text"
icon={<DeleteFilled />}
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
orientation="horizontal"
/>
</Space>
}
>
<Form.Item
name={[field.name, "templates"]}
label={t("bodyshop.fields.speedprint.templates")}
rules={[
{
required: true,
//message: t("general.validation.required"),
type: "array"
}
]}
> >
<Select <Form.Item
mode="multiple" name={[field.name, "templates"]}
options={Object.keys(TemplateListGenerated).map((key) => ({ label={t("bodyshop.fields.speedprint.templates")}
value: TemplateListGenerated[key].key, rules={[
label: TemplateListGenerated[key].title {
}))} required: true,
/> //message: t("general.validation.required"),
</Form.Item> type: "array"
</LayoutFormRow> }
</Form.Item> ]}
); >
})} <Select
<Form.Item> mode="multiple"
<Button options={Object.keys(TemplateListGenerated).map((key) => ({
type="dashed" value: TemplateListGenerated[key].key,
onClick={() => { label: TemplateListGenerated[key].title
add(); }))}
}} />
style={{ width: "100%" }} </Form.Item>
> </LayoutFormRow>
{t("bodyshop.actions.addspeedprint")} </Form.Item>
</Button> );
</Form.Item> })
)}
</div> </div>
); </LayoutFormRow>
}} );
</Form.List> }}
</LayoutFormRow> </Form.List>
); );
} }

View File

@@ -3,6 +3,7 @@ import { Button, Checkbox, Col, Form, Input, InputNumber, Row, Select, Space, Sw
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 { 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 LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -78,208 +79,216 @@ export function ShopInfoTaskPresets({ bodyshop }) {
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.md_tasks_presets")}> <Form.List
<Form.List name={["md_tasks_presets", "presets"]}
name={["md_tasks_presets", "presets"]} rules={[
rules={[ {
{ validator: async (_, presets) => {
validator: async (_, presets) => { const allocationErrors = getTaskPresetAllocationErrors(presets, t);
const allocationErrors = getTaskPresetAllocationErrors(presets, t);
if (allocationErrors.length > 0) { if (allocationErrors.length > 0) {
throw new Error(allocationErrors.join(" ")); throw new Error(allocationErrors.join(" "));
}
} }
} }
]} }
> ]}
{(fields, { add, remove, move }, { errors }) => { >
return ( {(fields, { add, remove, move }, { errors }) => {
return (
<LayoutFormRow
header={t("bodyshop.labels.md_tasks_presets")}
actions={[
<Button
key="add-task-preset"
type="primary"
block
onClick={() => {
add();
}}
>
{t("bodyshop.actions.add_task_preset")}
</Button>
]}
>
<div> <div>
{fields.map((field, index) => { {fields.length === 0 ? (
const taskPreset = taskPresets[field.name] || {}; <ConfigListEmptyState actionLabel={t("bodyshop.actions.add_task_preset")} />
) : (
fields.map((field, index) => {
const taskPreset = taskPresets[field.name] || {};
return ( return (
<Form.Item key={field.key}> <Form.Item key={field.key}>
<LayoutFormRow <LayoutFormRow
noDivider noDivider
title={getFormListItemTitle( title={getFormListItemTitle(
t("bodyshop.fields.md_tasks_presets.name"), t("bodyshop.fields.md_tasks_presets.name"),
index, index,
taskPreset.name, taskPreset.name,
taskPreset.memo taskPreset.memo
)} )}
extra={ extra={
<Space align="center" size="small"> <Space align="center" size="small">
<Button <Button
type="text" type="text"
icon={<DeleteFilled />} danger
onClick={() => { icon={<DeleteFilled />}
remove(field.name); onClick={() => {
}} remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
orientation="horizontal"
/>
</Space>
}
>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.name")}
key={`${index}name`}
name={[field.name, "name"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input />
</Form.Item>
<Form.Item
span={12}
label={t("bodyshop.fields.md_tasks_presets.hourstype")}
key={`${index}hourstype`}
name={[field.name, "hourstype"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Checkbox.Group>
<Row>
<Col span={4}>
<Checkbox value="LAA" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAA")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAB" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAB")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAD" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAD")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAE" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAE")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAF" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAF")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAG" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAG")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAM" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAM")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAR" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAR")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAS" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAS")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAU" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAU")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LA1" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LA1")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LA2" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LA2")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LA3" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LA3")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LA4" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LA4")}
</Checkbox>
</Col>
</Row>
</Checkbox.Group>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.percent")}
key={`${index}percent`}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={[field.name, "percent"]}
>
<InputNumber min={0} max={100} />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.memo")}
key={`${index}memo`}
name={[field.name, "memo"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.nextstatus")}
key={`${index}nextstatus`}
name={[field.name, "nextstatus"]}
>
<Select
options={bodyshop.md_ro_statuses.production_statuses.map((o) => ({
value: o,
label: o
}))}
/> />
<FormListMoveArrows </Form.Item>
move={move} </LayoutFormRow>
index={index} </Form.Item>
total={fields.length} );
orientation="horizontal" })
/> )}
</Space>
}
>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.name")}
key={`${index}name`}
name={[field.name, "name"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input />
</Form.Item>
<Form.Item
span={12}
label={t("bodyshop.fields.md_tasks_presets.hourstype")}
key={`${index}hourstype`}
name={[field.name, "hourstype"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Checkbox.Group>
<Row>
<Col span={4}>
<Checkbox value="LAA" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAA")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAB" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAB")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAD" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAD")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAE" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAE")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAF" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAF")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAG" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAG")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAM" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAM")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAR" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAR")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAS" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAS")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LAU" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LAU")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LA1" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LA1")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LA2" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LA2")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LA3" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LA3")}
</Checkbox>
</Col>
<Col span={4}>
<Checkbox value="LA4" style={{ lineHeight: "32px" }}>
{t("joblines.fields.lbr_types.LA4")}
</Checkbox>
</Col>
</Row>
</Checkbox.Group>
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.percent")}
key={`${index}percent`}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={[field.name, "percent"]}
>
<InputNumber min={0} max={100} />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.memo")}
key={`${index}memo`}
name={[field.name, "memo"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.md_tasks_presets.nextstatus")}
key={`${index}nextstatus`}
name={[field.name, "nextstatus"]}
>
<Select
options={bodyshop.md_ro_statuses.production_statuses.map((o) => ({
value: o,
label: o
}))}
/>
</Form.Item>
</LayoutFormRow>
</Form.Item>
);
})}
<Form.ErrorList errors={errors} /> <Form.ErrorList errors={errors} />
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("bodyshop.actions.add_task_preset")}
</Button>
</Form.Item>
</div> </div>
); </LayoutFormRow>
}} );
</Form.List> }}
</LayoutFormRow> </Form.List>
</> </>
); );
} }

View File

@@ -13,6 +13,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/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 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 { import {
@@ -209,8 +211,8 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
<Card <Card
title={isTeamHydrating ? undefined : teamCardTitle} title={isTeamHydrating ? undefined : teamCardTitle}
extra={ extra={
<Button type="primary" onClick={() => form.submit()} disabled={isTeamHydrating}> <Button type="primary" onClick={() => form.submit()} disabled={isTeamHydrating} style={{ minWidth: 190 }}>
{t("general.actions.save")} {t("employee_teams.actions.save_team")}
</Button> </Button>
} }
> >
@@ -239,8 +241,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
</div> </div>
<div <div
style={{ style={{
...INLINE_TITLE_GROUP_STYLE, ...INLINE_TITLE_SWITCH_GROUP_STYLE,
flex: "0 0 auto",
marginLeft: "auto" marginLeft: "auto"
}} }}
> >
@@ -277,188 +278,181 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
<InputNumber min={0} precision={1} suffix="%" /> <InputNumber min={0} precision={1} suffix="%" />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow title={t("employee_teams.labels.members")}> <Form.List name={["employee_team_members"]}>
<Form.List name={["employee_team_members"]}> {(fields, { add, remove, move }) => {
{(fields, { add, remove, move }) => { return (
return ( <LayoutFormRow
title={t("employee_teams.labels.members")}
actions={[
<Button
key="add-team-member"
type="primary"
block
onClick={() => {
add({
percentage: 0,
payout_method: "hourly",
labor_rates: {},
commission_rates: {}
});
}}
>
{t("employee_teams.actions.newmember")}
</Button>
]}
>
<div> <div>
{fields.map((field, index) => { {fields.length === 0 ? (
return ( <ConfigListEmptyState actionLabel={t("employee_teams.actions.newmember")} />
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}> ) : (
<Form.Item name={[field.name, "id"]} hidden> fields.map((field, index) => {
<Input type="hidden" /> return (
</Form.Item> <Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
<LayoutFormRow <Form.Item name={[field.name, "id"]} hidden>
grow <Input type="hidden" />
title={ </Form.Item>
<div style={INLINE_TITLE_ROW_STYLE}> <LayoutFormRow
<HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} /> grow
<div title={
style={{ <div style={INLINE_TITLE_ROW_STYLE}>
...INLINE_TITLE_GROUP_STYLE, <HolderOutlined style={INLINE_TITLE_HANDLE_STYLE} />
flex: "1 1 320px" <div style={INLINE_TITLE_GROUP_STYLE}>
}} <div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.employeeid")}</div>
> <Form.Item
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.employeeid")}</div> noStyle
<Form.Item name={[field.name, "employeeid"]}
noStyle rules={[
name={[field.name, "employeeid"]} {
rules={[ required: true
{ }
required: true ]}
} >
]} <EmployeeSearchSelectComponent options={bodyshop.employees} />
> </Form.Item>
<EmployeeSearchSelectComponent options={bodyshop.employees} /> </div>
</Form.Item> <div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
<div style={INLINE_TITLE_GROUP_STYLE}>
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.allocation")}</div>
<Form.Item
noStyle
name={[field.name, "percentage"]}
rules={[
{
required: true
}
]}
>
<InputNumber
min={0}
max={100}
precision={2}
size="small"
aria-label={t("employee_teams.fields.allocation")}
suffix="%"
style={{
...INLINE_TITLE_INPUT_STYLE,
width: "100%"
}}
/>
</Form.Item>
</div>
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
<div style={INLINE_TITLE_GROUP_STYLE}>
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.payout_method")}</div>
<Form.Item
noStyle
key={`${index}-payout-method`}
name={[field.name, "payout_method"]}
initialValue="hourly"
rules={[
{
required: true
}
]}
>
<Select
aria-label={t("employee_teams.fields.payout_method")}
size="small"
options={payoutMethodOptions}
style={{ width: "100%" }}
styles={{
selector: INLINE_TITLE_INPUT_STYLE
}}
/>
</Form.Item>
</div>
</div> </div>
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} /> }
<div wrapTitle
style={{ extra={
...INLINE_TITLE_GROUP_STYLE, <Space align="center" size="small">
flex: "0 1 170px" <Button
}} type="text"
danger
icon={<DeleteFilled />}
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
orientation="horizontal"
/>
</Space>
}
>
<div>
<Form.Item
noStyle
dependencies={[["employee_team_members", field.name, "payout_method"]]}
> >
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.allocation")}</div> {() => {
<Form.Item const payoutMethod =
noStyle form.getFieldValue(["employee_team_members", field.name, "payout_method"]) ||
name={[field.name, "percentage"]} "hourly";
rules={[ const fieldName = payoutMethod === "commission" ? "commission_rates" : "labor_rates";
{
required: true
}
]}
>
<InputNumber
min={0}
max={100}
precision={2}
size="small"
aria-label={t("employee_teams.fields.allocation")}
suffix="%"
style={{
...INLINE_TITLE_INPUT_STYLE,
width: "100%"
}}
/>
</Form.Item>
</div>
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
<div
style={{
...INLINE_TITLE_GROUP_STYLE,
flex: "0 1 260px"
}}
>
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.payout_method")}</div>
<Form.Item
noStyle
key={`${index}-payout-method`}
name={[field.name, "payout_method"]}
initialValue="hourly"
rules={[
{
required: true
}
]}
>
<Select
aria-label={t("employee_teams.fields.payout_method")}
size="small"
options={payoutMethodOptions}
style={{ width: "100%" }}
styles={{
selector: INLINE_TITLE_INPUT_STYLE
}}
/>
</Form.Item>
</div>
</div>
}
wrapTitle
extra={
<Space align="center" size="small">
<Button
type="text"
icon={<DeleteFilled />}
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
orientation="horizontal"
/>
</Space>
}
>
<div>
<Form.Item
noStyle
dependencies={[["employee_team_members", field.name, "payout_method"]]}
>
{() => {
const payoutMethod =
form.getFieldValue(["employee_team_members", field.name, "payout_method"]) ||
"hourly";
const fieldName = payoutMethod === "commission" ? "commission_rates" : "labor_rates";
return ( return (
<Row gutter={[16, 0]}> <Row gutter={[16, 0]}>
{LABOR_TYPES.map((laborType) => ( {LABOR_TYPES.map((laborType) => (
<Col <Col
{...TEAM_MEMBER_RATE_FIELD_COLS} {...TEAM_MEMBER_RATE_FIELD_COLS}
key={`${index}-${fieldName}-${laborType}`} key={`${index}-${fieldName}-${laborType}`}
>
<Form.Item
label={t(`joblines.fields.lbr_types.${laborType}`)}
name={[field.name, fieldName, laborType]}
rules={[
{
required: true
}
]}
> >
{payoutMethod === "commission" ? ( <Form.Item
<InputNumber min={0} max={100} precision={2} suffix="%" /> label={t(`joblines.fields.lbr_types.${laborType}`)}
) : ( name={[field.name, fieldName, laborType]}
<CurrencyInput prefix="$" /> rules={[
)} {
</Form.Item> required: true
</Col> }
))} ]}
</Row> >
); {payoutMethod === "commission" ? (
}} <InputNumber min={0} max={100} precision={2} suffix="%" />
</Form.Item> ) : (
</div> <CurrencyInput prefix="$" />
</LayoutFormRow> )}
</Form.Item> </Form.Item>
); </Col>
})} ))}
<Form.Item> </Row>
<Button );
type="dashed" }}
onClick={() => { </Form.Item>
add({ </div>
percentage: 0, </LayoutFormRow>
payout_method: "hourly", </Form.Item>
labor_rates: {}, );
commission_rates: {} })
}); )}
}}
style={{ width: "100%" }}
>
{t("employee_teams.actions.newmember")}
</Button>
</Form.Item>
</div> </div>
); </LayoutFormRow>
}} );
</Form.List> }}
</LayoutFormRow> </Form.List>
</Form> </Form>
)} )}
</Card> </Card>

View File

@@ -42,9 +42,11 @@ vi.mock("react-i18next", () => ({
"employee_teams.options.commission": "Commission", "employee_teams.options.commission": "Commission",
"employee_teams.options.commission_percentage": "Commission", "employee_teams.options.commission_percentage": "Commission",
"employee_teams.actions.newmember": "New Team Member", "employee_teams.actions.newmember": "New Team Member",
"employee_teams.actions.save_team": "Save Employee Team",
"employee_teams.errors.minimum_one_member": "Add at least one team member.", "employee_teams.errors.minimum_one_member": "Add at least one team member.",
"employee_teams.errors.duplicate_member": "Team members must be unique.", "employee_teams.errors.duplicate_member": "Team members must be unique.",
"employee_teams.errors.allocation_total_exact": "Allocation must total exactly 100%.", "employee_teams.errors.allocation_total_exact": "Allocation must total exactly 100%.",
"general.labels.click_to_begin": `Click ${values.action ?? ""} to begin`,
"general.actions.save": "Save", "general.actions.save": "Save",
"employees.successes.save": "Saved" "employees.successes.save": "Saved"
}; };
@@ -101,11 +103,12 @@ vi.mock("../form-items-formatted/currency-form-item.component", () => ({
})); }));
vi.mock("../layout-form-row/layout-form-row.component", () => ({ vi.mock("../layout-form-row/layout-form-row.component", () => ({
default: ({ title, extra, children }) => ( default: ({ title, extra, actions, children }) => (
<div> <div>
{title} {title}
{extra} {extra}
{children} {children}
{actions}
</div> </div>
) )
})); }));
@@ -211,7 +214,7 @@ describe("ShopEmployeeTeamsFormComponent", () => {
rate: 27.5 rate: 27.5
}); });
fireEvent.click(screen.getByRole("button", { name: "Save" })); fireEvent.click(screen.getByRole("button", { name: "Save Employee Team" }));
await waitFor(() => { await waitFor(() => {
expect(insertEmployeeTeamMock).toHaveBeenCalledWith({ expect(insertEmployeeTeamMock).toHaveBeenCalledWith({

View File

@@ -2,6 +2,8 @@ import { Button } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx";
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 }) {
@@ -9,13 +11,28 @@ export default function ShopEmployeeTeamsListComponent({ loading, employee_teams
const history = useNavigate(); const history = useNavigate();
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const navigateToTeam = (employeeTeamId) => {
history({
search: queryString.stringify({
...search,
employeeTeamId
})
});
};
const clearTeamSelection = () => {
const { employeeTeamId, ...nextSearch } = search;
void employeeTeamId;
history({
search: queryString.stringify(nextSearch)
});
};
const handleOnRowClick = (record) => { const handleOnRowClick = (record) => {
if (record) { if (record) {
search.employeeTeamId = record.id; navigateToTeam(record.id);
history({ search: queryString.stringify(search) });
} else { } else {
delete search.employeeTeamId; clearTeamSelection();
history({ search: queryString.stringify(search) });
} }
}; };
const columns = [ const columns = [
@@ -27,43 +44,38 @@ export default function ShopEmployeeTeamsListComponent({ loading, employee_teams
]; ];
return ( return (
<div> <LayoutFormRow
<ResponsiveTable title={t("bodyshop.labels.employee_teams")}
title={() => { actions={[
return ( <Button key="new-team" type="primary" block onClick={() => navigateToTeam("new")}>
<Button {t("employee_teams.actions.new")}
type="primary" </Button>
onClick={() => { ]}
search.employeeTeamId = "new"; >
history({ search: queryString.stringify(search) }); {employee_teams.length === 0 ? (
}} <ConfigListEmptyState actionLabel={t("employee_teams.actions.new")} />
> ) : (
{t("employee_teams.actions.new")} <ResponsiveTable
</Button> loading={loading}
); pagination={{ placement: "top" }}
}} columns={columns}
loading={loading} mobileColumnKeys={["name"]}
pagination={{ placement: "top" }} rowKey="id"
columns={columns} dataSource={employee_teams}
mobileColumnKeys={["name"]} rowSelection={{
rowKey="id" onSelect: (props) => navigateToTeam(props.id),
dataSource={employee_teams} type: "radio",
rowSelection={{ selectedRowKeys: [search.employeeTeamId]
onSelect: (props) => { }}
search.employeeTeamId = props.id; onRow={(record) => {
history({ search: queryString.stringify(search) }); return {
}, onClick: () => {
type: "radio", handleOnRowClick(record);
selectedRowKeys: [search.employeeTeamId] }
}} };
onRow={(record) => { }}
return { />
onClick: () => { )}
handleOnRowClick(record); </LayoutFormRow>
}
};
}}
/>
</div>
); );
} }

View File

@@ -6,6 +6,7 @@ import { createStructuredSelector } from "reselect";
import { QUERY_SHOP_ASSOCIATIONS } from "../../graphql/user.queries"; import { QUERY_SHOP_ASSOCIATIONS } from "../../graphql/user.queries";
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 LayoutFormRow from "../layout-form-row/layout-form-row.component";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import ResponsiveTable from "../responsive-table/responsive-table.component"; import ResponsiveTable from "../responsive-table/responsive-table.component";
import ShopUsersAuthEdit from "../shop-users-auth-edit/shop-users-auth-edit.component"; import ShopUsersAuthEdit from "../shop-users-auth-edit/shop-users-auth-edit.component";
@@ -66,7 +67,7 @@ export function ShopInfoUsersComponent({ bodyshop }) {
return <AlertComponent type="error" title={JSON.stringify(error)} />; return <AlertComponent type="error" title={JSON.stringify(error)} />;
} }
return ( return (
<div> <LayoutFormRow title={t("bodyshop.labels.licensing")}>
<ResponsiveTable <ResponsiveTable
loading={loading} loading={loading}
pagination={{ placement: "top" }} pagination={{ placement: "top" }}
@@ -75,6 +76,6 @@ export function ShopInfoUsersComponent({ bodyshop }) {
rowKey="id" rowKey="id"
dataSource={data && data.associations} dataSource={data && data.associations}
/> />
</div> </LayoutFormRow>
); );
} }

View File

@@ -292,7 +292,23 @@
}, },
"bodyshop": { "bodyshop": {
"actions": { "actions": {
"add_adjuster": "Add Adjuster",
"add_control_number": "Add Control Number",
"add_cost_center": "Add Cost Center",
"add_courtesy_car_rate_preset": "Add Courtesy Car Contract Rate Preset",
"add_delivery_checklist_item": "Add Delivery Checklist Item",
"add_dms_allocation": "Add DMS Allocation",
"add_estimator": "Add Estimator",
"add_insurance_company": "Add Insurance Company",
"add_intake_checklist_item": "Add Intake Checklist Item",
"add_jobline_preset": "Add Jobline Preset",
"add_messaging_preset": "Add Messaging Preset",
"add_note_preset": "Add Note Preset",
"add_parts_order_comment": "Add Parts Order Comment",
"add_production_status_color": "Add Production Status Color",
"add_profit_center": "Add Profit Center",
"add_task_preset": "Add Task Preset", "add_task_preset": "Add Task Preset",
"add_to_email_preset": "Add To Email Preset",
"addapptcolor": "Add Appointment Color", "addapptcolor": "Add Appointment Color",
"addbucket": "Add Definition", "addbucket": "Add Definition",
"addpartslocation": "Add Parts Location", "addpartslocation": "Add Parts Location",
@@ -301,6 +317,7 @@
"addtemplate": "Add Template", "addtemplate": "Add Template",
"newlaborrate": "New Labor Rate", "newlaborrate": "New Labor Rate",
"newsalestaxcode": "New Sales Tax Code", "newsalestaxcode": "New Sales Tax Code",
"save_shop_information": "Save Shop Information",
"newstatus": "Add Status", "newstatus": "Add Status",
"testrender": "Test Render" "testrender": "Test Render"
}, },
@@ -1206,7 +1223,8 @@
"employee_teams": { "employee_teams": {
"actions": { "actions": {
"new": "New Team", "new": "New Team",
"newmember": "New Team Member" "newmember": "New Team Member",
"save_team": "Save Employee Team"
}, },
"errors": { "errors": {
"allocation_total_exact": "Team allocation must total exactly 100%.", "allocation_total_exact": "Team allocation must total exactly 100%.",
@@ -1236,9 +1254,11 @@
}, },
"employees": { "employees": {
"actions": { "actions": {
"addrate": "Add Rate",
"addvacation": "Add Vacation", "addvacation": "Add Vacation",
"new": "New Employee", "new": "New Employee",
"newrate": "New Rate", "newrate": "New Rate",
"save_employee": "Save Employee",
"select": "Select Employee" "select": "Select Employee"
}, },
"errors": { "errors": {
@@ -1403,6 +1423,7 @@
"beta": "BETA", "beta": "BETA",
"cancel": "Are you sure you want to cancel? Your changes will not be saved.", "cancel": "Are you sure you want to cancel? Your changes will not be saved.",
"changelog": "Change Log", "changelog": "Change Log",
"click_to_begin": "Click {{action}} to begin",
"clear": "Clear", "clear": "Clear",
"confirmpassword": "Confirm Password", "confirmpassword": "Confirm Password",
"created_at": "Created At", "created_at": "Created At",

View File

@@ -292,7 +292,23 @@
}, },
"bodyshop": { "bodyshop": {
"actions": { "actions": {
"add_adjuster": "",
"add_control_number": "",
"add_cost_center": "",
"add_courtesy_car_rate_preset": "",
"add_delivery_checklist_item": "",
"add_dms_allocation": "",
"add_estimator": "",
"add_insurance_company": "",
"add_intake_checklist_item": "",
"add_jobline_preset": "",
"add_messaging_preset": "",
"add_note_preset": "",
"add_parts_order_comment": "",
"add_production_status_color": "",
"add_profit_center": "",
"add_task_preset": "", "add_task_preset": "",
"add_to_email_preset": "",
"addapptcolor": "", "addapptcolor": "",
"addbucket": "", "addbucket": "",
"addpartslocation": "", "addpartslocation": "",
@@ -301,6 +317,7 @@
"addtemplate": "", "addtemplate": "",
"newlaborrate": "", "newlaborrate": "",
"newsalestaxcode": "", "newsalestaxcode": "",
"save_shop_information": "",
"newstatus": "", "newstatus": "",
"testrender": "" "testrender": ""
}, },
@@ -1206,7 +1223,8 @@
"employee_teams": { "employee_teams": {
"actions": { "actions": {
"new": "", "new": "",
"newmember": "" "newmember": "",
"save_team": ""
}, },
"errors": { "errors": {
"allocation_total_exact": "", "allocation_total_exact": "",
@@ -1236,9 +1254,11 @@
}, },
"employees": { "employees": {
"actions": { "actions": {
"addrate": "",
"addvacation": "", "addvacation": "",
"new": "Nuevo empleado", "new": "Nuevo empleado",
"newrate": "", "newrate": "",
"save_employee": "",
"select": "" "select": ""
}, },
"errors": { "errors": {
@@ -1403,6 +1423,7 @@
"beta": "", "beta": "",
"cancel": "", "cancel": "",
"changelog": "", "changelog": "",
"click_to_begin": "",
"clear": "", "clear": "",
"confirmpassword": "", "confirmpassword": "",
"created_at": "", "created_at": "",

View File

@@ -292,7 +292,23 @@
}, },
"bodyshop": { "bodyshop": {
"actions": { "actions": {
"add_adjuster": "",
"add_control_number": "",
"add_cost_center": "",
"add_courtesy_car_rate_preset": "",
"add_delivery_checklist_item": "",
"add_dms_allocation": "",
"add_estimator": "",
"add_insurance_company": "",
"add_intake_checklist_item": "",
"add_jobline_preset": "",
"add_messaging_preset": "",
"add_note_preset": "",
"add_parts_order_comment": "",
"add_production_status_color": "",
"add_profit_center": "",
"add_task_preset": "", "add_task_preset": "",
"add_to_email_preset": "",
"addapptcolor": "", "addapptcolor": "",
"addbucket": "", "addbucket": "",
"addpartslocation": "", "addpartslocation": "",
@@ -301,6 +317,7 @@
"addtemplate": "", "addtemplate": "",
"newlaborrate": "", "newlaborrate": "",
"newsalestaxcode": "", "newsalestaxcode": "",
"save_shop_information": "",
"newstatus": "", "newstatus": "",
"testrender": "" "testrender": ""
}, },
@@ -1206,7 +1223,8 @@
"employee_teams": { "employee_teams": {
"actions": { "actions": {
"new": "", "new": "",
"newmember": "" "newmember": "",
"save_team": ""
}, },
"errors": { "errors": {
"allocation_total_exact": "", "allocation_total_exact": "",
@@ -1236,9 +1254,11 @@
}, },
"employees": { "employees": {
"actions": { "actions": {
"addrate": "",
"addvacation": "", "addvacation": "",
"new": "Nouvel employé", "new": "Nouvel employé",
"newrate": "", "newrate": "",
"save_employee": "",
"select": "" "select": ""
}, },
"errors": { "errors": {
@@ -1403,6 +1423,7 @@
"beta": "", "beta": "",
"cancel": "", "cancel": "",
"changelog": "", "changelog": "",
"click_to_begin": "",
"clear": "", "clear": "",
"confirmpassword": "", "confirmpassword": "",
"created_at": "", "created_at": "",